diff --git a/NewHorizons/AchievementsPlus/AchievementHandler.cs b/NewHorizons/AchievementsPlus/AchievementHandler.cs index 12fe2f6d..1b629cbc 100644 --- a/NewHorizons/AchievementsPlus/AchievementHandler.cs +++ b/NewHorizons/AchievementsPlus/AchievementHandler.cs @@ -1,13 +1,20 @@ +using NewHorizons.External.Configs; using NewHorizons.Utility; using OWML.ModHelper; using System; +using System.Collections.Generic; +using System.Linq; namespace NewHorizons.AchievementsPlus { public static class AchievementHandler { + public static bool Enabled { get => _enabled; } + private static bool _enabled; private static IAchievements API; + + private static List _achievements; public static void Init() { @@ -22,6 +29,8 @@ namespace NewHorizons.AchievementsPlus _enabled = true; + _achievements = new List(); + // Register base NH achievements NH.WarpDriveAchievement.Init(); NH.MultipleSystemAchievement.Init(); @@ -30,8 +39,33 @@ namespace NewHorizons.AchievementsPlus NH.ProbeLostAchievement.Init(); API.RegisterTranslationsFromFiles(Main.Instance, "Assets/translations"); + + GlobalMessenger.AddListener("DialogueConditionChanged", OnDialogueConditionChanged); } + public static void OnDestroy() + { + if (!_enabled) return; + + GlobalMessenger.RemoveListener("DialogueConditionChanged", OnDialogueConditionChanged); + } + + public static void RegisterAddon(AddonConfig addon, ModBehaviour mod) + { + if (addon.achievements == null) return; + + if (!_enabled) return; + + foreach (var achievement in addon.achievements) + { + _achievements.Add(achievement); + + API.RegisterAchievement(achievement.ID, achievement.secret, mod); + } + } + + public static void RegisterTranslationsFromFiles(ModBehaviour mod, string path) => API.RegisterTranslationsFromFiles(mod, path); + public static void Earn(string unique_id) { if (!_enabled) return; @@ -45,5 +79,49 @@ namespace NewHorizons.AchievementsPlus API.RegisterAchievement(unique_id, secret, mod); } + + public static void OnLearnSignal() + { + if (!_enabled) return; + + foreach (var achievement in _achievements.Where(x => x.signalIDs != null)) + { + CheckAchievement(achievement); + } + } + + public static void OnRevealFact() + { + if (!_enabled) return; + + foreach (var achievement in _achievements.Where(x => x.factIDs != null)) + { + CheckAchievement(achievement); + } + } + + public static void OnDialogueConditionChanged(string s, bool b) => OnSetCondition(); + + public static void OnSetCondition() + { + if (!_enabled) return; + + foreach (var achievement in _achievements.Where(x => x.conditionIDs != null)) + { + CheckAchievement(achievement); + } + } + + private static void CheckAchievement(AchievementInfo achievement) + { + if (!_enabled) return; + + if (API.HasAchievement(achievement.ID)) return; + + if (achievement.IsUnlocked()) + { + Earn(achievement.ID); + } + } } } diff --git a/NewHorizons/AchievementsPlus/AchievementInfo.cs b/NewHorizons/AchievementsPlus/AchievementInfo.cs new file mode 100644 index 00000000..d1789a75 --- /dev/null +++ b/NewHorizons/AchievementsPlus/AchievementInfo.cs @@ -0,0 +1,79 @@ +using NewHorizons.Builder.Props; +using Newtonsoft.Json; +using System.Linq; + +namespace NewHorizons.AchievementsPlus +{ + /// + /// Info for an achievement to be used with the Achievements+ mod. + /// + [JsonObject] + public class AchievementInfo + { + /// + /// The unique ID of the achievement. This must be globally unique, meaning all achivements for + /// you mod should start with something to identify the mod they are from. For example, Real Solar System + /// uses "RSS_" and Signals+ would use "SIGNALS_PLUS_". + /// + public string ID; + + /// + /// Should the name and description of the achievement be hidden until it is unlocked. Good for hiding spoilers! + /// + public bool secret; + + /// + /// A list of facts that must be discovered before this achievement is unlocked. You can also set the achievement + /// to be unlocked by a reveal trigger in Props -> Reveals. Optional. + /// + public string[] factIDs; + + /// + /// A list of signals that must be discovered before this achievement is unlocked. Optional. + /// + public string[] signalIDs; + + /// + /// A list of conditions that must be true before this achievement is unlocked. Conditions can be set via dialogue. Optional. + /// + public string[] conditionIDs; + + // Cache signal ids to the enum + [JsonIgnore] + private SignalName[] _signalIDs; + + public bool IsUnlocked() + { + if (signalIDs != null) + { + if (_signalIDs == null) + { + _signalIDs = signalIDs.Select(x => SignalBuilder.StringToSignalName(x)).ToArray(); + } + + foreach(var signal in _signalIDs) + { + if (!PlayerData.KnowsSignal(signal)) return false; + } + } + + if (factIDs != null) + { + foreach (var fact in factIDs) + { + if (!Locator.GetShipLogManager().IsFactRevealed(fact)) return false; + } + } + + if (conditionIDs != null) + { + foreach (var condition in conditionIDs) + { + if (!DialogueConditionManager.SharedInstance.GetConditionState(condition)) return false; + } + } + + return true; + } + } +} diff --git a/NewHorizons/AchievementsPlus/IAchievements.cs b/NewHorizons/AchievementsPlus/IAchievements.cs index 351acbbf..8505624d 100644 --- a/NewHorizons/AchievementsPlus/IAchievements.cs +++ b/NewHorizons/AchievementsPlus/IAchievements.cs @@ -8,5 +8,6 @@ namespace NewHorizons.AchievementsPlus void RegisterTranslation(string uniqueID, TextTranslation.Language language, string name, string description); void RegisterTranslationsFromFiles(ModBehaviour mod, string folderPath); void EarnAchievement(string uniqueID); + bool HasAchievement(string uniqueID); } } diff --git a/NewHorizons/Builder/Props/SignalBuilder.cs b/NewHorizons/Builder/Props/SignalBuilder.cs index 856e57d6..24da4772 100644 --- a/NewHorizons/Builder/Props/SignalBuilder.cs +++ b/NewHorizons/Builder/Props/SignalBuilder.cs @@ -1,3 +1,4 @@ +using NewHorizons.AchievementsPlus; using NewHorizons.Components; using NewHorizons.External.Modules; using NewHorizons.Utility; @@ -233,7 +234,7 @@ namespace NewHorizons.Builder.Props return customName; } - private static SignalName StringToSignalName(string str) + public static SignalName StringToSignalName(string str) { foreach (SignalName name in Enum.GetValues(typeof(SignalName))) { diff --git a/NewHorizons/Builder/ShipLog/RevealBuilder.cs b/NewHorizons/Builder/ShipLog/RevealBuilder.cs index 97a397c1..43133771 100644 --- a/NewHorizons/Builder/ShipLog/RevealBuilder.cs +++ b/NewHorizons/Builder/ShipLog/RevealBuilder.cs @@ -1,4 +1,5 @@ -using NewHorizons.External.Modules; +using NewHorizons.Components.Achievement; +using NewHorizons.External.Modules; using OWML.Common; using UnityEngine; using Logger = NewHorizons.Utility.Logger; @@ -46,35 +47,77 @@ namespace NewHorizons.Builder.ShipLog private static void MakeTrigger(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) { - SphereShape newShape = MakeShape(go, info, Shape.CollisionMode.Volume); - OWTriggerVolume newVolume = go.AddComponent(); - newVolume._shape = newShape; - ShipLogFactListTriggerVolume volume = go.AddComponent(); - volume._factIDs = info.reveals; + var shape = MakeShape(go, info, Shape.CollisionMode.Volume); + + var volume = go.AddComponent(); + volume._shape = shape; + + if (info.reveals != null) + { + var factRevealVolume = go.AddComponent(); + factRevealVolume._factIDs = info.reveals; + } + + if (!string.IsNullOrEmpty(info.achievementID)) + { + var achievementVolume = go.AddComponent(); + achievementVolume.achievementID = info.achievementID; + } } private static void MakeObservable(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) { go.layer = LayerMask.NameToLayer("Interactible"); - SphereCollider newSphere = go.AddComponent(); - newSphere.radius = info.radius; - OWCollider newCollider = go.AddComponent(); - ShipLogFactObserveTrigger newObserveTrigger = go.AddComponent(); - newObserveTrigger._factIDs = info.reveals; - newObserveTrigger._maxViewDistance = info.maxDistance == -1f ? 2f : info.maxDistance; - newObserveTrigger._maxViewAngle = info.maxAngle; - newObserveTrigger._owCollider = newCollider; - newObserveTrigger._disableColliderOnRevealFact = true; + + var sphere = go.AddComponent(); + sphere.radius = info.radius; + + var collider = go.AddComponent(); + + var maxDistance = info.maxDistance == -1f ? 2f : info.maxDistance; + + if (info.reveals != null) + { + var observeTrigger = go.AddComponent(); + observeTrigger._factIDs = info.reveals; + observeTrigger._maxViewDistance = maxDistance; + observeTrigger._maxViewAngle = info.maxAngle; + observeTrigger._owCollider = collider; + observeTrigger._disableColliderOnRevealFact = true; + } + + if (!string.IsNullOrEmpty(info.achievementID)) + { + var achievementTrigger = go.AddComponent(); + achievementTrigger.achievementID = info.achievementID; + achievementTrigger.disableColliderOnUnlockAchievement = true; + achievementTrigger.maxViewDistance = maxDistance; + achievementTrigger.maxViewAngle = info.maxAngle; + } } private static void MakeSnapshot(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) { - SphereShape newShape = MakeShape(go, info, Shape.CollisionMode.Manual); - ShapeVisibilityTracker newTracker = go.AddComponent(); - newTracker._shapes = new Shape[] { newShape }; - ShipLogFactSnapshotTrigger newSnapshotTrigger = go.AddComponent(); - newSnapshotTrigger._maxDistance = info.maxDistance == -1f ? 200f : info.maxDistance; - newSnapshotTrigger._factIDs = info.reveals; + var shape = MakeShape(go, info, Shape.CollisionMode.Manual); + + var visibilityTracker = go.AddComponent(); + visibilityTracker._shapes = new Shape[] { shape }; + + var maxDistance = info.maxDistance == -1f ? 200f : info.maxDistance; + + if (info.reveals != null) + { + var snapshotTrigger = go.AddComponent(); + snapshotTrigger._maxDistance = maxDistance; + snapshotTrigger._factIDs = info.reveals; + } + + if (!string.IsNullOrEmpty(info.achievementID)) + { + var achievementTrigger = go.AddComponent(); + achievementTrigger.maxDistance = maxDistance; + achievementTrigger.achievementID = info.achievementID; + } } } } diff --git a/NewHorizons/Components/Achievement/AchievementObserveTrigger.cs b/NewHorizons/Components/Achievement/AchievementObserveTrigger.cs new file mode 100644 index 00000000..0b5bb37b --- /dev/null +++ b/NewHorizons/Components/Achievement/AchievementObserveTrigger.cs @@ -0,0 +1,52 @@ +using NewHorizons.AchievementsPlus; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Components.Achievement +{ + public class AchievementObserveTrigger : MonoBehaviour, IObservable + { + public string achievementID; + public float maxViewDistance = 2f; + public float maxViewAngle = 180f; + public bool disableColliderOnUnlockAchievement; + public bool achievementUnlocked; + + private OWCollider _owCollider; + + private void Reset() + { + gameObject.layer = LayerMask.NameToLayer("Interactible"); + } + + private void Awake() + { + if (disableColliderOnUnlockAchievement) + { + _owCollider = gameObject.GetAddComponent(); + } + } + + public void Observe(RaycastHit raycastHit, Vector3 raycastOrigin) + { + float num = Vector3.Angle(raycastHit.point - raycastOrigin, -transform.forward); + if (!achievementUnlocked && raycastHit.distance < maxViewDistance && num < maxViewAngle) + { + if (disableColliderOnUnlockAchievement) + { + _owCollider.SetActivation(false); + } + + AchievementHandler.Earn(achievementID); + + achievementUnlocked = true; + } + } + + public void LoseFocus() { } + } +} diff --git a/NewHorizons/Components/Achievement/AchievementSnapshotTrigger.cs b/NewHorizons/Components/Achievement/AchievementSnapshotTrigger.cs new file mode 100644 index 00000000..d1a6c895 --- /dev/null +++ b/NewHorizons/Components/Achievement/AchievementSnapshotTrigger.cs @@ -0,0 +1,38 @@ +using NewHorizons.AchievementsPlus; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Components.Achievement +{ + // Modified version of ShipLogFactSnapshotTrigger + public class AchievementSnapshotTrigger : MonoBehaviour + { + private void Awake() + { + _visibilityTracker = GetComponent(); + GlobalMessenger.AddListener("ProbeSnapshot", new Callback(OnProbeSnapshot)); + } + + private void OnDestroy() + { + GlobalMessenger.RemoveListener("ProbeSnapshot", new Callback(OnProbeSnapshot)); + } + + private void OnProbeSnapshot(ProbeCamera probeCamera) + { + if (_visibilityTracker != null && _visibilityTracker.IsVisibleToProbe(probeCamera.GetOWCamera()) && (_visibilityTracker.transform.position - probeCamera.transform.position).magnitude < maxDistance) + { + AchievementHandler.Earn(achievementID); + } + } + + public string achievementID; + public float maxDistance = 200f; + + private VisibilityTracker _visibilityTracker; + } +} diff --git a/NewHorizons/Components/Achievement/AchievementVolume.cs b/NewHorizons/Components/Achievement/AchievementVolume.cs new file mode 100644 index 00000000..47a013bc --- /dev/null +++ b/NewHorizons/Components/Achievement/AchievementVolume.cs @@ -0,0 +1,37 @@ +using NewHorizons.AchievementsPlus; +using UnityEngine; + +namespace NewHorizons.Components.Achievement +{ + public class AchievementVolume : MonoBehaviour + { + private void Start() + { + _trigger = gameObject.GetRequiredComponent(); + _trigger.OnEntry += OnEntry; + return; + } + + private void OnDestroy() + { + _trigger.OnEntry -= OnEntry; + } + + private void OnEntry(GameObject hitObj) + { + if ((!player || hitObj.CompareTag("PlayerDetector")) && (!probe || hitObj.CompareTag("ProbeDetector"))) + { + AchievementHandler.Earn(achievementID); + + _trigger.OnEntry -= OnEntry; + } + } + + public string achievementID; + + public bool player = true; + public bool probe; + + private OWTriggerVolume _trigger; + } +} diff --git a/NewHorizons/External/Configs/AddonConfig.cs b/NewHorizons/External/Configs/AddonConfig.cs new file mode 100644 index 00000000..bf89193f --- /dev/null +++ b/NewHorizons/External/Configs/AddonConfig.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using NewHorizons.AchievementsPlus; +using NewHorizons.External.Modules; +using NewHorizons.External.Modules.VariableSize; +using Newtonsoft.Json; + +namespace NewHorizons.External.Configs +{ + /// + /// Describes the New Horizons addon itself + /// + [JsonObject] + public class AddonConfig + { + /// + /// Achievements for this mod if the user is playing with Achievements+ + /// Achievement icons must be put in a folder named "icons" + /// The icon for the mod must match the name of the mod (e.g., New Horizons.png) + /// The icons for achievements must match their unique IDs (e.g., NH_WARP_DRIVE.png) + /// + public AchievementInfo[] achievements; + + } +} diff --git a/NewHorizons/External/Configs/TranslationConfig.cs b/NewHorizons/External/Configs/TranslationConfig.cs index d6506c7a..854d5bde 100644 --- a/NewHorizons/External/Configs/TranslationConfig.cs +++ b/NewHorizons/External/Configs/TranslationConfig.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace NewHorizons.External.Configs { + [JsonObject] public class TranslationConfig { /// @@ -21,6 +23,30 @@ namespace NewHorizons.External.Configs /// public Dictionary UIDictionary; + + // Literally only exists for the schema generation, Achievements+ handles the parsing + #region Achievements+ + + /// + /// Translation table for achievements. The key is the unique ID of the achievement + /// + public readonly Dictionary AchievementTranslations; + + [JsonObject] + public class AchievementTranslationInfo + { + /// + /// The name of the achievement. + /// + public string Name; + + /// + /// The short description for this achievement. + /// + public readonly string Description; + } + #endregion + public TranslationConfig(string filename) { var dict = JObject.Parse(File.ReadAllText(filename)).ToObject>(); diff --git a/NewHorizons/External/Modules/PropModule.cs b/NewHorizons/External/Modules/PropModule.cs index ce05ccb1..b2894f7e 100644 --- a/NewHorizons/External/Modules/PropModule.cs +++ b/NewHorizons/External/Modules/PropModule.cs @@ -385,6 +385,11 @@ namespace NewHorizons.External.Modules /// A list of facts to reveal /// public string[] reveals; + + /// + /// An achievement to unlock. Optional. + /// + public string achievementID; } [JsonObject] diff --git a/NewHorizons/Icons/NH_MULTIPLE_SYSTEM.png b/NewHorizons/Icons/NH_MULTIPLE_SYSTEM.png new file mode 100644 index 00000000..adcd73ca Binary files /dev/null and b/NewHorizons/Icons/NH_MULTIPLE_SYSTEM.png differ diff --git a/NewHorizons/Icons/NH_WARP_DRIVE.png b/NewHorizons/Icons/NH_WARP_DRIVE.png new file mode 100644 index 00000000..263cf0f6 Binary files /dev/null and b/NewHorizons/Icons/NH_WARP_DRIVE.png differ diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index c4d6e583..723c217d 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -19,6 +19,7 @@ using Logger = NewHorizons.Utility.Logger; using NewHorizons.Utility.DebugUtilities; using Newtonsoft.Json; using NewHorizons.Utility.DebugMenu; +using NewHorizons.AchievementsPlus; namespace NewHorizons { @@ -152,7 +153,7 @@ namespace NewHorizons Instance.ModHelper.Events.Unity.FireOnNextUpdate(() => _firstLoad = false); Instance.ModHelper.Menus.PauseMenu.OnInit += DebugReload.InitializePauseMenu; - AchievementsPlus.AchievementHandler.Init(); + AchievementHandler.Init(); } public void OnDestroy() @@ -160,7 +161,9 @@ namespace NewHorizons Logger.Log($"Destroying NewHorizons"); SceneManager.sceneLoaded -= OnSceneLoaded; GlobalMessenger.RemoveListener("PlayerDeath", OnDeath); - GlobalMessenger.RemoveListener("WakeUp", new Callback(OnWakeUp)); + GlobalMessenger.RemoveListener("WakeUp", new Callback(OnWakeUp)); + + AchievementHandler.OnDestroy(); } private static void OnWakeUp() @@ -360,11 +363,19 @@ namespace NewHorizons BodyDict[body.Config.starSystem].Add(body); } } + } + // Has to go before translations for achievements + if (File.Exists(folder + "addon-manifest.json")) + { + var addonConfig = mod.ModHelper.Storage.Load("addon-manifest.json"); + + AchievementHandler.RegisterAddon(addonConfig, mod as ModBehaviour); } if (Directory.Exists(folder + @"translations\")) { LoadTranslations(folder, mod); - } + } + } catch (Exception ex) { @@ -389,7 +400,12 @@ namespace NewHorizons foundFile = true; - TranslationHandler.RegisterTranslation(language, config); + TranslationHandler.RegisterTranslation(language, config); + + if (AchievementHandler.Enabled) + { + AchievementHandler.RegisterTranslationsFromFiles(mod as ModBehaviour, "translations"); + } } } if (!foundFile) Logger.LogWarning($"{mod.ModHelper.Manifest.Name} has a folder for translations but none were loaded"); diff --git a/NewHorizons/Patches/PlayerDataPatches.cs b/NewHorizons/Patches/PlayerDataPatches.cs index 176b9547..b2c7f423 100644 --- a/NewHorizons/Patches/PlayerDataPatches.cs +++ b/NewHorizons/Patches/PlayerDataPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using NewHorizons.AchievementsPlus; using NewHorizons.AchievementsPlus.NH; using NewHorizons.Builder.Props; using NewHorizons.External; @@ -58,7 +59,13 @@ namespace NewHorizons.Patches var customSignalName = SignalBuilder.GetCustomSignalName(__0); if (customSignalName != null) { - if (!NewHorizonsData.KnowsSignal(customSignalName)) NewHorizonsData.LearnSignal(customSignalName); + if (!NewHorizonsData.KnowsSignal(customSignalName)) + { + NewHorizonsData.LearnSignal(customSignalName); + } + + AchievementHandler.OnLearnSignal(); + return false; } return true; diff --git a/NewHorizons/Patches/ShipLogPatches.cs b/NewHorizons/Patches/ShipLogPatches.cs index 6774b408..00241e33 100644 --- a/NewHorizons/Patches/ShipLogPatches.cs +++ b/NewHorizons/Patches/ShipLogPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using NewHorizons.AchievementsPlus; using NewHorizons.Builder.ShipLog; using NewHorizons.Components; using NewHorizons.Handlers; @@ -217,6 +218,8 @@ namespace NewHorizons.Patches public static void ShipLogManager_RevealFact(string __0) { StarChartHandler.OnRevealFact(__0); + + AchievementHandler.OnRevealFact(); } } } \ No newline at end of file diff --git a/SchemaExporter/SchemaExporter.cs b/SchemaExporter/SchemaExporter.cs index f58241a6..87a701bd 100644 --- a/SchemaExporter/SchemaExporter.cs +++ b/SchemaExporter/SchemaExporter.cs @@ -27,6 +27,9 @@ public static class SchemaExporter var systemSchema = new Schema("Star System Schema", "Schema for a star system in New Horizons", $"{folderName}/star_system_schema", settings); systemSchema.Output(); + var addonSchema = new Schema("Addon Manifest Schema", + "Schema for an addon manifest in New Horizons", $"{folderName}/addon_manifest_schema", settings); + addonSchema.Output(); var translationSchema = new Schema("Translation Schema", "Schema for a translation file in New Horizons", $"{folderName}/translation_schema", settings); translationSchema.Output();