diff --git a/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs b/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs index f92d1daf7..6672b15ff 100644 --- a/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs +++ b/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs @@ -6,6 +6,7 @@ using NewHorizons.External.Modules.Props; using NewHorizons.Utility; using NewHorizons.Utility.OWML; +using OWML.Common; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -80,7 +81,7 @@ internal static void InitPrefabs() if (_wallCollision == null) _wallCollision = Main.NHPrivateAssetBundle.LoadAsset("BrambleCollision"); } - public static GameObject Make(NewHorizonsBody body, GameObject go, NHAstroObject ao, Sector sector, OWRigidbody owRigidBody) + public static GameObject Make(NewHorizonsBody body, GameObject go, NHAstroObject ao, Sector sector, IModBehaviour mod, OWRigidbody owRigidBody) { InitPrefabs(); @@ -102,7 +103,7 @@ public static GameObject Make(NewHorizonsBody body, GameObject go, NHAstroObject default: geometryPrefab = _hubGeometry; break; } - var geometry = DetailBuilder.Make(go, sector, geometryPrefab, new DetailInfo()); + var geometry = DetailBuilder.Make(go, sector, mod, geometryPrefab, new DetailInfo()); var exitWarps = _exitWarps.InstantiateInactive(); var repelVolume = _repelVolume.InstantiateInactive(); diff --git a/NewHorizons/Builder/Props/DetailBuilder.cs b/NewHorizons/Builder/Props/DetailBuilder.cs index 245ba6c1f..0f510106d 100644 --- a/NewHorizons/Builder/Props/DetailBuilder.cs +++ b/NewHorizons/Builder/Props/DetailBuilder.cs @@ -27,6 +27,14 @@ static DetailBuilder() SceneManager.sceneUnloaded += SceneManager_sceneUnloaded; } + #region obsolete + // Never change method signatures, people directly reference the NH dll and it can break backwards compatability + // In particular, Outer Wives needs this method signature + [Obsolete] + public static GameObject Make(GameObject go, Sector sector, GameObject prefab, DetailInfo detail) + => Make(go, sector, null, prefab, detail); + #endregion + private static void SceneManager_sceneUnloaded(Scene scene) { foreach (var prefab in _fixedPrefabCache.Values) @@ -52,24 +60,16 @@ public static GameObject GetSpawnedGameObjectByDetailInfo(DetailInfo detail) /// /// Create a detail using an asset bundle or a path in the scene hierarchy of the item to copy. /// - public static GameObject Make(GameObject go, Sector sector, IModBehaviour mod, DetailInfo detail) + public static GameObject Make(GameObject planetGO, Sector sector, IModBehaviour mod, DetailInfo info) { - if (detail.assetBundle != null) + if (info.assetBundle != null) { // Shouldn't happen if (mod == null) return null; - return Make(go, sector, AssetBundleUtilities.LoadPrefab(detail.assetBundle, detail.path, mod), detail); + return Make(planetGO, sector, mod, AssetBundleUtilities.LoadPrefab(info.assetBundle, info.path, mod), info); } - else - return Make(go, sector, detail); - } - /// - /// Create a detail using a path in the scene hierarchy of the item to copy. - /// - public static GameObject Make(GameObject planetGO, Sector sector, DetailInfo info) - { if (_emptyPrefab == null) _emptyPrefab = new GameObject("Empty"); // Allow for empty game objects so you can set up conditional activation on them and parent other props to them @@ -82,14 +82,14 @@ public static GameObject Make(GameObject planetGO, Sector sector, DetailInfo inf } else { - return Make(planetGO, sector, prefab, info); + return Make(planetGO, sector, mod, prefab, info); } } /// /// Create a detail using a prefab. /// - public static GameObject Make(GameObject go, Sector sector, GameObject prefab, DetailInfo detail) + public static GameObject Make(GameObject go, Sector sector, IModBehaviour mod, GameObject prefab, DetailInfo detail) { if (prefab == null) return null; @@ -163,6 +163,16 @@ public static GameObject Make(GameObject go, Sector sector, GameObject prefab, D } } + if (detail.item != null) + { + ItemBuilder.MakeItem(prop, go, sector, detail.item, mod); + } + + 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; diff --git a/NewHorizons/Builder/Props/GravityCannonBuilder.cs b/NewHorizons/Builder/Props/GravityCannonBuilder.cs index 44b1f0758..16978d4c4 100644 --- a/NewHorizons/Builder/Props/GravityCannonBuilder.cs +++ b/NewHorizons/Builder/Props/GravityCannonBuilder.cs @@ -1,5 +1,4 @@ using NewHorizons.Builder.Props.TranslatorText; -using NewHorizons.Components.Orbital; using NewHorizons.External.Modules; using NewHorizons.External.Modules.Props; using NewHorizons.External.Modules.Props.Shuttle; @@ -18,7 +17,6 @@ public static class GravityCannonBuilder { private static GameObject _interfacePrefab; private static GameObject _detailedPlatformPrefab, _platformPrefab; - private static GameObject _orbPrefab; internal static void InitPrefab() { @@ -59,24 +57,16 @@ internal static void InitPrefab() GameObject.DestroyImmediate(_platformPrefab.FindChild("Structure_NOM_GravityCannon_Crystals")); GameObject.DestroyImmediate(_platformPrefab.FindChild("Structure_NOM_GravityCannon_Geo")); } - - if (_orbPrefab == null) - { - _orbPrefab = SearchUtilities.Find("Prefab_NOM_InterfaceOrb") - .InstantiateInactive() - .Rename("Prefab_NOM_InterfaceOrb") - .DontDestroyOnLoad(); - } } public static GameObject Make(GameObject planetGO, Sector sector, GravityCannonInfo info, IModBehaviour mod) { InitPrefab(); - if (_interfacePrefab == null || planetGO == null || sector == null || _detailedPlatformPrefab == null || _platformPrefab == null || _orbPrefab == null) return null; + if (_interfacePrefab == null || planetGO == null || sector == null || _detailedPlatformPrefab == null || _platformPrefab == null) return null; var detailInfo = new DetailInfo(info.controls) { keepLoaded = true }; - var gravityCannonObject = DetailBuilder.Make(planetGO, sector, _interfacePrefab, detailInfo); + var gravityCannonObject = DetailBuilder.Make(planetGO, sector, mod, _interfacePrefab, detailInfo); gravityCannonObject.SetActive(false); var gravityCannonController = gravityCannonObject.GetComponent(); @@ -87,7 +77,7 @@ public static GameObject Make(GameObject planetGO, Sector sector, GravityCannonI gravityCannonController._retrieveShipLogFact = info.retrieveReveal ?? string.Empty; gravityCannonController._launchShipLogFact = info.launchReveal ?? string.Empty; - CreatePlatform(planetGO, sector, gravityCannonController, info); + CreatePlatform(planetGO, sector, mod, gravityCannonController, info); if (info.computer != null) { @@ -102,56 +92,11 @@ public static GameObject Make(GameObject planetGO, Sector sector, GravityCannonI gravityCannonController._nomaiComputer = null; } - CreateOrb(planetGO, gravityCannonController); - gravityCannonObject.SetActive(true); return gravityCannonObject; } - private static void CreateOrb(GameObject planetGO, GravityCannonController gravityCannonController) - { - var orb = _orbPrefab.InstantiateInactive().Rename(_orbPrefab.name); - orb.transform.parent = gravityCannonController.transform; - orb.transform.localPosition = new Vector3(0f, 0.9673f, 0f); - orb.transform.localScale = Vector3.one; - orb.SetActive(true); - - var planetBody = planetGO.GetComponent(); - var orbBody = orb.GetComponent(); - - var nomaiInterfaceOrb = orb.GetComponent(); - nomaiInterfaceOrb._orbBody = orbBody; - nomaiInterfaceOrb._slotRoot = gravityCannonController.gameObject; - orbBody._origParent = planetGO.transform; - orbBody._origParentBody = planetBody; - nomaiInterfaceOrb._slots = nomaiInterfaceOrb._slotRoot.GetComponentsInChildren(); - nomaiInterfaceOrb.SetParentBody(planetBody); - nomaiInterfaceOrb._safetyRails = new OWRail[0]; - nomaiInterfaceOrb.RemoveAllLocks(); - - var spawnVelocity = planetBody.GetVelocity(); - var spawnAngularVelocity = planetBody.GetPointTangentialVelocity(orbBody.transform.position); - var velocity = spawnVelocity + spawnAngularVelocity; - - orbBody._lastVelocity = velocity; - orbBody._currentVelocity = velocity; - - // detect planet gravity - // somehow Intervention has GetAttachedOWRigidbody as null sometimes, idk why - var gravityVolume = planetGO.GetAttachedOWRigidbody()?.GetAttachedGravityVolume(); - orb.GetComponent()._detectableFields = gravityVolume ? new ForceVolume[] { gravityVolume } : new ForceVolume[0]; - - Delay.RunWhenAndInNUpdates(() => - { - foreach (var component in orb.GetComponents()) - { - component.enabled = true; - } - nomaiInterfaceOrb.RemoveAllLocks(); - }, () => Main.IsSystemReady, 10); - } - private static NomaiComputer CreateComputer(GameObject planetGO, Sector sector, GeneralPropInfo computerInfo, NomaiShuttleController.ShuttleID id) { // Load the position info from the GeneralPropInfo @@ -175,9 +120,9 @@ private static NomaiComputer CreateComputer(GameObject planetGO, Sector sector, return computer; } - private static GameObject CreatePlatform(GameObject planetGO, Sector sector, GravityCannonController gravityCannonController, GravityCannonInfo platformInfo) + private static GameObject CreatePlatform(GameObject planetGO, Sector sector, IModBehaviour mod, GravityCannonController gravityCannonController, GravityCannonInfo platformInfo) { - var platform = DetailBuilder.Make(planetGO, sector, platformInfo.detailed ? _detailedPlatformPrefab : _platformPrefab, new DetailInfo(platformInfo) { keepLoaded = true }); + var platform = DetailBuilder.Make(planetGO, sector, mod, platformInfo.detailed ? _detailedPlatformPrefab : _platformPrefab, new DetailInfo(platformInfo) { keepLoaded = true }); gravityCannonController._forceVolume = platform.FindChild("ForceVolume").GetComponent(); gravityCannonController._platformTrigger = platform.FindChild("PlatformTrigger").GetComponent(); diff --git a/NewHorizons/Builder/Props/ItemBuilder.cs b/NewHorizons/Builder/Props/ItemBuilder.cs new file mode 100644 index 000000000..825eafceb --- /dev/null +++ b/NewHorizons/Builder/Props/ItemBuilder.cs @@ -0,0 +1,184 @@ +using NewHorizons.Components.Props; +using NewHorizons.External.Modules.Props.Item; +using NewHorizons.Handlers; +using NewHorizons.Utility.OWML; +using OWML.Common; +using OWML.Utils; +using System.Collections.Generic; +using UnityEngine; + +namespace NewHorizons.Builder.Props +{ + public static class ItemBuilder + { + private static Dictionary _itemTypes; + + internal static void Init() + { + if (_itemTypes != null) + { + foreach (var value in _itemTypes.Values) + { + EnumUtils.Remove(value); + } + } + _itemTypes = new Dictionary(); + } + + public static NHItem MakeItem(GameObject go, GameObject planetGO, Sector sector, ItemInfo info, IModBehaviour mod) + { + var itemName = info.name; + if (string.IsNullOrEmpty(itemName)) + { + itemName = go.name; + } + + var itemTypeName = info.itemType; + if (string.IsNullOrEmpty(itemTypeName)) + { + itemTypeName = itemName; + } + + var itemType = GetOrCreateItemType(itemTypeName); + + var item = go.GetAddComponent(); + item._sector = sector; + item._interactable = info.interactRange > 0f; + item._interactRange = info.interactRange; + item._localDropOffset = info.dropOffset ?? Vector3.zero; + item._localDropNormal = info.dropNormal ?? Vector3.up; + + item.DisplayName = itemName; + item.ItemType = itemType; + item.Droppable = info.droppable; + if (!string.IsNullOrEmpty(info.pickupAudio)) + { + item.PickupAudio = AudioTypeHandler.GetAudioType(info.pickupAudio, mod); + } + if (!string.IsNullOrEmpty(info.dropAudio)) + { + item.DropAudio = AudioTypeHandler.GetAudioType(info.dropAudio, mod); + } + if (!string.IsNullOrEmpty(info.socketAudio)) + { + item.SocketAudio = AudioTypeHandler.GetAudioType(info.socketAudio, mod); + } + else + { + item.SocketAudio = item.DropAudio; + } + if (!string.IsNullOrEmpty(info.unsocketAudio)) + { + item.UnsocketAudio = AudioTypeHandler.GetAudioType(info.unsocketAudio, mod); + } + else + { + item.UnsocketAudio = item.PickupAudio; + } + item.PickupCondition = info.pickupCondition; + item.ClearPickupConditionOnDrop = info.clearPickupConditionOnDrop; + item.PickupFact = info.pickupFact; + + Delay.FireOnNextUpdate(() => + { + if (item != null && !string.IsNullOrEmpty(info.pathToInitialSocket)) + { + var socketGO = planetGO.transform.Find(info.pathToInitialSocket); + if (socketGO != null) + { + var socket = socketGO.GetComponent(); + if (socket != null) + { + if (socket.PlaceIntoSocket(item)) + { + // Successfully socketed + } + else + { + NHLogger.LogError($"Could not insert item {itemName} into socket at path {socketGO}"); + } + } + else + { + NHLogger.LogError($"Could not find a socket to parent item {itemName} to at path {socketGO}"); + } + } + else + { + NHLogger.LogError($"Could not find a socket to parent item {itemName} to at path {socketGO}"); + } + } + }); + + return item; + } + + public static NHItemSocket MakeSocket(GameObject go, GameObject planetGO, Sector sector, ItemSocketInfo info) + { + var itemType = EnumUtils.TryParse(info.itemType, true, out ItemType result) ? result : ItemType.Invalid; + if (itemType == ItemType.Invalid && !string.IsNullOrEmpty(info.itemType)) + { + itemType = EnumUtilities.Create(info.itemType); + } + + var socket = go.GetAddComponent(); + socket._sector = sector; + socket._interactable = info.interactRange > 0f; + socket._interactRange = info.interactRange; + + if (!string.IsNullOrEmpty(info.socketPath)) + { + socket._socketTransform = go.transform.Find(info.socketPath); + } + if (socket._socketTransform == null) + { + var socketGO = GeneralPropBuilder.MakeNew("Socket", planetGO, sector, info, defaultParent: go.transform); + socketGO.SetActive(true); + socket._socketTransform = socketGO.transform; + } + + socket.ItemType = itemType; + socket.UseGiveTakePrompts = info.useGiveTakePrompts; + socket.InsertCondition = info.insertCondition; + socket.ClearInsertConditionOnRemoval = info.clearInsertConditionOnRemoval; + socket.InsertFact = info.insertFact; + socket.RemovalCondition = info.removalCondition; + socket.ClearRemovalConditionOnInsert = info.clearRemovalConditionOnInsert; + socket.RemovalFact = info.removalFact; + + Delay.FireInNUpdates(() => + { + if (socket != null && !socket._socketedItem) + { + socket.TriggerRemovalConditions(); + } + }, 2); + + return socket; + } + + public static ItemType GetOrCreateItemType(string name) + { + var itemType = ItemType.Invalid; + if (_itemTypes.ContainsKey(name)) + { + itemType = _itemTypes[name]; + } + else if (EnumUtils.TryParse(name, true, out ItemType result)) + { + itemType = result; + } + else if (!string.IsNullOrEmpty(name)) + { + itemType = EnumUtils.Create(name); + _itemTypes.Add(name, itemType); + } + return itemType; + } + + public static bool IsCustomItemType(ItemType type) + { + return _itemTypes.ContainsValue(type); + } + } +} diff --git a/NewHorizons/Builder/Props/NomaiTextBuilder.cs b/NewHorizons/Builder/Props/NomaiTextBuilder.cs index e70353651..ce4ed43d4 100644 --- a/NewHorizons/Builder/Props/NomaiTextBuilder.cs +++ b/NewHorizons/Builder/Props/NomaiTextBuilder.cs @@ -376,7 +376,7 @@ public static GameObject Make(GameObject planetGO, Sector sector, NomaiTextInfo } case NomaiTextType.PreCrashComputer: { - var computerObject = DetailBuilder.Make(planetGO, sector, _preCrashComputerPrefab, new DetailInfo(info)); + var computerObject = DetailBuilder.Make(planetGO, sector, mod, _preCrashComputerPrefab, new DetailInfo(info)); computerObject.SetActive(false); var up = computerObject.transform.position - planetGO.transform.position; @@ -493,7 +493,7 @@ public static GameObject Make(GameObject planetGO, Sector sector, NomaiTextInfo case NomaiTextType.Recorder: { var prefab = (info.type == NomaiTextType.PreCrashRecorder ? _preCrashRecorderPrefab : _recorderPrefab); - var recorderObject = DetailBuilder.Make(planetGO, sector, prefab, new DetailInfo(info)); + var recorderObject = DetailBuilder.Make(planetGO, sector, mod, prefab, new DetailInfo(info)); recorderObject.SetActive(false); if (info.rotation == null) diff --git a/NewHorizons/Builder/Props/ProjectionBuilder.cs b/NewHorizons/Builder/Props/ProjectionBuilder.cs index c73b330e9..7795f81cb 100644 --- a/NewHorizons/Builder/Props/ProjectionBuilder.cs +++ b/NewHorizons/Builder/Props/ProjectionBuilder.cs @@ -110,6 +110,7 @@ private static GameObject MakeSlideReel(GameObject planetGO, Sector sector, Proj // Now we replace the slides int slidesCount = info.slides.Length; var slideCollection = new SlideCollection(slidesCount); + slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null // The base game ones only have 15 slides max var textures = new Texture2D[slidesCount >= 15 ? 15 : slidesCount]; @@ -177,7 +178,8 @@ public static GameObject MakeAutoProjector(GameObject planetGO, Sector sector, P // Now we replace the slides int slidesCount = info.slides.Length; var slideCollection = new SlideCollection(slidesCount); - + slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null + var imageLoader = AddAsyncLoader(projectorObj, mod, info.slides, ref slideCollection); imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index) => { slideCollection.slides[index]._image = ImageUtilities.Invert(tex); }); @@ -203,7 +205,7 @@ public static GameObject MakeMindSlidesTarget(GameObject planetGO, Sector sector if (_visionTorchDetectorPrefab == null) return null; // spawn a trigger for the vision torch - var g = DetailBuilder.Make(planetGO, sector, _visionTorchDetectorPrefab, new DetailInfo(info) { scale = 2, rename = !string.IsNullOrEmpty(info.rename) ? info.rename : "VisionStaffDetector" }); + var g = DetailBuilder.Make(planetGO, sector, mod, _visionTorchDetectorPrefab, new DetailInfo(info) { scale = 2, rename = !string.IsNullOrEmpty(info.rename) ? info.rename : "VisionStaffDetector" }); if (g == null) { @@ -215,6 +217,7 @@ public static GameObject MakeMindSlidesTarget(GameObject planetGO, Sector sector var slides = info.slides; var slidesCount = slides.Length; var slideCollection = new SlideCollection(slidesCount); // TODO: uh I think that info.slides[i].playTimeDuration is not being read here... note to self for when I implement support for that: 0.7 is what to default to if playTimeDuration turns out to be 0 + slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null var imageLoader = AddAsyncLoader(g, mod, info.slides, ref slideCollection); imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index) => { slideCollection.slides[index]._image = tex; }); @@ -240,7 +243,7 @@ public static GameObject MakeStandingVisionTorch(GameObject planetGO, Sector sec if (_standingVisionTorchPrefab == null) return null; // Spawn the torch itself - var standingTorch = DetailBuilder.Make(planetGO, sector, _standingVisionTorchPrefab, new DetailInfo(info)); + var standingTorch = DetailBuilder.Make(planetGO, sector, mod, _standingVisionTorchPrefab, new DetailInfo(info)); if (standingTorch == null) { @@ -262,6 +265,7 @@ public static GameObject MakeStandingVisionTorch(GameObject planetGO, Sector sec var slides = info.slides; var slidesCount = slides.Length; var slideCollection = new SlideCollection(slidesCount); + slideCollection.streamingAssetIdentifier = string.Empty; // NREs if null var imageLoader = AddAsyncLoader(standingTorch, mod, slides, ref slideCollection); @@ -331,7 +335,7 @@ private static ImageUtilities.AsyncImageLoader AddAsyncLoader(GameObject gameObj private static void AddModules(SlideInfo slideInfo, ref Slide slide, IModBehaviour mod) { var modules = new List(); - if (!String.IsNullOrEmpty(slideInfo.beatAudio)) + if (!string.IsNullOrEmpty(slideInfo.beatAudio)) { var audioBeat = new SlideBeatAudioModule { @@ -340,7 +344,7 @@ private static void AddModules(SlideInfo slideInfo, ref Slide slide, IModBehavio }; modules.Add(audioBeat); } - if (!String.IsNullOrEmpty(slideInfo.backdropAudio)) + if (!string.IsNullOrEmpty(slideInfo.backdropAudio)) { var audioBackdrop = new SlideBackdropAudioModule { @@ -349,13 +353,13 @@ private static void AddModules(SlideInfo slideInfo, ref Slide slide, IModBehavio }; modules.Add(audioBackdrop); } - if (slideInfo.ambientLightIntensity > 0) + if (slideInfo.ambientLightIntensity != 0) { var ambientLight = new SlideAmbientLightModule { _intensity = slideInfo.ambientLightIntensity, _range = slideInfo.ambientLightRange, - _color = slideInfo.ambientLightColor.ToColor(), + _color = slideInfo.ambientLightColor?.ToColor() ?? Color.white, _spotIntensityMod = slideInfo.spotIntensityMod }; modules.Add(ambientLight); @@ -376,7 +380,7 @@ private static void AddModules(SlideInfo slideInfo, ref Slide slide, IModBehavio }; modules.Add(blackFrame); } - if (!String.IsNullOrEmpty(slideInfo.reveal)) + if (!string.IsNullOrEmpty(slideInfo.reveal)) { var shipLogEntry = new SlideShipLogEntryModule { diff --git a/NewHorizons/Builder/Props/PropBuildManager.cs b/NewHorizons/Builder/Props/PropBuildManager.cs index af781f529..86dcf8268 100644 --- a/NewHorizons/Builder/Props/PropBuildManager.cs +++ b/NewHorizons/Builder/Props/PropBuildManager.cs @@ -39,7 +39,7 @@ public static void Make(GameObject go, Sector sector, OWRigidbody planetBody, Ne { try { - ShuttleBuilder.Make(go, sector, shuttleInfo); + ShuttleBuilder.Make(go, sector, nhBody.Mod, shuttleInfo); } catch (Exception ex) { @@ -283,7 +283,7 @@ public static void Make(GameObject go, Sector sector, OWRigidbody planetBody, Ne { try { - WarpPadBuilder.Make(go, sector, warpReceiver); + WarpPadBuilder.Make(go, sector, nhBody.Mod, warpReceiver); } catch (Exception ex) { @@ -297,7 +297,7 @@ public static void Make(GameObject go, Sector sector, OWRigidbody planetBody, Ne { try { - WarpPadBuilder.Make(go, sector, warpTransmitter); + WarpPadBuilder.Make(go, sector, nhBody.Mod, warpTransmitter); } catch (Exception ex) { diff --git a/NewHorizons/Builder/Props/RemoteBuilder.cs b/NewHorizons/Builder/Props/RemoteBuilder.cs index e4d6b5478..4ae0ac01b 100644 --- a/NewHorizons/Builder/Props/RemoteBuilder.cs +++ b/NewHorizons/Builder/Props/RemoteBuilder.cs @@ -149,7 +149,7 @@ public static void Make(GameObject go, Sector sector, RemoteInfo info, NewHorizo { try { - MakeWhiteboard(go, sector, id, decal, info.whiteboard, nhBody); + MakeWhiteboard(go, sector, nhBody.Mod, id, decal, info.whiteboard, nhBody); } catch (Exception ex) { @@ -173,9 +173,9 @@ public static void Make(GameObject go, Sector sector, RemoteInfo info, NewHorizo } } - public static void MakeWhiteboard(GameObject go, Sector sector, NomaiRemoteCameraPlatform.ID id, Texture2D decal, RemoteWhiteboardInfo info, NewHorizonsBody nhBody) + public static void MakeWhiteboard(GameObject go, Sector sector, IModBehaviour mod, NomaiRemoteCameraPlatform.ID id, Texture2D decal, RemoteWhiteboardInfo info, NewHorizonsBody nhBody) { - var whiteboard = DetailBuilder.Make(go, sector, _whiteboardPrefab, new DetailInfo(info)); + var whiteboard = DetailBuilder.Make(go, sector, mod, _whiteboardPrefab, new DetailInfo(info)); whiteboard.SetActive(false); var decalMat = new Material(_decalMaterial); @@ -215,7 +215,7 @@ public static void MakeWhiteboard(GameObject go, Sector sector, NomaiRemoteCamer public static void MakePlatform(GameObject go, Sector sector, NomaiRemoteCameraPlatform.ID id, Texture2D decal, PlatformInfo info, IModBehaviour mod) { - var platform = DetailBuilder.Make(go, sector, _remoteCameraPlatformPrefab, new DetailInfo(info)); + var platform = DetailBuilder.Make(go, sector, mod, _remoteCameraPlatformPrefab, new DetailInfo(info)); platform.SetActive(false); var decalMat = new Material(_decalMaterial); diff --git a/NewHorizons/Builder/Props/ScatterBuilder.cs b/NewHorizons/Builder/Props/ScatterBuilder.cs index 1ec4e2d33..e725bf8a3 100644 --- a/NewHorizons/Builder/Props/ScatterBuilder.cs +++ b/NewHorizons/Builder/Props/ScatterBuilder.cs @@ -77,7 +77,7 @@ private static void MakeScatter(GameObject go, ScatterInfo[] scatterInfo, float stretch = propInfo.stretch, keepLoaded = propInfo.keepLoaded }; - var scatterPrefab = DetailBuilder.Make(go, sector, prefab, detailInfo); + var scatterPrefab = DetailBuilder.Make(go, sector, mod, prefab, detailInfo); for (int i = 0; i < propInfo.count; i++) { diff --git a/NewHorizons/Builder/Props/ShuttleBuilder.cs b/NewHorizons/Builder/Props/ShuttleBuilder.cs index f855990d3..469be74de 100644 --- a/NewHorizons/Builder/Props/ShuttleBuilder.cs +++ b/NewHorizons/Builder/Props/ShuttleBuilder.cs @@ -3,6 +3,7 @@ using NewHorizons.External.Modules.Props.Shuttle; using NewHorizons.Handlers; using NewHorizons.Utility; +using OWML.Common; using System.Collections.Generic; using UnityEngine; @@ -11,7 +12,6 @@ namespace NewHorizons.Builder.Props public static class ShuttleBuilder { private static GameObject _prefab; - private static GameObject _orbPrefab; private static GameObject _bodyPrefab; public static Dictionary Shuttles { get; } = new(); @@ -52,9 +52,9 @@ internal static void InitPrefab() neutralSlot._attractive = true; neutralSlot._muteAudio = true; nhShuttleController._neutralSlot = neutralSlot; - // TODO: at some point delay rigidbody parenting so we dont have to find orb via references. mainly to fix orbs on existing details and rafts not rotating with planets - _orbPrefab = shuttleController._orb.gameObject?.InstantiateInactive()?.Rename("Prefab_QM_Shuttle_InterfaceOrbSmall")?.DontDestroyOnLoad(); - nhShuttleController._orb = _orbPrefab.GetComponent(); + + var orb = shuttleController._orb.gameObject; + nhShuttleController._orb = orb.GetComponent(); nhShuttleController._orb._sector = nhShuttleController._interiorSector; nhShuttleController._orb._slotRoot = slots; nhShuttleController._orb._safetyRails = slots.GetComponentsInChildren(); @@ -69,14 +69,14 @@ internal static void InitPrefab() } } - public static GameObject Make(GameObject planetGO, Sector sector, ShuttleInfo info) + public static GameObject Make(GameObject planetGO, Sector sector, IModBehaviour mod, ShuttleInfo info) { InitPrefab(); if (_prefab == null || planetGO == null || sector == null) return null; var detailInfo = new DetailInfo(info) { keepLoaded = true }; - var shuttleObject = DetailBuilder.Make(planetGO, sector, _prefab, detailInfo); + var shuttleObject = DetailBuilder.Make(planetGO, sector, mod, _prefab, detailInfo); shuttleObject.SetActive(false); StreamingHandler.SetUpStreaming(shuttleObject, sector); @@ -87,7 +87,7 @@ public static GameObject Make(GameObject planetGO, Sector sector, ShuttleInfo in shuttleController._cannon = Locator.GetGravityCannon(id); GameObject slots = shuttleObject.FindChild("Sector_NomaiShuttleInterior/Interactibles_NomaiShuttleInterior/ControlPanel/Slots"); - GameObject orbObject = _orbPrefab.InstantiateInactive().Rename("InterfaceOrbSmall"); + GameObject orbObject = shuttleController._orb.gameObject; orbObject.transform.SetParent(slots.transform, false); orbObject.transform.localPosition = new Vector3(-0.0153f, -0.2386f, 0.0205f); shuttleController._orb = orbObject.GetComponent(); diff --git a/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs b/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs index 6ff148ef2..9ad3ba5cf 100644 --- a/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs +++ b/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs @@ -242,7 +242,7 @@ public static GameObject Make(GameObject planetGO, Sector sector, TranslatorText } case NomaiTextType.PreCrashComputer: { - var computerObject = DetailBuilder.Make(planetGO, sector, PreCrashComputerPrefab, new DetailInfo(info)); + var computerObject = DetailBuilder.Make(planetGO, sector, nhBody.Mod, PreCrashComputerPrefab, new DetailInfo(info)); computerObject.SetActive(false); var computer = computerObject.GetComponent(); @@ -323,7 +323,7 @@ public static GameObject Make(GameObject planetGO, Sector sector, TranslatorText case NomaiTextType.Recorder: { var prefab = (info.type == NomaiTextType.PreCrashRecorder ? _preCrashRecorderPrefab : _recorderPrefab); - var recorderObject = DetailBuilder.Make(planetGO, sector, prefab, new DetailInfo(info)); + var recorderObject = DetailBuilder.Make(planetGO, sector, nhBody.Mod, prefab, new DetailInfo(info)); recorderObject.SetActive(false); var nomaiText = recorderObject.GetComponentInChildren(); @@ -373,7 +373,7 @@ public static GameObject Make(GameObject planetGO, Sector sector, TranslatorText path = "BrittleHollow_Body/Sector_BH/Sector_NorthHemisphere/Sector_NorthPole/Sector_HangingCity/Sector_HangingCity_District2/Interactables_HangingCity_District2/VisibleFrom_HangingCity/Props_NOM_Whiteboard (1)", rename = info.rename ?? "Props_NOM_Whiteboard", }; - var whiteboardObject = DetailBuilder.Make(planetGO, sector, whiteboardInfo); + var whiteboardObject = DetailBuilder.Make(planetGO, sector, nhBody.Mod, whiteboardInfo); // Spawn a scroll and insert it into the whiteboard, but only if text is provided if (!string.IsNullOrEmpty(info.xmlFile)) diff --git a/NewHorizons/Builder/Props/WarpPadBuilder.cs b/NewHorizons/Builder/Props/WarpPadBuilder.cs index 4c5a9ae8b..4342a374d 100644 --- a/NewHorizons/Builder/Props/WarpPadBuilder.cs +++ b/NewHorizons/Builder/Props/WarpPadBuilder.cs @@ -6,6 +6,7 @@ using NewHorizons.Utility; using NewHorizons.Utility.OuterWilds; using NewHorizons.Utility.OWML; +using OWML.Common; using OWML.Utils; using UnityEngine; @@ -86,10 +87,10 @@ public static void InitPrefabs() } } - public static void Make(GameObject planetGO, Sector sector, NomaiWarpReceiverInfo info) + public static void Make(GameObject planetGO, Sector sector, IModBehaviour mod, NomaiWarpReceiverInfo info) { var detailInfo = new DetailInfo(info); - var receiverObject = DetailBuilder.Make(planetGO, sector, info.detailed ? _detailedReceiverPrefab : _receiverPrefab, detailInfo); + var receiverObject = DetailBuilder.Make(planetGO, sector, mod, info.detailed ? _detailedReceiverPrefab : _receiverPrefab, detailInfo); NHLogger.Log($"Position is {detailInfo.position} was {info.position}"); @@ -122,13 +123,13 @@ public static void Make(GameObject planetGO, Sector sector, NomaiWarpReceiverInf if (info.computer != null) { - CreateComputer(planetGO, sector, info.computer, receiver); + CreateComputer(planetGO, sector, mod, info.computer, receiver); } } - public static void Make(GameObject planetGO, Sector sector, NomaiWarpTransmitterInfo info) + public static void Make(GameObject planetGO, Sector sector, IModBehaviour mod, NomaiWarpTransmitterInfo info) { - var transmitterObject = DetailBuilder.Make(planetGO, sector, _transmitterPrefab, new DetailInfo(info)); + var transmitterObject = DetailBuilder.Make(planetGO, sector, mod, _transmitterPrefab, new DetailInfo(info)); var transmitter = transmitterObject.GetComponentInChildren(); transmitter._frequency = GetFrequency(info.frequency); @@ -145,9 +146,9 @@ public static void Make(GameObject planetGO, Sector sector, NomaiWarpTransmitter transmitterObject.SetActive(true); } - private static void CreateComputer(GameObject planetGO, Sector sector, GeneralPropInfo computerInfo, NomaiWarpReceiver receiver) + private static void CreateComputer(GameObject planetGO, Sector sector, IModBehaviour mod, GeneralPropInfo computerInfo, NomaiWarpReceiver receiver) { - var computerObject = DetailBuilder.Make(planetGO, sector, TranslatorTextBuilder.ComputerPrefab, new DetailInfo(computerInfo)); + var computerObject = DetailBuilder.Make(planetGO, sector, mod, TranslatorTextBuilder.ComputerPrefab, new DetailInfo(computerInfo)); var computer = computerObject.GetComponentInChildren(); computer.SetSector(sector); diff --git a/NewHorizons/Components/Props/NHItem.cs b/NewHorizons/Components/Props/NHItem.cs new file mode 100644 index 000000000..b21db7eaa --- /dev/null +++ b/NewHorizons/Components/Props/NHItem.cs @@ -0,0 +1,103 @@ +using NewHorizons.Builder.Props; +using NewHorizons.Handlers; +using OWML.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Components.Props +{ + public class NHItem : OWItem + { + public string DisplayName; + public bool Droppable; + public AudioType PickupAudio; + public AudioType DropAudio; + public AudioType SocketAudio; + public AudioType UnsocketAudio; + public string PickupCondition; + public bool ClearPickupConditionOnDrop; + public string PickupFact; + + public ItemType ItemType + { + get => _type; + set => _type = value; + } + + public override string GetDisplayName() + { + return TranslationHandler.GetTranslation(DisplayName, TranslationHandler.TextType.UI); + } + + public override bool CheckIsDroppable() + { + return Droppable; + } + + public override void PickUpItem(Transform holdTranform) + { + base.PickUpItem(holdTranform); + TriggerPickupConditions(); + PlayCustomSound(PickupAudio); + } + + public override void DropItem(Vector3 position, Vector3 normal, Transform parent, Sector sector, IItemDropTarget customDropTarget) + { + base.DropItem(position, normal, parent, sector, customDropTarget); + TriggerDropConditions(); + PlayCustomSound(DropAudio); + } + + public override void SocketItem(Transform socketTransform, Sector sector) + { + base.SocketItem(socketTransform, sector); + TriggerDropConditions(); + PlayCustomSound(SocketAudio); + } + + public override void OnCompleteUnsocket() + { + base.OnCompleteUnsocket(); + TriggerPickupConditions(); + PlayCustomSound(UnsocketAudio); + } + + internal void TriggerPickupConditions() + { + if (!string.IsNullOrEmpty(PickupCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(PickupCondition, true); + } + if (!string.IsNullOrEmpty(PickupFact)) + { + Locator.GetShipLogManager().RevealFact(PickupFact); + } + } + + internal void TriggerDropConditions() + { + if (ClearPickupConditionOnDrop && !string.IsNullOrEmpty(PickupCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(PickupCondition, false); + } + } + + void PlayCustomSound(AudioType audioType) + { + if (ItemBuilder.IsCustomItemType(ItemType)) + { + Locator.GetPlayerAudioController()._oneShotExternalSource.PlayOneShot(audioType); + } + else + { + // Vanilla items play sounds via hard-coded ItemType switch statements + // in the PlayerAudioController code, so there's no clean way to override them + } + } + } +} diff --git a/NewHorizons/Components/Props/NHItemSocket.cs b/NewHorizons/Components/Props/NHItemSocket.cs new file mode 100644 index 000000000..bd40a96d3 --- /dev/null +++ b/NewHorizons/Components/Props/NHItemSocket.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Components.Props +{ + public class NHItemSocket : OWItemSocket + { + public bool UseGiveTakePrompts; + public string InsertCondition; + public bool ClearInsertConditionOnRemoval; + public string InsertFact; + public string RemovalCondition; + public bool ClearRemovalConditionOnInsert; + public string RemovalFact; + + public ItemType ItemType + { + get => _acceptableType; + set => _acceptableType = value; + } + + public override bool UsesGiveTakePrompts() + { + return UseGiveTakePrompts; + } + + public override bool AcceptsItem(OWItem item) + { + if (item == null || item._type == ItemType.Invalid) + { + return false; + } + return ItemType == item._type; + } + + public override bool PlaceIntoSocket(OWItem item) + { + if (base.PlaceIntoSocket(item)) + { + TriggerInsertConditions(); + return true; + } + return false; + } + + public override OWItem RemoveFromSocket() + { + var removedItem = base.RemoveFromSocket(); + if (removedItem != null) + { + TriggerRemovalConditions(); + } + return removedItem; + } + + internal void TriggerInsertConditions() + { + if (!string.IsNullOrEmpty(InsertCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(InsertCondition, true); + } + if (ClearRemovalConditionOnInsert && !string.IsNullOrEmpty(RemovalCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(RemovalCondition, false); + } + if (!string.IsNullOrEmpty(InsertFact)) + { + Locator.GetShipLogManager().RevealFact(InsertFact); + } + } + + internal void TriggerRemovalConditions() + { + if (!string.IsNullOrEmpty(RemovalCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(RemovalCondition, true); + } + if (ClearInsertConditionOnRemoval && !string.IsNullOrEmpty(InsertCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(InsertCondition, false); + } + if (!string.IsNullOrEmpty(RemovalFact)) + { + Locator.GetShipLogManager().RevealFact(RemovalFact); + } + } + } +} diff --git a/NewHorizons/External/Modules/Props/DetailInfo.cs b/NewHorizons/External/Modules/Props/DetailInfo.cs index 48b3b125f..d96545629 100644 --- a/NewHorizons/External/Modules/Props/DetailInfo.cs +++ b/NewHorizons/External/Modules/Props/DetailInfo.cs @@ -1,3 +1,4 @@ +using NewHorizons.External.Modules.Props.Item; using NewHorizons.External.SerializableData; using Newtonsoft.Json; using System; @@ -101,6 +102,16 @@ public DetailInfo(GeneralPointPropInfo info) /// [DefaultValue(true)] public bool blinkWhenActiveChanged = true; + /// + /// Should this detail be treated as an interactible item + /// + public ItemInfo item; + + /// + /// Should this detail be treated as a socket for an interactible item + /// + public ItemSocketInfo itemSocket; + [Obsolete("alignToNormal is deprecated. Use alignRadial instead")] public bool alignToNormal; } diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/SlideInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/SlideInfo.cs index 3b656dfd5..26b6053d1 100644 --- a/NewHorizons/External/Modules/Props/EchoesOfTheEye/SlideInfo.cs +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/SlideInfo.cs @@ -1,5 +1,6 @@ using NewHorizons.External.SerializableData; using Newtonsoft.Json; +using System.ComponentModel; namespace NewHorizons.External.Modules.Props.EchoesOfTheEye { @@ -7,80 +8,81 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye public class SlideInfo { /// - /// Ambient light colour when viewing this slide. + /// The path to the image file for this slide. /// - public MColor ambientLightColor; - + public string imagePath; // SlideAmbientLightModule /// /// Ambient light intensity when viewing this slide. + /// Set this to add ambient light module. Base game default is 1. /// public float ambientLightIntensity; /// /// Ambient light range when viewing this slide. /// - public float ambientLightRange; + [DefaultValue(20f)] public float ambientLightRange = 20f; + + /// + /// Ambient light colour when viewing this slide. Defaults to white. + /// + public MColor ambientLightColor; + + /// + /// Spotlight intensity modifier when viewing this slide. + /// + [DefaultValue(0f)] public float spotIntensityMod = 0f; // SlideBackdropAudioModule /// - /// The name of the AudioClip that will continuously play while watching these slides + /// The name of the AudioClip that will continuously loop while watching these slides. + /// Set this to include backdrop audio module. Base game default is Reel_1_Backdrop_A. /// public string backdropAudio; /// - /// The time to fade into the backdrop audio + /// The time to fade into the backdrop audio. /// - public float backdropFadeTime; + [DefaultValue(2f)] public float backdropFadeTime = 2f; // SlideBeatAudioModule /// /// The name of the AudioClip for a one-shot sound when opening the slide. + /// Set this to include beat audio module. Base game default is Reel_1_Beat_A. /// public string beatAudio; /// - /// The time delay until the one-shot audio + /// The time delay until the one-shot audio. /// - public float beatDelay; - + [DefaultValue(0f)] public float beatDelay = 0f; // SlideBlackFrameModule /// /// Before viewing this slide, there will be a black frame for this many seconds. + /// Set this to include black frame module. Base game default is 0. /// public float blackFrameDuration; - /// - /// The path to the image file for this slide. - /// - public string imagePath; - - // SlidePlayTimeModule /// /// Play-time duration for auto-projector slides. + /// Set this to include play time module. Base game default is 0. /// public float playTimeDuration; - // SlideShipLogEntryModule /// - /// Ship log fact revealed when viewing this slide + /// Ship log fact revealed when viewing this slide. + /// Set this to include ship log entry module. Base game default is "". /// public string reveal; - - /// - /// Spotlight intensity modifier when viewing this slide. - /// - public float spotIntensityMod; } - -} +} \ No newline at end of file diff --git a/NewHorizons/External/Modules/Props/Item/ItemInfo.cs b/NewHorizons/External/Modules/Props/Item/ItemInfo.cs new file mode 100644 index 000000000..6876bd4c4 --- /dev/null +++ b/NewHorizons/External/Modules/Props/Item/ItemInfo.cs @@ -0,0 +1,78 @@ +using NewHorizons.External.SerializableData; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Props.Item +{ + [JsonObject] + public class ItemInfo + { + /// + /// The name of the item to be displayed in the UI. Defaults to the name of the detail object. + /// + public string name; + /// + /// The type of the item, which determines its orientation when held and what sockets it fits into. This can be a custom string, or a vanilla ItemType (Scroll, WarpCode, SharedStone, ConversationStone, Lantern, SlideReel, DreamLantern, or VisionTorch). Defaults to the item name. + /// + public string itemType; + /// + /// The furthest distance where the player can interact with this item. Defaults to two meters, same as most vanilla items. Set this to zero to disable all interaction by default. + /// + [DefaultValue(2f)] public float interactRange = 2f; + /// + /// Whether the item can be dropped. Defaults to true. + /// + [DefaultValue(true)] public bool droppable = true; + /// + /// A relative offset to apply to the item's position when dropping it on the ground. + /// + public MVector3 dropOffset; + /// + /// The direction the item will be oriented when dropping it on the ground. Defaults to up (0, 1, 0). + /// + public MVector3 dropNormal; + /// + /// 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. + /// + 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. + /// + 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. + /// + 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. + /// + public string unsocketAudio; + /// + /// A dialogue condition to set when picking up this item. + /// + public string pickupCondition; + /// + /// Whether the pickup condition should be cleared when dropping the item. Defaults to true. + /// + [DefaultValue(true)] public bool clearPickupConditionOnDrop = true; + /// + /// A ship log fact to reveal when picking up this item. + /// + public string pickupFact; + /// + /// A relative path from the planet to a socket that this item will be automatically inserted into. + /// + public string pathToInitialSocket; + } +} diff --git a/NewHorizons/External/Modules/Props/Item/ItemSocketInfo.cs b/NewHorizons/External/Modules/Props/Item/ItemSocketInfo.cs new file mode 100644 index 000000000..76ac16f7d --- /dev/null +++ b/NewHorizons/External/Modules/Props/Item/ItemSocketInfo.cs @@ -0,0 +1,58 @@ +using NewHorizons.External.SerializableData; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Props.Item +{ + [JsonObject] + public class ItemSocketInfo : GeneralPropInfo + { + /// + /// The relative path to a child game object of this detail that will act as the socket point for the item. Will be used instead of this socket's positioning info if set. + /// + public string socketPath; + /// + /// The type of item allowed in this socket. This can be a custom string, or a vanilla ItemType (Scroll, WarpCode, SharedStone, ConversationStone, Lantern, SlideReel, DreamLantern, or VisionTorch). + /// + public string itemType; + /// + /// The furthest distance where the player can interact with this item socket. Defaults to two meters, same as most vanilla item sockets. Set this to zero to disable all interaction by default. + /// + [DefaultValue(2f)] public float interactRange = 2f; + /// + /// Whether to use "Give Item" / "Take Item" prompts instead of "Insert Item" / "Remove Item". + /// + public bool useGiveTakePrompts; + /// + /// A dialogue condition to set when inserting an item into this socket. + /// + public string insertCondition; + /// + /// Whether the insert condition should be cleared when removing the socketed item. Defaults to true. + /// + [DefaultValue(true)] public bool clearInsertConditionOnRemoval = true; + /// + /// A ship log fact to reveal when inserting an item into this socket. + /// + public string insertFact; + /// + /// A dialogue condition to set when removing an item from this socket, or when the socket is empty. + /// + public string removalCondition; + /// + /// Whether the removal condition should be cleared when inserting a socketed item. Defaults to true. + /// + [DefaultValue(true)] public bool clearRemovalConditionOnInsert = true; + /// + /// A ship log fact to reveal when removing an item from this socket, or when the socket is empty. + /// + public string removalFact; + } +} diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 9b73c8ba1..26e8eef66 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -377,7 +377,7 @@ public static GameObject GenerateBrambleDimensionBody(NewHorizonsBody body) ao._rootSector = sector; ao._type = AstroObject.Type.None; - BrambleDimensionBuilder.Make(body, go, ao, sector, owRigidBody); + BrambleDimensionBuilder.Make(body, go, ao, sector, body.Mod, owRigidBody); go = SharedGenerateBody(body, go, sector, owRigidBody); diff --git a/NewHorizons/INewHorizons.cs b/NewHorizons/INewHorizons.cs index 903b76e59..22132568d 100644 --- a/NewHorizons/INewHorizons.cs +++ b/NewHorizons/INewHorizons.cs @@ -16,6 +16,9 @@ public interface INewHorizons [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] void Create(Dictionary config, IModBehaviour mod); + + [Obsolete("SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignRadial) is deprecated, please use SpawnObject(IModBehaviour mod, GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignRadial) instead")] + GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal); #endregion /// @@ -105,7 +108,7 @@ public interface INewHorizons /// Allows you to spawn a copy of a prop by specifying its path. /// This is the same as using Props->details in a config, but also returns the spawned gameObject to you. /// - GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, + GameObject SpawnObject(IModBehaviour mod, GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal); /// @@ -173,5 +176,36 @@ AudioSignal SpawnSignal(IModBehaviour mod, GameObject root, string audio, string /// A dictionary of each curiousity ID to its colour and highlight colour in the ship log. Optional. void AddShipLogXML(IModBehaviour mod, XElement xml, string planetName, string imageFolder = null, Dictionary entryPositions = null, Dictionary curiousityColours = null); #endregion + + #region Translations + /// + /// Look up shiplog-related translated text for the given text key. + /// Defaults to English if no translation in the current language is available, and just the key if no English translation is available. + /// + /// The text key to look up. + /// + string GetTranslationForShipLog(string text); + /// + /// Look up dialogue-related translated text for the given text key. + /// Defaults to English if no translation in the current language is available, and just the key if no English translation is available. + /// + /// The text key to look up. + /// + string GetTranslationForDialogue(string text); + /// + /// Look up UI-related translated text for the given text key. + /// Defaults to English if no translation in the current language is available, and just the key if no English translation is available. + /// + /// The text key to look up. + /// + string GetTranslationForUI(string text); + /// + /// Look up miscellaneous translated text for the given text key. + /// Defaults to English if no translation in the current language is available, and just the key if no English translation is available. + /// + /// The text key to look up. + /// + string GetTranslationForOtherText(string text); + #endregion } } diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index e996ce2bc..5d438e9de 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -437,6 +437,7 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode) // Some builders have to be reset each loop SignalBuilder.Init(); BrambleDimensionBuilder.Init(); + ItemBuilder.Init(); AstroObjectLocator.Init(); StreamingHandler.Init(); AudioTypeHandler.Init(); diff --git a/NewHorizons/NewHorizonsApi.cs b/NewHorizons/NewHorizonsApi.cs index 0b25a339e..e5507abc0 100644 --- a/NewHorizons/NewHorizonsApi.cs +++ b/NewHorizons/NewHorizonsApi.cs @@ -70,6 +70,13 @@ public void Create(Dictionary config, IModBehaviour mod) } } + [Obsolete("SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignRadial) is deprecated, please use SpawnObject(IModBehaviour mod, GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignRadial) instead")] + public GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, + float scale, bool alignRadial) + { + return SpawnObject(null, planet, sector, propToCopyPath, position, eulerAngles, scale, alignRadial); + } + public void LoadConfigs(IModBehaviour mod) { Main.Instance.LoadConfigs(mod); @@ -170,7 +177,7 @@ public T QuerySystem(string jsonPath) return default; } - public GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, + public GameObject SpawnObject(IModBehaviour mod, GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignRadial) { var prefab = SearchUtilities.Find(propToCopyPath); @@ -181,7 +188,7 @@ public GameObject SpawnObject(GameObject planet, Sector sector, string propToCop scale = scale, alignRadial = alignRadial }; - return DetailBuilder.Make(planet, sector, prefab, detailInfo); + return DetailBuilder.Make(planet, sector, mod, prefab, detailInfo); } public AudioSignal SpawnSignal(IModBehaviour mod, GameObject root, string audio, string name, string frequency, @@ -322,5 +329,13 @@ public void AddShipLogXML(IModBehaviour mod, XElement xml, string planetName, st /// /// public void RegisterCustomBuilder(Action builder) => PlanetCreationHandler.CustomBuilders.Add(builder); + + public string GetTranslationForShipLog(string text) => TranslationHandler.GetTranslation(text, TranslationHandler.TextType.SHIPLOG); + + public string GetTranslationForDialogue(string text) => TranslationHandler.GetTranslation(text, TranslationHandler.TextType.DIALOGUE); + + public string GetTranslationForUI(string text) => TranslationHandler.GetTranslation(text, TranslationHandler.TextType.UI); + + public string GetTranslationForOtherText(string text) => TranslationHandler.GetTranslation(text, TranslationHandler.TextType.OTHER); } } diff --git a/NewHorizons/Patches/RigidbodyPatches.cs b/NewHorizons/Patches/RigidbodyPatches.cs new file mode 100644 index 000000000..c0e8d3e35 --- /dev/null +++ b/NewHorizons/Patches/RigidbodyPatches.cs @@ -0,0 +1,193 @@ +using HarmonyLib; +using NewHorizons.Utility; +using System.Collections.Generic; +using UnityEngine; + +namespace NewHorizons.Patches; + +/// +/// From QSB +/// +/// By delaying rigidbody stuff here we make copying objects with orbs work properly +/// Should also improve rafts +/// +[HarmonyPatch(typeof(OWRigidbody))] +public static class OWRigidbodyPatches +{ + private static readonly Dictionary _setParentQueue = new(); + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.Awake))] + private static bool Awake(OWRigidbody __instance) + { + __instance._transform = __instance.transform; + + if (!__instance._scaleRoot) + { + __instance._scaleRoot = __instance._transform; + } + + CenterOfTheUniverse.TrackRigidbody(__instance); + __instance._offsetApplier = __instance.gameObject.GetAddComponent(); + __instance._offsetApplier.Init(__instance); + if (__instance._simulateInSector) + { + __instance._simulateInSector.OnSectorOccupantsUpdated += __instance.OnSectorOccupantsUpdated; + } + + __instance._origParent = __instance._transform.parent; + __instance._origParentBody = __instance._origParent ? __instance._origParent.GetAttachedOWRigidbody() : null; + if (__instance._transform.parent) + { + _setParentQueue[__instance] = null; + } + + __instance._rigidbody = __instance.GetRequiredComponent(); + __instance._rigidbody.interpolation = RigidbodyInterpolation.None; + if (!__instance._autoGenerateCenterOfMass) + { + __instance._rigidbody.centerOfMass = __instance._centerOfMass; + } + + if (__instance.IsSimulatedKinematic()) + { + __instance.EnableKinematicSimulation(); + } + + __instance._origCenterOfMass = __instance.RunningKinematicSimulation() ? __instance._kinematicRigidbody.centerOfMass : __instance._rigidbody.centerOfMass; + __instance._referenceFrame = new ReferenceFrame(__instance); + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.Start))] + private static void Start(OWRigidbody __instance) + { + if (_setParentQueue.TryGetValue(__instance, out var parent)) + { + __instance._transform.parent = parent; + _setParentQueue.Remove(__instance); + } + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.OnDestroy))] + private static void OnDestroy(OWRigidbody __instance) + { + _setParentQueue.Remove(__instance); + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.Suspend), typeof(Transform), typeof(OWRigidbody))] + private static bool Suspend(OWRigidbody __instance, Transform suspensionParent, OWRigidbody suspensionBody) + { + if (!__instance._suspended || __instance._unsuspendNextUpdate) + { + __instance._suspensionBody = suspensionBody; + var direction = __instance.GetVelocity() - suspensionBody.GetPointVelocity(__instance._transform.position); + __instance._cachedRelativeVelocity = suspensionBody.transform.InverseTransformDirection(direction); + __instance._cachedAngularVelocity = __instance.RunningKinematicSimulation() ? __instance._kinematicRigidbody.angularVelocity : __instance._rigidbody.angularVelocity; + __instance.enabled = false; + __instance._offsetApplier.enabled = false; + if (__instance.RunningKinematicSimulation()) + { + __instance._kinematicRigidbody.enabled = false; + } + else + { + __instance.MakeKinematic(); + } + + if (_setParentQueue.ContainsKey(__instance)) + { + _setParentQueue[__instance] = suspensionParent; + } + else + { + __instance._transform.parent = suspensionParent; + } + + __instance._suspended = true; + __instance._unsuspendNextUpdate = false; + if (!Physics.autoSyncTransforms) + { + Physics.SyncTransforms(); + } + + if (__instance._childColliders == null) + { + __instance._childColliders = __instance.GetComponentsInChildren(); + foreach (var childCollider in __instance._childColliders) + { + childCollider.gameObject.GetAddComponent().ListenForParentBodySuspension(); + } + } + + __instance.RaiseEvent(nameof(__instance.OnSuspendOWRigidbody), __instance); + } + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.ChangeSuspensionBody))] + private static bool ChangeSuspensionBody(OWRigidbody __instance, OWRigidbody newSuspensionBody) + { + if (__instance._suspended) + { + __instance._cachedRelativeVelocity = Vector3.zero; + __instance._suspensionBody = newSuspensionBody; + if (_setParentQueue.ContainsKey(__instance)) + { + _setParentQueue[__instance] = newSuspensionBody.transform; + } + else + { + __instance._transform.parent = newSuspensionBody.transform; + } + } + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(OWRigidbody.UnsuspendImmediate))] + private static bool UnsuspendImmediate(OWRigidbody __instance, bool restoreCachedVelocity) + { + if (__instance._suspended) + { + if (__instance.RunningKinematicSimulation()) + { + __instance._kinematicRigidbody.enabled = true; + } + else + { + __instance.MakeNonKinematic(); + } + + __instance.enabled = true; + if (_setParentQueue.ContainsKey(__instance)) + { + _setParentQueue[__instance] = null; + } + else + { + __instance._transform.parent = null; + } + + if (!Physics.autoSyncTransforms) + { + Physics.SyncTransforms(); + } + + var cachedVelocity = restoreCachedVelocity ? __instance._suspensionBody.transform.TransformDirection(__instance._cachedRelativeVelocity) : Vector3.zero; + __instance.SetVelocity(__instance._suspensionBody.GetPointVelocity(__instance._transform.position) + cachedVelocity); + __instance.SetAngularVelocity(restoreCachedVelocity ? __instance._cachedAngularVelocity : Vector3.zero); + __instance._suspended = false; + __instance._suspensionBody = null; + __instance.RaiseEvent(nameof(__instance.OnUnsuspendOWRigidbody), __instance); + } + + return false; + } +} diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 380c4dfb6..35fc04b21 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -1381,6 +1381,157 @@ "type": "boolean", "description": "Should the player close their eyes while the activation state changes. Only relevant if activationCondition or deactivationCondition are set.", "default": true + }, + "item": { + "description": "Should this detail be treated as an interactible item", + "$ref": "#/definitions/ItemInfo" + }, + "itemSocket": { + "description": "Should this detail be treated as a socket for an interactible item", + "$ref": "#/definitions/ItemSocketInfo" + } + } + }, + "ItemInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the item to be displayed in the UI. Defaults to the name of the detail object." + }, + "itemType": { + "type": "string", + "description": "The type of the item, which determines its orientation when held and what sockets it fits into. This can be a custom string, or a vanilla ItemType (Scroll, WarpCode, SharedStone, ConversationStone, Lantern, SlideReel, DreamLantern, or VisionTorch). Defaults to the item name." + }, + "interactRange": { + "type": "number", + "description": "The furthest distance where the player can interact with this item. Defaults to two meters, same as most vanilla items. Set this to zero to disable all interaction by default.", + "format": "float", + "default": 2.0 + }, + "droppable": { + "type": "boolean", + "description": "Whether the item can be dropped. Defaults to true.", + "default": true + }, + "dropOffset": { + "description": "A relative offset to apply to the item's position when dropping it on the ground.", + "$ref": "#/definitions/MVector3" + }, + "dropNormal": { + "description": "The direction the item will be oriented when dropping it on the ground. Defaults to up (0, 1, 0).", + "$ref": "#/definitions/MVector3" + }, + "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." + }, + "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." + }, + "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." + }, + "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." + }, + "pickupCondition": { + "type": "string", + "description": "A dialogue condition to set when picking up this item." + }, + "clearPickupConditionOnDrop": { + "type": "boolean", + "description": "Whether the pickup condition should be cleared when dropping the item. Defaults to true.", + "default": true + }, + "pickupFact": { + "type": "string", + "description": "A ship log fact to reveal when picking up this item." + }, + "pathToInitialSocket": { + "type": "string", + "description": "A relative path from the planet to a socket that this item will be automatically inserted into." + } + } + }, + "ItemSocketInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "rotation": { + "description": "Rotation of the object", + "$ref": "#/definitions/MVector3" + }, + "alignRadial": { + "type": [ + "boolean", + "null" + ], + "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "socketPath": { + "type": "string", + "description": "The relative path to a child game object of this detail that will act as the socket point for the item. Will be used instead of this socket's positioning info if set." + }, + "itemType": { + "type": "string", + "description": "The type of item allowed in this socket. This can be a custom string, or a vanilla ItemType (Scroll, WarpCode, SharedStone, ConversationStone, Lantern, SlideReel, DreamLantern, or VisionTorch)." + }, + "interactRange": { + "type": "number", + "description": "The furthest distance where the player can interact with this item socket. Defaults to two meters, same as most vanilla item sockets. Set this to zero to disable all interaction by default.", + "format": "float", + "default": 2.0 + }, + "useGiveTakePrompts": { + "type": "boolean", + "description": "Whether to use \"Give Item\" / \"Take Item\" prompts instead of \"Insert Item\" / \"Remove Item\"." + }, + "insertCondition": { + "type": "string", + "description": "A dialogue condition to set when inserting an item into this socket." + }, + "clearInsertConditionOnRemoval": { + "type": "boolean", + "description": "Whether the insert condition should be cleared when removing the socketed item. Defaults to true.", + "default": true + }, + "insertFact": { + "type": "string", + "description": "A ship log fact to reveal when inserting an item into this socket." + }, + "removalCondition": { + "type": "string", + "description": "A dialogue condition to set when removing an item from this socket, or when the socket is empty." + }, + "clearRemovalConditionOnInsert": { + "type": "boolean", + "description": "Whether the removal condition should be cleared when inserting a socketed item. Defaults to true.", + "default": true + }, + "removalFact": { + "type": "string", + "description": "A ship log fact to reveal when removing an item from this socket, or when the socket is empty." } } }, @@ -1935,60 +2086,64 @@ "type": "object", "additionalProperties": false, "properties": { - "ambientLightColor": { - "description": "Ambient light colour when viewing this slide.", - "$ref": "#/definitions/MColor" + "imagePath": { + "type": "string", + "description": "The path to the image file for this slide." }, "ambientLightIntensity": { "type": "number", - "description": "Ambient light intensity when viewing this slide.", + "description": "Ambient light intensity when viewing this slide.\nSet this to add ambient light module. Base game default is 1.", "format": "float" }, "ambientLightRange": { "type": "number", "description": "Ambient light range when viewing this slide.", - "format": "float" + "format": "float", + "default": 20.0 + }, + "ambientLightColor": { + "description": "Ambient light colour when viewing this slide. Defaults to white.", + "$ref": "#/definitions/MColor" + }, + "spotIntensityMod": { + "type": "number", + "description": "Spotlight intensity modifier when viewing this slide.", + "format": "float", + "default": 0.0 }, "backdropAudio": { "type": "string", - "description": "The name of the AudioClip that will continuously play while watching these slides" + "description": "The name of the AudioClip that will continuously loop while watching these slides.\nSet this to include backdrop audio module. Base game default is Reel_1_Backdrop_A." }, "backdropFadeTime": { "type": "number", - "description": "The time to fade into the backdrop audio", - "format": "float" + "description": "The time to fade into the backdrop audio.", + "format": "float", + "default": 2.0 }, "beatAudio": { "type": "string", - "description": "The name of the AudioClip for a one-shot sound when opening the slide." + "description": "The name of the AudioClip for a one-shot sound when opening the slide.\nSet this to include beat audio module. Base game default is Reel_1_Beat_A." }, "beatDelay": { "type": "number", - "description": "The time delay until the one-shot audio", - "format": "float" + "description": "The time delay until the one-shot audio.", + "format": "float", + "default": 0.0 }, "blackFrameDuration": { "type": "number", - "description": "Before viewing this slide, there will be a black frame for this many seconds.", + "description": "Before viewing this slide, there will be a black frame for this many seconds.\nSet this to include black frame module. Base game default is 0.", "format": "float" }, - "imagePath": { - "type": "string", - "description": "The path to the image file for this slide." - }, "playTimeDuration": { "type": "number", - "description": "Play-time duration for auto-projector slides.", + "description": "Play-time duration for auto-projector slides.\nSet this to include play time module. Base game default is 0.", "format": "float" }, "reveal": { "type": "string", - "description": "Ship log fact revealed when viewing this slide" - }, - "spotIntensityMod": { - "type": "number", - "description": "Spotlight intensity modifier when viewing this slide.", - "format": "float" + "description": "Ship log fact revealed when viewing this slide.\nSet this to include ship log entry module. Base game default is \"\"." } } }, diff --git a/NewHorizons/Utility/DebugTools/DebugPropPlacer.cs b/NewHorizons/Utility/DebugTools/DebugPropPlacer.cs index 1db7bd363..4a107eaf4 100644 --- a/NewHorizons/Utility/DebugTools/DebugPropPlacer.cs +++ b/NewHorizons/Utility/DebugTools/DebugPropPlacer.cs @@ -157,7 +157,7 @@ public void PlaceObject(DebugRaycastData data) position = data.pos, rotation = data.rot.eulerAngles, }; - var prop = DetailBuilder.Make(planetGO, sector, prefab, detailInfo); + var prop = DetailBuilder.Make(planetGO, sector, null, prefab, detailInfo); var body = data.hitBodyGameObject.GetComponent(); if (body != null) RegisterProp(body, prop); diff --git a/NewHorizons/Utility/NewHorizonExtensions.cs b/NewHorizons/Utility/NewHorizonExtensions.cs index b292b06be..dd907c45b 100644 --- a/NewHorizons/Utility/NewHorizonExtensions.cs +++ b/NewHorizons/Utility/NewHorizonExtensions.cs @@ -305,5 +305,39 @@ public static AnimationCurve ToAnimationCurve(this HeightDensityPair[] pairs) } return curve; } + + // From QSB + public static void RaiseEvent(this T instance, string eventName, params object[] args) + { + const BindingFlags flags = BindingFlags.Instance + | BindingFlags.Static + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly; + if (typeof(T) + .GetField(eventName, flags)? + .GetValue(instance) is not MulticastDelegate multiDelegate) + { + return; + } + + multiDelegate.SafeInvoke(args); + } + + // From QSB + public static void SafeInvoke(this MulticastDelegate multicast, params object[] args) + { + foreach (var del in multicast.GetInvocationList()) + { + try + { + del.DynamicInvoke(args); + } + catch (TargetInvocationException ex) + { + NHLogger.LogError($"Error invoking delegate! {ex.InnerException}"); + } + } + } } } diff --git a/NewHorizons/manifest.json b/NewHorizons/manifest.json index 52c18523f..7755e5ee6 100644 --- a/NewHorizons/manifest.json +++ b/NewHorizons/manifest.json @@ -4,7 +4,7 @@ "author": "xen, Bwc9876, clay, MegaPiggy, John, Trifid, Hawkbar, Book", "name": "New Horizons", "uniqueName": "xen.NewHorizons", - "version": "1.17.3", + "version": "1.18.0", "owmlVersion": "2.9.8", "dependencies": [ "JohnCorby.VanillaFix", "_nebula.MenuFramework", "xen.CommonCameraUtility", "dgarro.CustomShipLogModes" ], "conflicts": [ "Raicuparta.QuantumSpaceBuddies", "PacificEngine.OW_CommonResources" ],