oni-priority-ux/mod/PLibBuildings/PBuilding.cs

480 lines
15 KiB
C#

/*
* 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 {
/// <summary>
/// A class used for creating new buildings. Abstracts many of the details to allow them
/// to be used across different game versions.
/// </summary>
public sealed partial class PBuilding {
/// <summary>
/// The building ID which should precede this building ID in the plan menu.
/// </summary>
public string AddAfter { get; set; }
/// <summary>
/// Whether the building is always operational.
/// </summary>
public bool AlwaysOperational { get; set; }
/// <summary>
/// The building's animation.
/// </summary>
public string Animation { get; set; }
/// <summary>
/// The audio sounds used when placing/completing the building.
/// </summary>
public string AudioCategory { get; set; }
/// <summary>
/// The audio volume used when placing/completing the building.
/// </summary>
public string AudioSize { get; set; }
/// <summary>
/// Whether this building can break down.
/// </summary>
public bool Breaks { get; set; }
/// <summary>
/// The build menu category.
/// </summary>
public HashedString Category { get; set; }
/// <summary>
/// The construction time in seconds on x1 speed.
/// </summary>
public float ConstructionTime { get; set; }
/// <summary>
/// The decor of this building.
/// </summary>
public EffectorValues Decor { get; set; }
/// <summary>
/// The building description.
/// </summary>
public string Description { get; set; }
/// <summary>
/// Text describing the building's effect.
/// </summary>
public string EffectText { get; set; }
/// <summary>
/// Whether this building can entomb.
/// </summary>
public bool Entombs { get; set; }
/// <summary>
/// The heat generation from the exhaust in kDTU/s.
/// </summary>
public float ExhaustHeatGeneration { get; set; }
/// <summary>
/// Whether this building can flood.
/// </summary>
public bool Floods { get; set; }
/// <summary>
/// The default priority of this building, with null to not add a priority.
/// </summary>
public int? DefaultPriority { get; set; }
/// <summary>
/// The self-heating when active in kDTU/s.
/// </summary>
public float HeatGeneration { get; set; }
/// <summary>
/// The building height.
/// </summary>
public int Height { get; set; }
/// <summary>
/// The building HP until it breaks down.
/// </summary>
public int HP { get; set; }
/// <summary>
/// The ingredients required for construction.
/// </summary>
public IList<BuildIngredient> Ingredients { get; }
/// <summary>
/// The building ID.
/// </summary>
public string ID { get; }
/// <summary>
/// Whether this building is an industrial machine.
/// </summary>
public bool IndustrialMachine { get; set; }
/// <summary>
/// The input conduits.
/// </summary>
public IList<ConduitConnection> InputConduits { get; }
/// <summary>
/// Whether this building is (or can be) a solid tile.
/// </summary>
public bool IsSolidTile { get; set; }
/// <summary>
/// The logic ports.
/// </summary>
public IList<LogicPorts.Port> LogicIO { get; }
/// <summary>
/// The building name.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// The noise of this building (not used by Klei).
/// </summary>
public EffectorValues Noise { get; set; }
/// <summary>
/// The layer for this building.
/// </summary>
public ObjectLayer ObjectLayer { get; set; }
/// <summary>
/// The output conduits.
/// </summary>
public IList<ConduitConnection> OutputConduits { get; }
/// <summary>
/// If null, the building does not overheat; otherwise, it overheats at this
/// temperature in K.
/// </summary>
public float? OverheatTemperature { get; set; }
/// <summary>
/// The location where this building may be built.
/// </summary>
public BuildLocationRule Placement { get; set; }
/// <summary>
/// If null, the building has no power input; otherwise, it uses this much power.
/// </summary>
public PowerRequirement PowerInput { get; set; }
/// <summary>
/// If null, the building has no power output; otherwise, it provides this much power.
/// </summary>
public PowerRequirement PowerOutput { get; set; }
/// <summary>
/// The directions this building can face.
/// </summary>
public PermittedRotations RotateMode { get; set; }
/// <summary>
/// The scene layer for this building.
/// </summary>
public Grid.SceneLayer SceneLayer { get; set; }
/// <summary>
/// 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
/// </summary>
public string SubCategory { get; set; }
/// <summary>
/// The technology name required to unlock the building.
/// </summary>
public string Tech { get; set; }
/// <summary>
/// The view mode used when placing this building.
/// </summary>
public HashedString ViewMode { get; set; }
/// <summary>
/// The building width.
/// </summary>
public int Width { get; set; }
/// <summary>
/// Whether the building was added to the plan menu.
/// </summary>
private bool addedPlan;
/// <summary>
/// Whether the strings were added.
/// </summary>
private bool addedStrings;
/// <summary>
/// Whether the technology wes added.
/// </summary>
private bool addedTech;
/// <summary>
/// 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.
/// </summary>
/// <param name="id">The building ID.</param>
/// <param name="name">The building name.</param>
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<BuildIngredient>(4);
IndustrialMachine = false;
InputConduits = new List<ConduitConnection>(4);
HP = 100;
ID = id;
LogicIO = new List<LogicPorts.Port>(4);
Name = name;
Noise = TUNING.NOISE_POLLUTION.NONE;
ObjectLayer = PGameUtils.GetObjectLayer(nameof(ObjectLayer.Building), ObjectLayer.
Building);
OutputConduits = new List<ConduitConnection>(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;
}
/// <summary>
/// Creates the building def from this class.
/// </summary>
/// <returns>The Klei building def.</returns>
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;
}
/// <summary>
/// Configures the building template of this building. Should be called in
/// ConfigureBuildingTemplate.
/// </summary>
/// <param name="go">The game object to configure.</param>
public void ConfigureBuildingTemplate(GameObject go) {
if (AlwaysOperational)
ApplyAlwaysOperational(go);
}
/// <summary>
/// Populates the logic ports of this building. Must be used <b>after</b> the
/// PBuilding.DoPostConfigureComplete method if logic ports are required.
///
/// Should be called in DoPostConfigureComplete, DoPostConfigurePreview, and
/// DoPostConfigureUnderConstruction.
/// </summary>
/// <param name="go">The game object to configure.</param>
public void CreateLogicPorts(GameObject go) {
SplitLogicPorts(go);
}
/// <summary>
/// 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,
/// <b>after</b> this method has been invoked.
/// </summary>
/// <param name="go">The game object to configure.</param>
public void DoPostConfigureComplete(GameObject go) {
if (InputConduits.Count == 1) {
var conduitConsumer = go.AddOrGet<ConduitConsumer>();
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<ConduitDispenser>();
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<EnergyConsumer>();
if (PowerOutput != null)
go.AddOrGet<EnergyGenerator>();
// 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);
}
}
}