/* * Copyright 2022 Peter Han * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using PeterHan.PLib.Core; using System; using System.Collections.Generic; using UnityEngine; namespace PeterHan.PLib.Buildings { /// /// A class used for creating new buildings. Abstracts many of the details to allow them /// to be used across different game versions. /// public sealed partial class PBuilding { /// /// The building ID which should precede this building ID in the plan menu. /// public string AddAfter { get; set; } /// /// Whether the building is always operational. /// public bool AlwaysOperational { get; set; } /// /// The building's animation. /// public string Animation { get; set; } /// /// The audio sounds used when placing/completing the building. /// public string AudioCategory { get; set; } /// /// The audio volume used when placing/completing the building. /// public string AudioSize { get; set; } /// /// Whether this building can break down. /// public bool Breaks { get; set; } /// /// The build menu category. /// public HashedString Category { get; set; } /// /// The construction time in seconds on x1 speed. /// public float ConstructionTime { get; set; } /// /// The decor of this building. /// public EffectorValues Decor { get; set; } /// /// The building description. /// public string Description { get; set; } /// /// Text describing the building's effect. /// public string EffectText { get; set; } /// /// Whether this building can entomb. /// public bool Entombs { get; set; } /// /// The heat generation from the exhaust in kDTU/s. /// public float ExhaustHeatGeneration { get; set; } /// /// Whether this building can flood. /// public bool Floods { get; set; } /// /// The default priority of this building, with null to not add a priority. /// public int? DefaultPriority { get; set; } /// /// The self-heating when active in kDTU/s. /// public float HeatGeneration { get; set; } /// /// The building height. /// public int Height { get; set; } /// /// The building HP until it breaks down. /// public int HP { get; set; } /// /// The ingredients required for construction. /// public IList Ingredients { get; } /// /// The building ID. /// public string ID { get; } /// /// Whether this building is an industrial machine. /// public bool IndustrialMachine { get; set; } /// /// The input conduits. /// public IList InputConduits { get; } /// /// Whether this building is (or can be) a solid tile. /// public bool IsSolidTile { get; set; } /// /// The logic ports. /// public IList LogicIO { get; } /// /// The building name. /// public string Name { get; private set; } /// /// The noise of this building (not used by Klei). /// public EffectorValues Noise { get; set; } /// /// The layer for this building. /// public ObjectLayer ObjectLayer { get; set; } /// /// The output conduits. /// public IList OutputConduits { get; } /// /// If null, the building does not overheat; otherwise, it overheats at this /// temperature in K. /// public float? OverheatTemperature { get; set; } /// /// The location where this building may be built. /// public BuildLocationRule Placement { get; set; } /// /// If null, the building has no power input; otherwise, it uses this much power. /// public PowerRequirement PowerInput { get; set; } /// /// If null, the building has no power output; otherwise, it provides this much power. /// public PowerRequirement PowerOutput { get; set; } /// /// The directions this building can face. /// public PermittedRotations RotateMode { get; set; } /// /// The scene layer for this building. /// public Grid.SceneLayer SceneLayer { get; set; } /// /// The subcategory for this building. /// /// The base game currently defines the following: /// Base: /// ladders, tiles, printing pods, doors, storage, tubes, default /// Oxygen: /// producers, scrubbers /// Power: /// generators, wires, batteries, transformers, switches /// Food: /// cooking, farming, ranching /// Plumbing: /// bathroom, pipes, pumps, valves, sensors /// HVAC: /// pipes, pumps, valves, sensors /// Refining: /// materials, oil, advanced /// Medical: /// cleaning, hospital, wellness /// Furniture: /// bed, lights, dining, recreation, pots, sculpture, electronic decor, moulding, /// canvas, dispaly, monument, signs /// Equipment: /// research, exploration, work stations, suits general, oxygen masks, atmo suits, /// jet suits, lead suits /// Utilities: /// temperature, other utilities, special /// Automation: /// wires, sensors, logic gates, utilities /// Solid Transport: /// conduit, valves, utilities /// Rocketry: /// telescopes, launch pad, railguns, engines, fuel and oxidizer, cargo, utility, /// command, fittings /// Radiation: /// HEP, uranium, radiation /// public string SubCategory { get; set; } /// /// The technology name required to unlock the building. /// public string Tech { get; set; } /// /// The view mode used when placing this building. /// public HashedString ViewMode { get; set; } /// /// The building width. /// public int Width { get; set; } /// /// Whether the building was added to the plan menu. /// private bool addedPlan; /// /// Whether the strings were added. /// private bool addedStrings; /// /// Whether the technology wes added. /// private bool addedTech; /// /// Creates a new building. All buildings thus created must be registered using /// PBuilding.Register and have an appropriate IBuildingConfig class. /// /// Building should be created in OnLoad or a post-load patch (not in static /// initializers) to give the localization framework time to patch the LocString /// containing the building name and description. /// /// The building ID. /// The building name. public PBuilding(string id, string name) { if (string.IsNullOrEmpty(id)) throw new ArgumentNullException(nameof(id)); if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); AddAfter = null; AlwaysOperational = false; Animation = ""; AudioCategory = "Metal"; AudioSize = "medium"; Breaks = true; Category = DEFAULT_CATEGORY; ConstructionTime = 10.0f; Decor = TUNING.BUILDINGS.DECOR.NONE; DefaultPriority = null; Description = "Default Building Description"; EffectText = "Default Building Effect"; Entombs = true; ExhaustHeatGeneration = 0.0f; Floods = true; HeatGeneration = 0.0f; Height = 1; Ingredients = new List(4); IndustrialMachine = false; InputConduits = new List(4); HP = 100; ID = id; LogicIO = new List(4); Name = name; Noise = TUNING.NOISE_POLLUTION.NONE; ObjectLayer = PGameUtils.GetObjectLayer(nameof(ObjectLayer.Building), ObjectLayer. Building); OutputConduits = new List(4); OverheatTemperature = null; Placement = BuildLocationRule.OnFloor; PowerInput = null; PowerOutput = null; RotateMode = PermittedRotations.Unrotatable; SceneLayer = Grid.SceneLayer.Building; // Hard coded strings in base game, no const to reference SubCategory = "default"; Tech = null; ViewMode = OverlayModes.None.ID; Width = 1; addedPlan = false; addedStrings = false; addedTech = false; } /// /// Creates the building def from this class. /// /// The Klei building def. public BuildingDef CreateDef() { // The number of fields in BuildingDef makes it somewhat impractical to detour if (Width < 1) throw new InvalidOperationException("Building width: " + Width); if (Height < 1) throw new InvalidOperationException("Building height: " + Height); if (HP < 1) throw new InvalidOperationException("Building HP: " + HP); if (ConstructionTime.IsNaNOrInfinity()) throw new InvalidOperationException("Construction time: " + ConstructionTime); // Build an ingredients list int n = Ingredients.Count; if (n < 1) throw new InvalidOperationException("No ingredients for build"); float[] quantity = new float[n]; string[] tag = new string[n]; for (int i = 0; i < n; i++) { var ingredient = Ingredients[i]; if (ingredient == null) throw new ArgumentNullException(nameof(ingredient)); quantity[i] = ingredient.Quantity; tag[i] = ingredient.Material; } // Melting point is not currently used var def = BuildingTemplates.CreateBuildingDef(ID, Width, Height, Animation, HP, Math.Max(0.1f, ConstructionTime), quantity, tag, 2400.0f, Placement, Decor, Noise); // Solid tile? if (IsSolidTile) { //def.isSolidTile = true; def.BaseTimeUntilRepair = -1.0f; def.UseStructureTemperature = false; BuildingTemplates.CreateFoundationTileDef(def); } def.AudioCategory = AudioCategory; def.AudioSize = AudioSize; if (OverheatTemperature != null) { def.Overheatable = true; def.OverheatTemperature = OverheatTemperature ?? 348.15f; } else def.Overheatable = false; // Plug in if (PowerInput != null) { def.RequiresPowerInput = true; def.EnergyConsumptionWhenActive = PowerInput.MaxWattage; def.PowerInputOffset = PowerInput.PlugLocation; } // Plug out if (PowerOutput != null) { def.RequiresPowerOutput = true; def.GeneratorWattageRating = PowerOutput.MaxWattage; def.PowerOutputOffset = PowerOutput.PlugLocation; } def.Breakable = Breaks; def.PermittedRotations = RotateMode; def.ExhaustKilowattsWhenActive = ExhaustHeatGeneration; def.SelfHeatKilowattsWhenActive = HeatGeneration; def.Floodable = Floods; def.Entombable = Entombs; def.ObjectLayer = ObjectLayer; def.SceneLayer = SceneLayer; def.ViewMode = ViewMode; // Conduits (multiple per building are hard but will be added someday...) if (InputConduits.Count > 1) throw new InvalidOperationException("Only supports one input conduit"); foreach (var conduit in InputConduits) { def.UtilityInputOffset = conduit.Location; def.InputConduitType = conduit.Type; } if (OutputConduits.Count > 1) throw new InvalidOperationException("Only supports one output conduit"); foreach (var conduit in OutputConduits) { def.UtilityOutputOffset = conduit.Location; def.OutputConduitType = conduit.Type; } return def; } /// /// Configures the building template of this building. Should be called in /// ConfigureBuildingTemplate. /// /// The game object to configure. public void ConfigureBuildingTemplate(GameObject go) { if (AlwaysOperational) ApplyAlwaysOperational(go); } /// /// Populates the logic ports of this building. Must be used after the /// PBuilding.DoPostConfigureComplete method if logic ports are required. /// /// Should be called in DoPostConfigureComplete, DoPostConfigurePreview, and /// DoPostConfigureUnderConstruction. /// /// The game object to configure. public void CreateLogicPorts(GameObject go) { SplitLogicPorts(go); } /// /// Performs the post-configure complete steps that this building object can do. /// Not exhaustive! Other components must likely be added. /// /// This method does NOT add the logic ports. Use CreateLogicPorts to do so, /// after this method has been invoked. /// /// The game object to configure. public void DoPostConfigureComplete(GameObject go) { if (InputConduits.Count == 1) { var conduitConsumer = go.AddOrGet(); foreach (var conduit in InputConduits) { conduitConsumer.alwaysConsume = true; conduitConsumer.conduitType = conduit.Type; conduitConsumer.wrongElementResult = ConduitConsumer.WrongElementResult. Store; } } if (OutputConduits.Count == 1) { var conduitDispenser = go.AddOrGet(); foreach (var conduit in OutputConduits) { conduitDispenser.alwaysDispense = true; conduitDispenser.conduitType = conduit.Type; conduitDispenser.elementFilter = null; } } if (IndustrialMachine && go.TryGetComponent(out KPrefabID id)) id.AddTag(RoomConstraints.ConstraintTags.IndustrialMachinery, false); if (PowerInput != null) go.AddOrGet(); if (PowerOutput != null) go.AddOrGet(); // Set a default priority if (DefaultPriority != null && go.TryGetComponent(out Prioritizable pr)) { Prioritizable.AddRef(go); pr.SetMasterPriority(new PrioritySetting(PriorityScreen.PriorityClass.basic, DefaultPriority ?? 5)); } } public override string ToString() { return "PBuilding[ID={0}]".F(ID); } } }