using NewHorizons.External.Modules; using NewHorizons.External.Modules.Props; using NewHorizons.External.Modules.Props.Audio; using NewHorizons.External.Modules.Props.Dialogue; using NewHorizons.External.Modules.Props.Quantum; using NewHorizons.External.Modules.VariableSize; using NewHorizons.External.Modules.Volumes; using NewHorizons.External.Modules.Volumes.VolumeInfos; using NewHorizons.Utility.OWML; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using UnityEngine; namespace NewHorizons.External.Configs { /// /// Describes a celestial body to generate /// [JsonObject(Title = "Celestial Body")] public class PlanetConfig { #region Fields /// /// Unique name of your planet /// [Required] public string name; /// /// Unique star system containing your planet. If you set this to be a custom solar system remember to add a Spawn module to one of the bodies, or else you can't get to the system. /// [DefaultValue("SolarSystem")] public string starSystem = "SolarSystem"; /// /// Does this config describe a quantum state of a custom planet defined in another file? /// public bool isQuantumState; /// /// Does this config describe a stellar remnant of a custom star defined in another file? /// public bool isStellarRemnant; /// /// Should this planet ever be shown on the title screen? /// [DefaultValue(true)] public bool canShowOnTitle = true; /// /// `true` if you want to delete this planet /// public bool destroy; /// /// A list of paths to child GameObjects to destroy on this planet /// public string[] removeChildren; #endregion #region Modules /// /// Add ambient lights to this body /// public AmbientLightModule[] AmbientLights; /// /// Generate asteroids around this body /// public AsteroidBeltModule AsteroidBelt; /// /// Describes this Body's atmosphere /// public AtmosphereModule Atmosphere; /// /// Base Properties of this Body /// public BaseModule Base; /// /// Add bramble nodes to this planet and/or make this planet a bramble dimension /// public BrambleModule Bramble; /// /// Add a cloaking field to this planet /// public CloakModule Cloak; /// /// Make this body into a focal point (barycenter) /// public FocalPointModule FocalPoint; /// /// Add funnel from this planet to another /// public FunnelModule Funnel; /// /// Generate the surface of this planet using a heightmap /// public HeightMapModule HeightMap; /// /// Add lava to this planet /// public LavaModule Lava; /// /// Describes this Body's orbit (or lack there of) /// public OrbitModule Orbit; /// /// Procedural Generation /// public ProcGenModule ProcGen; /// /// Spawn various objects on this body /// public PropModule Props; /// /// Reference frame properties of this body /// public ReferenceFrameModule ReferenceFrame; /// /// Create rings around the planet /// public RingModule[] Rings; /// /// Add sand to this planet /// public SandModule Sand; /// /// Add ship log entries to this planet and describe how it looks in map mode /// public ShipLogModule ShipLog; /// /// Settings for shock effect on planet when the nearest star goes supernova /// public ShockEffectModule ShockEffect; /// /// Spawn the player at this planet /// public SpawnModule Spawn; /// /// Make this body a star /// public StarModule Star; /// /// Add water to this planet /// public WaterModule Water; /// /// Add particle effects in a field around the planet. /// Also known as Vection Fields. /// public ParticleFieldModule[] ParticleFields; /// /// Add various volumes on this body /// public VolumesModule Volumes; /// /// Add a comet tail to this body, like the Interloper /// public CometTailModule CometTail; /// /// Extra data that may be used by extension mods /// public object extras; #endregion #region Obsolete [Obsolete("ChildrenToDestroy is deprecated, please use RemoveChildren instead")] public string[] childrenToDestroy; [Obsolete("Singularity is deprecated, please use Props->singularities")] public SingularityModule Singularity; [Obsolete("Signal is deprecated, please use Props->signals")] public SignalModule Signal; [Obsolete("Ring is deprecated, please use Rings")] public RingModule Ring; #endregion Obsolete #region ctor validation and migration public PlanetConfig() { // Always have to have a base module if (Base == null) Base = new BaseModule(); if (Orbit == null) Orbit = new OrbitModule(); if (ShipLog == null) ShipLog = new ShipLogModule(); if (ReferenceFrame == null) ReferenceFrame = new ReferenceFrameModule(); } public void Validate() { // If we can correct a part of the config, do it // If it cannot be solved, throw an exception if (Base.centerOfSolarSystem) Orbit.isStatic = true; if (Atmosphere?.clouds?.lightningGradient != null) Atmosphere.clouds.hasLightning = true; if (Bramble?.dimension != null && Orbit?.staticPosition == null) throw new Exception($"Dimension {name} must have Orbit.staticPosition defined."); if (Bramble?.dimension != null) canShowOnTitle = false; if (Orbit?.staticPosition != null) Orbit.isStatic = true; // For each quantum group, verify the following: // this group's id should be unique // if type == sockets, group.sockets should not be null or empty // if type == sockets, count every prop that references this group. the number should be < group.sockets.Count // if type == sockets, for each socket, if rotation == null, rotation = Vector3.zero // if type == sockets, for each socket, position must not be null // For each detail prop, // if detail.quantumGroupID != null, there exists a quantum group with that id if (Props?.quantumGroups != null && Props?.details != null) { Dictionary existingGroups = new Dictionary(); foreach (var quantumGroup in Props.quantumGroups) { if (existingGroups.ContainsKey(quantumGroup.id)) { NHLogger.LogWarning($"Duplicate quantumGroup id found: {quantumGroup.id}"); quantumGroup.type = QuantumGroupType.FailedValidation; } existingGroups[quantumGroup.id] = quantumGroup; if (quantumGroup.type == QuantumGroupType.Sockets) { if (quantumGroup.sockets?.Length == 0) { NHLogger.LogError($"quantumGroup {quantumGroup.id} is of type \"sockets\" but has no defined sockets."); quantumGroup.type = QuantumGroupType.FailedValidation; } else { foreach (var socket in quantumGroup.sockets) { if (socket.rotation == null) socket.rotation = UnityEngine.Vector3.zero; if (socket.position == null) { NHLogger.LogError($"quantumGroup {quantumGroup.id} has a socket without a position."); quantumGroup.type = QuantumGroupType.FailedValidation; } } } } } var existingGroupsPropCounts = new Dictionary(); foreach (var prop in Props?.details) { if (prop.quantumGroupID == null) continue; if (!existingGroups.ContainsKey(prop.quantumGroupID)) NHLogger.LogWarning($"A prop wants to be a part of quantum group {prop.quantumGroupID}, but this group does not exist."); else existingGroupsPropCounts[prop.quantumGroupID] = existingGroupsPropCounts.GetValueOrDefault(prop.quantumGroupID) + 1; } foreach (var quantumGroup in Props.quantumGroups) { if (quantumGroup.type == QuantumGroupType.Sockets && existingGroupsPropCounts.GetValueOrDefault(quantumGroup.id) >= quantumGroup.sockets?.Length) { NHLogger.LogError($"quantumGroup {quantumGroup.id} is of type \"sockets\" and has more props than sockets."); quantumGroup.type = QuantumGroupType.FailedValidation; } } } // Stars and focal points shouldnt be destroyed by stars if (Star != null || FocalPoint != null) Base.invulnerableToSun = true; } public void Migrate() { // Backwards compatability // Should be the only place that obsolete things are referenced #pragma warning disable 612, 618 if (Base.waterSize != 0) Water = new WaterModule { size = Base.waterSize, tint = Base.waterTint }; if (Base.lavaSize != 0) Lava = new LavaModule { size = Base.lavaSize }; if (Base.blackHoleSize != 0) Singularity = new SingularityModule { type = SingularityModule.SingularityType.BlackHole, size = Base.blackHoleSize }; if (Base.isSatellite) Base.showMinimap = false; if (!Base.hasReferenceFrame) ReferenceFrame.enabled = false; if (childrenToDestroy != null) removeChildren = childrenToDestroy; if (Base.cloakRadius != 0) Cloak = new CloakModule { radius = Base.cloakRadius }; if (Base.hasAmbientLight || Base.ambientLight != 0) { if (AmbientLights == null) AmbientLights = new AmbientLightModule[0]; AmbientLights = AmbientLights.Append(new AmbientLightModule { intensity = Base.ambientLight != 0 ? Base.ambientLight : 0.5f }).ToArray(); } if (Atmosphere != null) { if (!string.IsNullOrEmpty(Atmosphere.cloud)) Atmosphere.clouds = new AtmosphereModule.CloudInfo { outerCloudRadius = Atmosphere.size, innerCloudRadius = Atmosphere.size * 0.9f, tint = Atmosphere.cloudTint, texturePath = Atmosphere.cloud, capPath = Atmosphere.cloudCap, rampPath = Atmosphere.cloudRamp, fluidType = Atmosphere.fluidType, useBasicCloudShader = Atmosphere.useBasicCloudShader, unlit = !Atmosphere.shadowsOnClouds }; // Validate if (Atmosphere.clouds?.lightningGradient != null) Atmosphere.clouds.hasLightning = true; // Former is obsolete, latter is to validate if (Atmosphere.hasAtmosphere || Atmosphere.atmosphereTint != null) Atmosphere.useAtmosphereShader = true; // useBasicCloudShader is obsolete if (Atmosphere.clouds != null && Atmosphere.clouds.useBasicCloudShader) Atmosphere.clouds.cloudsPrefab = CloudPrefabType.Basic; if (Atmosphere.hasRain) { if (ParticleFields == null) ParticleFields = new ParticleFieldModule[0]; ParticleFields = ParticleFields.Append(new ParticleFieldModule { type = ParticleFieldModule.ParticleFieldType.Rain, rename = "RainEmitter" }).ToArray(); } if (Atmosphere.hasSnow) { if (ParticleFields == null) ParticleFields = new ParticleFieldModule[0]; for (int i = 0; i < 5; i++) { ParticleFields = ParticleFields.Append(new ParticleFieldModule { type = ParticleFieldModule.ParticleFieldType.SnowflakesHeavy, rename = "SnowEmitter" }).ToArray(); } } } if (Props?.tornados != null) foreach (var tornado in Props.tornados) if (tornado.downwards) tornado.type = TornadoInfo.TornadoType.Downwards; if (Props?.audioVolumes != null) { if (Volumes == null) Volumes = new VolumesModule(); if (Volumes.audioVolumes == null) Volumes.audioVolumes = new AudioVolumeInfo[0]; Volumes.audioVolumes = Volumes.audioVolumes.Concat(Props.audioVolumes).ToArray(); } if (Props?.reveal != null) { if (Volumes == null) Volumes = new VolumesModule(); if (Volumes.revealVolumes == null) Volumes.revealVolumes = new RevealVolumeInfo[0]; Volumes.revealVolumes = Volumes.revealVolumes.Concat(Props.reveal).ToArray(); } if (Base.sphereOfInfluence != 0f) Base.soiOverride = Base.sphereOfInfluence; // Moved a bunch of stuff off of shiplog module to star system module because it didnt exist when we made this if (ShipLog != null) { Main.SystemDict.TryGetValue(starSystem, out var system); if (ShipLog.entryPositions != null) { if (system.Config.entryPositions == null) system.Config.entryPositions = ShipLog.entryPositions; else system.Config.entryPositions = system.Config.entryPositions.Concat(ShipLog.entryPositions).ToArray(); } if (ShipLog.curiosities != null) { if (system.Config.curiosities == null) system.Config.curiosities = ShipLog.curiosities; else system.Config.curiosities = system.Config.curiosities.Concat(ShipLog.curiosities).ToArray(); } if (ShipLog.initialReveal != null) { if (system.Config.initialReveal == null) system.Config.initialReveal = ShipLog.initialReveal; else system.Config.initialReveal = system.Config.initialReveal.Concat(ShipLog.initialReveal).ToArray(); } } // Singularity is now a list in props so you can have many per planet if (Singularity != null) { if (Props == null) Props = new PropModule(); if (Props.singularities == null) Props.singularities = new SingularityModule[0]; Props.singularities = Props.singularities.Append(Singularity).ToArray(); } // Old singularity size if (Props?.singularities != null) { foreach (var singularity in Props.singularities) { if (singularity.size != 0f) { singularity.horizonRadius = singularity.size * 0.4f; switch (singularity.type) { case SingularityModule.SingularityType.BlackHole: singularity.distortRadius = singularity.size * 0.95f; break; case SingularityModule.SingularityType.WhiteHole: singularity.distortRadius = singularity.size * 2.8f; break; } } } } // Signals are now in props if (Signal?.signals != null) { if (Props == null) Props = new PropModule(); if (Props.signals == null) Props.signals = new SignalInfo[0]; Props.signals = Props.signals.Concat(Signal.signals).ToArray(); } // Star if (Star != null) { if (!Star.goSupernova) Star.stellarDeathType = StellarDeathType.None; // Gave up on supporting pulsars if (Star.stellarRemnantType == StellarRemnantType.Pulsar) Star.stellarRemnantType = StellarRemnantType.NeutronStar; } // Signals no longer use two different variables for audio if (Props?.signals != null) { foreach (var signal in Props.signals) { if (!string.IsNullOrEmpty(signal.audioClip)) signal.audio = signal.audioClip; if (!string.IsNullOrEmpty(signal.audioFilePath)) signal.audio = signal.audioFilePath; } } // Cloak if (Cloak != null) { if (!string.IsNullOrEmpty(Cloak.audioClip)) Cloak.audio = Cloak.audioClip; if (!string.IsNullOrEmpty(Cloak.audioFilePath)) Cloak.audio = Cloak.audioFilePath; } // Ring is now a list so you can have many per planet if (Ring != null) { if (Rings == null) Rings = new RingModule[0]; Rings = Rings.Append(Ring).ToArray(); } // Rings are no longer variable size module if (Rings != null) { foreach (var ring in Rings) { if (ring.curve != null) ring.scaleCurve = ring.curve; } } if (Base.zeroGravityRadius != 0f) { Volumes ??= new VolumesModule(); Volumes.zeroGravityVolumes ??= new PriorityVolumeInfo[0]; Volumes.zeroGravityVolumes = Volumes.zeroGravityVolumes.Append(new PriorityVolumeInfo() { priority = 1, rename = "ZeroGVolume", radius = Base.zeroGravityRadius, parentPath = "Volumes" }).ToArray(); } // So that old mods still have shock effects if (ShockEffect == null && Star == null && name != "Sun" && name != "EyeOfTheUniverse" && FocalPoint == null) { ShockEffect = new ShockEffectModule() { hasSupernovaShockEffect = true }; } // Spawn points reorganized to use GeneralPointPropInfo if (Spawn != null && Spawn.playerSpawn == null && Spawn.playerSpawnPoint != null) { Spawn.playerSpawn = new SpawnModule.PlayerSpawnPoint() { position = Spawn.playerSpawnPoint, rotation = Spawn.playerSpawnRotation, startWithSuit = Spawn.startWithSuit, }; } if (Spawn != null && Spawn.shipSpawn == null && Spawn.shipSpawnPoint != null) { Spawn.shipSpawn = new SpawnModule.ShipSpawnPoint() { position = Spawn.shipSpawnPoint, rotation = Spawn.shipSpawnRotation, }; } // Remote dialogue trigger reorganized to use GeneralPointPropInfo if (Props?.dialogue != null) { foreach (var dialogue in Props.dialogue) { if (dialogue.remoteTrigger == null && (dialogue.remoteTriggerPosition != null || dialogue.remoteTriggerRadius != 0)) { dialogue.remoteTrigger = new RemoteTriggerInfo { position = dialogue.remoteTriggerPosition, radius = dialogue.remoteTriggerRadius, prereqCondition = dialogue.remoteTriggerPrereqCondition, }; } } } // alignRadial added to all props with rotation; default behavior varies if (Spawn?.playerSpawn != null && Spawn.playerSpawn.rotation == null && !Spawn.playerSpawn.alignRadial.HasValue) { Spawn.playerSpawn.alignRadial = true; } if (Spawn?.shipSpawn != null && Spawn.shipSpawn.rotation == null && !Spawn.shipSpawn.alignRadial.HasValue) { Spawn.shipSpawn.alignRadial = true; } if (Props?.details != null) { foreach (var detail in Props.details) { if (!detail.alignRadial.HasValue) { detail.alignRadial = detail.alignToNormal; } } } if (Props?.proxyDetails != null) { foreach (var detail in Props.proxyDetails) { if (!detail.alignRadial.HasValue) { detail.alignRadial = detail.alignToNormal; } } } if (Props?.geysers != null) { foreach (var geyser in Props.geysers) { if (!geyser.alignRadial.HasValue && geyser.rotation == null) { geyser.alignRadial = true; } } } if (Props?.tornados != null) { foreach (var tornado in Props.tornados) { if (!tornado.alignRadial.HasValue && tornado.rotation == null) { tornado.alignRadial = true; } } } if (Props?.volcanoes != null) { foreach (var volcano in Props.volcanoes) { if (!volcano.alignRadial.HasValue && volcano.rotation == null) { volcano.alignRadial = true; } } } if (Props?.nomaiText != null) { foreach (var nomaiText in Props.nomaiText) { if (nomaiText.type == Modules.TranslatorText.NomaiTextType.Cairn) { nomaiText.type = Modules.TranslatorText.NomaiTextType.CairnBrittleHollow; } else if (nomaiText.type == Modules.TranslatorText.NomaiTextType.CairnVariant) { nomaiText.type = Modules.TranslatorText.NomaiTextType.CairnTimberHearth; } } } if (Props?.translatorText != null) { foreach (var translatorText in Props.translatorText) { if (translatorText.type == Modules.TranslatorText.NomaiTextType.Cairn) { translatorText.type = Modules.TranslatorText.NomaiTextType.CairnBrittleHollow; } else if (translatorText.type == Modules.TranslatorText.NomaiTextType.CairnVariant) { translatorText.type = Modules.TranslatorText.NomaiTextType.CairnTimberHearth; } } } if (Base.hasCometTail) { CometTail ??= new(); if (Base.cometTailRotation != null) { CometTail.rotationOverride = Base.cometTailRotation; } } if (Volumes?.destructionVolumes != null) { foreach (var destructionVolume in Volumes.destructionVolumes) { if (destructionVolume.onlyAffectsPlayerAndShip) destructionVolume.onlyAffectsPlayerRelatedBodies = true; } } } #endregion } }