diff --git a/NewHorizons/Builder/Props/DetailBuilder.cs b/NewHorizons/Builder/Props/DetailBuilder.cs index a395e080..022acd74 100644 --- a/NewHorizons/Builder/Props/DetailBuilder.cs +++ b/NewHorizons/Builder/Props/DetailBuilder.cs @@ -209,24 +209,6 @@ namespace NewHorizons.Builder.Props } } - if (detail.item != null) - { - ItemBuilder.MakeItem(prop, go, sector, detail.item, mod); - isItem = true; - if (detail.hasPhysics) - { - NHLogger.LogWarning($"An item with the path {detail.path} has both '{nameof(DetailInfo.hasPhysics)}' and '{nameof(DetailInfo.item)}' set. This will usually result in undesirable behavior."); - } - } - - if (detail.itemSocket != null) - { - ItemBuilder.MakeSocket(prop, go, sector, detail.itemSocket); - } - - // Items should always be kept loaded else they will vanish in your hand as you leave the sector - if (isItem) detail.keepLoaded = true; - prop.transform.localScale = detail.stretch ?? (detail.scale != 0 ? Vector3.one * detail.scale : prefab.transform.localScale); if (detail.removeChildren != null) @@ -271,11 +253,29 @@ namespace NewHorizons.Builder.Props prop = newDetailGO; } + if (detail.item != null) + { + ItemBuilder.MakeItem(prop, go, sector, detail.item, mod); + isItem = true; + if (detail.hasPhysics) + { + NHLogger.LogWarning($"An item with the path {detail.path} has both '{nameof(DetailInfo.hasPhysics)}' and '{nameof(DetailInfo.item)}' set. This will usually result in undesirable behavior."); + } + } + + if (detail.itemSocket != null) + { + ItemBuilder.MakeSocket(prop, go, sector, detail.itemSocket); + } + if (isItem) { // Else when you put them down you can't pick them back up var col = prop.GetComponent(); if (col != null) col._physicsRemoved = false; + + // Items should always be kept loaded else they will vanish in your hand as you leave the sector + detail.keepLoaded = true; } if (!detail.keepLoaded) GroupsBuilder.Make(prop, sector); diff --git a/NewHorizons/Builder/Props/ItemBuilder.cs b/NewHorizons/Builder/Props/ItemBuilder.cs index 5d4dc7ba..f4992e8a 100644 --- a/NewHorizons/Builder/Props/ItemBuilder.cs +++ b/NewHorizons/Builder/Props/ItemBuilder.cs @@ -59,10 +59,18 @@ namespace NewHorizons.Builder.Props { item.PickupAudio = AudioTypeHandler.GetAudioType(info.pickupAudio, mod); } + else + { + item.PickupAudio = AudioType.ToolItemWarpCorePickUp; + } if (!string.IsNullOrEmpty(info.dropAudio)) { item.DropAudio = AudioTypeHandler.GetAudioType(info.dropAudio, mod); } + else + { + item.DropAudio = AudioType.ToolItemWarpCoreDrop; + } if (!string.IsNullOrEmpty(info.socketAudio)) { item.SocketAudio = AudioTypeHandler.GetAudioType(info.socketAudio, mod); diff --git a/NewHorizons/Components/Conditionals/ConditionalsManager.cs b/NewHorizons/Components/Conditionals/ConditionalsManager.cs new file mode 100644 index 00000000..0d0e74f8 --- /dev/null +++ b/NewHorizons/Components/Conditionals/ConditionalsManager.cs @@ -0,0 +1,99 @@ +using NewHorizons.External.Modules.Conditionals; +using NewHorizons.Handlers; +using NewHorizons.Utility.OWML; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Components.Conditionals +{ + public class ConditionalsManager : MonoBehaviour + { + public const int MAX_RECURSION = 120; + + List _checks = new(); + bool _checksScheduled; + int _recursionCount = 0; + bool _avoidRecursionLogSpam = false; + + public void AddCheck(ConditionalCheckInfo check) + { + _checks.Add(check); + } + + public void RemoveCheck(ConditionalCheckInfo check) + { + _checks.Remove(check); + } + + protected void Awake() + { + GlobalMessenger.AddListener("DialogueConditionsReset", ScheduleChecks); + GlobalMessenger.AddListener("DialogueConditionChanged", ScheduleChecks); + GlobalMessenger.AddListener("NHPersistentConditionChanged", ScheduleChecks); + GlobalMessenger.AddListener("ShipLogUpdated", ScheduleChecks); + } + + protected void OnDestroy() + { + GlobalMessenger.RemoveListener("DialogueConditionsReset", ScheduleChecks); + GlobalMessenger.RemoveListener("DialogueConditionChanged", ScheduleChecks); + GlobalMessenger.RemoveListener("NHPersistentConditionChanged", ScheduleChecks); + GlobalMessenger.RemoveListener("ShipLogUpdated", ScheduleChecks); + } + + protected void LateUpdate() + { + if (_checksScheduled) + { + _checksScheduled = false; + DoChecks(); + } + else + { + // We had a frame without any checks being scheduled, so reset the recursion count and disable Update() again + _recursionCount = 0; + enabled = false; + } + } + + void DoChecks() + { + if (_recursionCount >= MAX_RECURSION) + { + if (!_avoidRecursionLogSpam) + { + NHLogger.LogError($"Possible infinite loop detected while processing conditional checks; conditions were changed every single frame for {MAX_RECURSION} frames. This is likely caused by a mod using conflicting conditional checks that both set and unset the same condition."); + _avoidRecursionLogSpam = true; + } + return; + } + + foreach (var check in _checks) + { + bool checkPassed = ConditionalsHandler.Check(check.check); + ConditionalsHandler.ApplyEffects(check.then, checkPassed); + } + _recursionCount++; + // Allow Update() to run + enabled = true; + } + + // We schedule checks for the end of the frame instead of doing them immediately because GlobalMessenger doesn't support recursion and will throw an error if we update a condition that fires off another GlobalMessenger event + void ScheduleChecks() + { + _checksScheduled = true; + // Allow Update() to run + enabled = true; + } + + // We could theoretically filter checks by conditionName here and only do the checks that matter, but the performance gain is likely not significant enough to be worth the extra complexity + void ScheduleChecks(string conditionName, bool conditionState) + { + ScheduleChecks(); + } + } +} diff --git a/NewHorizons/Components/Props/NHItem.cs b/NewHorizons/Components/Props/NHItem.cs index cdc783b5..90105887 100644 --- a/NewHorizons/Components/Props/NHItem.cs +++ b/NewHorizons/Components/Props/NHItem.cs @@ -20,6 +20,8 @@ namespace NewHorizons.Components.Props public bool ClearPickupConditionOnDrop; public string PickupFact; + string _translatedName; + public ItemType ItemType { get => _type; @@ -28,7 +30,11 @@ namespace NewHorizons.Components.Props public override string GetDisplayName() { - return TranslationHandler.GetTranslation(DisplayName, TranslationHandler.TextType.UI); + if (_translatedName == null) + { + _translatedName = TranslationHandler.GetTranslation(DisplayName, TranslationHandler.TextType.UI); + } + return _translatedName; } public override bool CheckIsDroppable() @@ -90,6 +96,7 @@ namespace NewHorizons.Components.Props void PlayCustomSound(AudioType audioType) { + if (audioType == AudioType.None) return; if (ItemBuilder.IsCustomItemType(ItemType)) { Locator.GetPlayerAudioController()._oneShotExternalSource.PlayOneShot(audioType); diff --git a/NewHorizons/External/Configs/StarSystemConfig.cs b/NewHorizons/External/Configs/StarSystemConfig.cs index 08d0d968..257d98e3 100644 --- a/NewHorizons/External/Configs/StarSystemConfig.cs +++ b/NewHorizons/External/Configs/StarSystemConfig.cs @@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Xml; using NewHorizons.External.Modules; +using NewHorizons.External.Modules.Conditionals; using NewHorizons.External.SerializableData; using Newtonsoft.Json; using static NewHorizons.External.Modules.ShipLogModule; @@ -155,6 +156,11 @@ namespace NewHorizons.External.Configs /// public CuriosityColorInfo[] curiosities; + /// + /// A list of conditional checks to be performed while in this star system. + /// + public ConditionalCheckInfo[] conditionalChecks; + /// /// Extra data that may be used by extension mods /// @@ -383,6 +389,15 @@ namespace NewHorizons.External.Configs GlobalMusic ??= otherConfig.GlobalMusic; } + if (conditionalChecks != null && otherConfig.conditionalChecks != null) + { + conditionalChecks = Concatenate(conditionalChecks, otherConfig.conditionalChecks); + } + else + { + conditionalChecks ??= otherConfig.conditionalChecks; + } + entryPositions = Concatenate(entryPositions, otherConfig.entryPositions); curiosities = Concatenate(curiosities, otherConfig.curiosities); initialReveal = Concatenate(initialReveal, otherConfig.initialReveal); diff --git a/NewHorizons/External/Modules/Conditionals/ConditionalCheckInfo.cs b/NewHorizons/External/Modules/Conditionals/ConditionalCheckInfo.cs new file mode 100644 index 00000000..c80a0728 --- /dev/null +++ b/NewHorizons/External/Modules/Conditionals/ConditionalCheckInfo.cs @@ -0,0 +1,86 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Conditionals +{ + [JsonObject] + public class ConditionalCheckInfo + { + /// + /// The conditions that must be met for the check to pass + /// + public ConditionalCheckConditionsInfo check; + /// + /// The effects of the check if it passes + /// + public ConditionalCheckEffectsInfo then; + } + + [JsonObject] + public class ConditionalCheckConditionsInfo + { + /// + /// The check will only pass if all of these dialogue conditions are set + /// + public string[] allConditionsSet; + /// + /// The check will only pass if any of these dialogue conditions are set + /// + public string[] anyConditionsSet; + /// + /// The check will only pass if all of these persistent conditions are set + /// + public string[] allPersistentConditionsSet; + /// + /// The check will only pass if any of these persistent conditions are set + /// + public string[] anyPersistentConditionsSet; + /// + /// The check will only pass if all of these ship log facts are revealed + /// + public string[] allFactsRevealed; + /// + /// The check will only pass if any of these ship log facts are revealed + /// + public string[] anyFactsRevealed; + + /// + /// If the check should pass only if the conditions are not met + /// + public bool invert; + } + + [JsonObject] + public class ConditionalCheckEffectsInfo + { + /// + /// The check will set these dialogue conditions if it passes + /// + public string[] setConditions; + /// + /// The check will unset these dialogue conditions if it passes + /// + public string[] unsetConditions; + /// + /// The check will set these persistent conditions if it passes + /// + public string[] setPersistentConditions; + /// + /// The check will unset these persistent conditions if it passes + /// + public string[] unsetPersistentConditions; + /// + /// The check will reveal these ship log facts if it passes + /// + public string[] revealFacts; + + /// + /// If the check should undo its effects if the conditions are not met anymore (unset the set conditions, etc.). Note: ship log facts cannot currently be unrevealed. + /// + public bool reversible; + } +} diff --git a/NewHorizons/External/Modules/Props/Item/ItemInfo.cs b/NewHorizons/External/Modules/Props/Item/ItemInfo.cs index 23253119..f837443a 100644 --- a/NewHorizons/External/Modules/Props/Item/ItemInfo.cs +++ b/NewHorizons/External/Modules/Props/Item/ItemInfo.cs @@ -62,21 +62,25 @@ namespace NewHorizons.External.Modules.Props.Item /// /// The audio to play when this item is picked up. Only applies to custom/non-vanilla item types. /// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// Defaults to "ToolItemWarpCorePickUp". Set to "None" to disable the sound entirely. /// public string pickupAudio; /// /// The audio to play when this item is dropped. Only applies to custom/non-vanilla item types. /// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// Defaults to "ToolItemWarpCoreDrop". Set to "None" to disable the sound entirely. /// public string dropAudio; /// /// The audio to play when this item is inserted into a socket. Only applies to custom/non-vanilla item types. /// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// Defaults to the pickup audio. Set to "None" to disable the sound entirely. /// public string socketAudio; /// /// The audio to play when this item is removed from a socket. Only applies to custom/non-vanilla item types. /// Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// Defaults to the drop audio. Set to "None" to disable the sound entirely. /// public string unsocketAudio; /// diff --git a/NewHorizons/Handlers/ConditionalsHandler.cs b/NewHorizons/Handlers/ConditionalsHandler.cs new file mode 100644 index 00000000..90392fa8 --- /dev/null +++ b/NewHorizons/Handlers/ConditionalsHandler.cs @@ -0,0 +1,103 @@ +using NewHorizons.External.Modules.Conditionals; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.Handlers +{ + public static class ConditionalsHandler + { + public static bool Check(ConditionalCheckConditionsInfo check) + { + var dcm = DialogueConditionManager.SharedInstance; + + var passed = true; + if (check.allConditionsSet != null && check.allConditionsSet.Length > 0) + { + passed = passed && check.allConditionsSet.All(dcm.GetConditionState); + } + if (check.anyConditionsSet != null && check.anyConditionsSet.Length > 0) + { + passed = passed && check.anyConditionsSet.Any(dcm.GetConditionState); + } + if (check.allPersistentConditionsSet != null && check.allPersistentConditionsSet.Length > 0) + { + passed = passed && check.allPersistentConditionsSet.All(PlayerData.GetPersistentCondition); + } + if (check.anyPersistentConditionsSet != null && check.anyPersistentConditionsSet.Length > 0) + { + passed = passed && check.anyPersistentConditionsSet.Any(PlayerData.GetPersistentCondition); + } + if (check.allFactsRevealed != null && check.allFactsRevealed.Length > 0) + { + passed = passed && check.allFactsRevealed.All(ShipLogHandler.KnowsFact); + } + if (check.anyFactsRevealed != null && check.anyFactsRevealed.Length > 0) + { + passed = passed && check.anyFactsRevealed.Any(ShipLogHandler.KnowsFact); + } + if (check.invert) + { + passed = !passed; + } + + return passed; + } + + public static void ApplyEffects(ConditionalCheckEffectsInfo effects, bool checkPassed) + { + if ((checkPassed || effects.reversible) && effects.setConditions != null) + { + foreach (var condition in effects.setConditions) + { + if (DialogueConditionManager.SharedInstance.GetConditionState(condition) != checkPassed) + { + DialogueConditionManager.SharedInstance.SetConditionState(condition, checkPassed); + } + } + } + else if ((!checkPassed || effects.reversible) && effects.unsetConditions != null) + { + foreach (var condition in effects.unsetConditions) + { + if (DialogueConditionManager.SharedInstance.GetConditionState(condition) != !checkPassed) + { + DialogueConditionManager.SharedInstance.SetConditionState(condition, !checkPassed); + } + } + } + if ((checkPassed || effects.reversible) && effects.setPersistentConditions != null) + { + foreach (var condition in effects.setPersistentConditions) + { + if (!PlayerData.GetPersistentCondition(condition) != checkPassed) + { + PlayerData.SetPersistentCondition(condition, checkPassed); + } + } + } + else if ((!checkPassed || effects.reversible) && effects.unsetPersistentConditions != null) + { + foreach (var condition in effects.unsetPersistentConditions) + { + if (PlayerData.GetPersistentCondition(condition) != !checkPassed) + { + PlayerData.SetPersistentCondition(condition, !checkPassed); + } + } + } + if (checkPassed && effects.revealFacts != null) + { + foreach (var fact in effects.revealFacts) + { + if (!ShipLogHandler.KnowsFact(fact)) + { + Locator.GetShipLogManager().RevealFact(fact); + } + } + } + } + } +} diff --git a/NewHorizons/Handlers/SystemCreationHandler.cs b/NewHorizons/Handlers/SystemCreationHandler.cs index db2951a3..8d8a4fdd 100644 --- a/NewHorizons/Handlers/SystemCreationHandler.cs +++ b/NewHorizons/Handlers/SystemCreationHandler.cs @@ -9,7 +9,7 @@ using UnityEngine; using Object = UnityEngine.Object; using NewHorizons.OtherMods; using NewHorizons.Components.EOTE; -using Epic.OnlineServices.Presence; +using NewHorizons.Components.Conditionals; namespace NewHorizons.Handlers { @@ -32,6 +32,15 @@ namespace NewHorizons.Handlers SkyboxBuilder.Make(system.Config.Skybox, system.Mod); } + if (system.Config.conditionalChecks != null) + { + var conditionalsManager = new GameObject("ConditionalsManager").AddComponent(); + foreach (var check in system.Config.conditionalChecks) + { + conditionalsManager.AddCheck(check); + } + } + // No time loop or travel audio at the eye if (Main.Instance.CurrentStarSystem == "EyeOfTheUniverse") return; diff --git a/NewHorizons/Patches/PlayerPatches/PlayerDataPatches.cs b/NewHorizons/Patches/PlayerPatches/PlayerDataPatches.cs index 6785a8a2..bcc352a9 100644 --- a/NewHorizons/Patches/PlayerPatches/PlayerDataPatches.cs +++ b/NewHorizons/Patches/PlayerPatches/PlayerDataPatches.cs @@ -143,5 +143,13 @@ namespace NewHorizons.Patches.PlayerPatches { NewHorizonsData.Save(); } + + [HarmonyPostfix] + [HarmonyPatch(nameof(PlayerData.SetPersistentCondition))] + public static void PlayerData_SetPersistentCondition(string condition, bool state) + { + // Firing off a custom event for the Conditionals system to use. This could've been done with direct calls or a Unity event but it felt cleaner to mirror the vanilla "DialogueConditionChanged" event. + GlobalMessenger.FireEvent("NHPersistentConditionChanged", condition, state); + } } } diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index f5ed593d..82ee3dda 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -1152,19 +1152,19 @@ }, "pickupAudio": { "type": "string", - "description": "The audio to play when this item is picked up. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + "description": "The audio to play when this item is picked up. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nDefaults to \"ToolItemWarpCorePickUp\". Set to \"None\" to disable the sound entirely." }, "dropAudio": { "type": "string", - "description": "The audio to play when this item is dropped. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + "description": "The audio to play when this item is dropped. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nDefaults to \"ToolItemWarpCoreDrop\". Set to \"None\" to disable the sound entirely." }, "socketAudio": { "type": "string", - "description": "The audio to play when this item is inserted into a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + "description": "The audio to play when this item is inserted into a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nDefaults to the pickup audio. Set to \"None\" to disable the sound entirely." }, "unsocketAudio": { "type": "string", - "description": "The audio to play when this item is removed from a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + "description": "The audio to play when this item is removed from a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nDefaults to the drop audio. Set to \"None\" to disable the sound entirely." }, "pickupCondition": { "type": "string", diff --git a/NewHorizons/Schemas/star_system_schema.json b/NewHorizons/Schemas/star_system_schema.json index d1466958..048bb732 100644 --- a/NewHorizons/Schemas/star_system_schema.json +++ b/NewHorizons/Schemas/star_system_schema.json @@ -110,6 +110,13 @@ "$ref": "#/definitions/CuriosityColorInfo" } }, + "conditionalChecks": { + "type": "array", + "description": "A list of conditional checks to be performed while in this star system.", + "items": { + "$ref": "#/definitions/ConditionalCheckInfo" + } + }, "extras": { "type": "object", "description": "Extra data that may be used by extension mods", @@ -451,6 +458,117 @@ "minimum": 0.0 } } + }, + "ConditionalCheckInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "check": { + "description": "The conditions that must be met for the check to pass", + "$ref": "#/definitions/ConditionalCheckConditionsInfo" + }, + "then": { + "description": "The effects of the check if it passes", + "$ref": "#/definitions/ConditionalCheckEffectsInfo" + } + } + }, + "ConditionalCheckConditionsInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "allConditionsSet": { + "type": "array", + "description": "The check will only pass if all of these dialogue conditions are set", + "items": { + "type": "string" + } + }, + "anyConditionsSet": { + "type": "array", + "description": "The check will only pass if any of these dialogue conditions are set", + "items": { + "type": "string" + } + }, + "allPersistentConditionsSet": { + "type": "array", + "description": "The check will only pass if all of these persistent conditions are set", + "items": { + "type": "string" + } + }, + "anyPersistentConditionsSet": { + "type": "array", + "description": "The check will only pass if any of these persistent conditions are set", + "items": { + "type": "string" + } + }, + "allFactsRevealed": { + "type": "array", + "description": "The check will only pass if all of these ship log facts are revealed", + "items": { + "type": "string" + } + }, + "anyFactsRevealed": { + "type": "array", + "description": "The check will only pass if any of these ship log facts are revealed", + "items": { + "type": "string" + } + }, + "invert": { + "type": "boolean", + "description": "If the check should pass only if the conditions are not met" + } + } + }, + "ConditionalCheckEffectsInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "setConditions": { + "type": "array", + "description": "The check will set these dialogue conditions if it passes", + "items": { + "type": "string" + } + }, + "unsetConditions": { + "type": "array", + "description": "The check will unset these dialogue conditions if it passes", + "items": { + "type": "string" + } + }, + "setPersistentConditions": { + "type": "array", + "description": "The check will set these persistent conditions if it passes", + "items": { + "type": "string" + } + }, + "unsetPersistentConditions": { + "type": "array", + "description": "The check will unset these persistent conditions if it passes", + "items": { + "type": "string" + } + }, + "revealFacts": { + "type": "array", + "description": "The check will reveal these ship log facts if it passes", + "items": { + "type": "string" + } + }, + "reversible": { + "type": "boolean", + "description": "If the check should undo its effects if the conditions are not met anymore (unset the set conditions, etc.). Note: ship log facts cannot currently be unrevealed." + } + } } }, "$docs": {