Initial commit

This commit is contained in:
2022-12-30 00:58:33 +01:00
commit 3f1075e67f
176 changed files with 25715 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
/*
* 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;
namespace PeterHan.PLib.Buildings {
/// <summary>
/// An ingredient to be used in a building.
/// </summary>
public class BuildIngredient {
/// <summary>
/// The material tag name.
/// </summary>
public string Material { get; }
/// <summary>
/// The quantity required in kg.
/// </summary>
public float Quantity { get; }
public BuildIngredient(string name, float quantity) {
if (string.IsNullOrEmpty(name))
throw new ArgumentNullException(nameof(name));
if (quantity.IsNaNOrInfinity() || quantity <= 0.0f)
throw new ArgumentException(nameof(quantity));
Material = name;
Quantity = quantity;
}
public BuildIngredient(string[] material, int tier) : this(material[0], tier) { }
public BuildIngredient(string material, int tier) {
// Intended for use from the MATERIALS and BUILDINGS presets
if (string.IsNullOrEmpty(material))
throw new ArgumentNullException(nameof(material));
Material = material;
float qty;
switch (tier) {
case -1:
// Tier -1: 5
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER_TINY[0];
break;
case 0:
// Tier 0: 25
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER0[0];
break;
case 1:
// Tier 1: 50
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER1[0];
break;
case 2:
// Tier 2: 100
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER2[0];
break;
case 3:
// Tier 3: 200
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER3[0];
break;
case 4:
// Tier 4: 400
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER4[0];
break;
case 5:
// Tier 5: 800
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER5[0];
break;
case 6:
// Tier 6: 1200
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER6[0];
break;
case 7:
// Tier 7: 2000
qty = TUNING.BUILDINGS.CONSTRUCTION_MASS_KG.TIER7[0];
break;
default:
throw new ArgumentException("tier must be between -1 and 7 inclusive");
}
Quantity = qty;
}
public override bool Equals(object obj) {
return obj is BuildIngredient other && other.Material == Material && other.
Quantity == Quantity;
}
public override int GetHashCode() {
return Material.GetHashCode();
}
public override string ToString() {
return "Material[Tag={0},Quantity={1:F0}]".F(Material, Quantity);
}
}
}

View File

@@ -0,0 +1,271 @@
/*
* 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 visualizer that colors cells with an overlay when a building is selected or being
/// previewed.
/// </summary>
public abstract class ColoredRangeVisualizer : KMonoBehaviour {
/// <summary>
/// The anim name to use when visualizing.
/// </summary>
private const string ANIM_NAME = "transferarmgrid_kanim";
/// <summary>
/// The animations to play when the visualization is created.
/// </summary>
private static readonly HashedString[] PRE_ANIMS = new HashedString[] {
"grid_pre",
"grid_loop"
};
/// <summary>
/// The animation to play when the visualization is destroyed.
/// </summary>
private static readonly HashedString POST_ANIM = "grid_pst";
/// <summary>
/// The layer on which to display the visualizer.
/// </summary>
public Grid.SceneLayer Layer { get; set; }
// These components are automatically populated by KMonoBehaviour
#pragma warning disable IDE0044 // Add readonly modifier
#pragma warning disable CS0649
[MyCmpGet]
protected BuildingPreview preview;
[MyCmpGet]
protected Rotatable rotatable;
#pragma warning restore CS0649
#pragma warning restore IDE0044 // Add readonly modifier
/// <summary>
/// The cells where animations are being displayed.
/// </summary>
private readonly HashSet<VisCellData> cells;
protected ColoredRangeVisualizer() {
cells = new HashSet<VisCellData>();
Layer = Grid.SceneLayer.FXFront;
}
/// <summary>
/// Creates or updates the visualizers as necessary.
/// </summary>
private void CreateVisualizers() {
var visCells = HashSetPool<VisCellData, ColoredRangeVisualizer>.Allocate();
var newCells = ListPool<VisCellData, ColoredRangeVisualizer>.Allocate();
try {
if (gameObject != null)
VisualizeCells(visCells);
// Destroy cells that are not used in the new one
foreach (var cell in cells)
if (visCells.Remove(cell))
newCells.Add(cell);
else
cell.Destroy();
// Newcomers get their controller created and added to the list
foreach (var newCell in visCells) {
newCell.CreateController(Layer);
newCells.Add(newCell);
}
// Copy back to global
cells.Clear();
foreach (var cell in newCells)
cells.Add(cell);
} finally {
visCells.Recycle();
newCells.Recycle();
}
}
/// <summary>
/// Called when cells are changed in the building radius.
/// </summary>
private void OnCellChange() {
CreateVisualizers();
}
protected override void OnCleanUp() {
Unsubscribe((int)GameHashes.SelectObject);
if (preview != null) {
Singleton<CellChangeMonitor>.Instance.UnregisterCellChangedHandler(transform,
OnCellChange);
if (rotatable != null)
Unsubscribe((int)GameHashes.Rotated);
}
RemoveVisualizers();
base.OnCleanUp();
}
/// <summary>
/// Called when the object is rotated.
/// </summary>
private void OnRotated(object _) {
CreateVisualizers();
}
/// <summary>
/// Called when the object is selected.
/// </summary>
/// <param name="data">true if selected, or false if deselected.</param>
private void OnSelect(object data) {
if (data is bool selected) {
var position = transform.position;
// Play the appropriate sound and update the visualizers
if (selected) {
PGameUtils.PlaySound("RadialGrid_form", position);
CreateVisualizers();
} else {
PGameUtils.PlaySound("RadialGrid_disappear", position);
RemoveVisualizers();
}
}
}
protected override void OnSpawn() {
base.OnSpawn();
Subscribe((int)GameHashes.SelectObject, OnSelect);
if (preview != null) {
// Previews can be moved
Singleton<CellChangeMonitor>.Instance.RegisterCellChangedHandler(transform,
OnCellChange, nameof(ColoredRangeVisualizer) + ".OnSpawn");
if (rotatable != null)
Subscribe((int)GameHashes.Rotated, OnRotated);
}
}
/// <summary>
/// Removes all of the visualizers.
/// </summary>
private void RemoveVisualizers() {
foreach (var cell in cells)
cell.Destroy();
cells.Clear();
}
/// <summary>
/// Calculates the offset cell from the specified starting point, including the
/// rotation of this object.
/// </summary>
/// <param name="baseCell">The starting cell.</param>
/// <param name="offset">The offset if the building had its default rotation.</param>
/// <returns>The computed destination cell.</returns>
protected int RotateOffsetCell(int baseCell, CellOffset offset) {
if (rotatable != null)
offset = rotatable.GetRotatedCellOffset(offset);
return Grid.OffsetCell(baseCell, offset);
}
/// <summary>
/// Called when cell visualizations need to be updated. Visualized cells should be
/// added to the collection supplied as an argument.
/// </summary>
/// <param name="newCells">The cells which should be visualized.</param>
protected abstract void VisualizeCells(ICollection<VisCellData> newCells);
/// <summary>
/// Stores the data about a particular cell, including its anim controller and tint
/// color.
/// </summary>
protected sealed class VisCellData : IComparable<VisCellData> {
/// <summary>
/// The target cell.
/// </summary>
public int Cell { get; }
/// <summary>
/// The anim controller for this cell.
/// </summary>
public KBatchedAnimController Controller { get; private set; }
/// <summary>
/// The tint used for this cell.
/// </summary>
public Color Tint { get; }
/// <summary>
/// Creates a visualized cell.
/// </summary>
/// <param name="cell">The cell to visualize.</param>
public VisCellData(int cell) : this(cell, Color.white) { }
/// <summary>
/// Creates a visualized cell.
/// </summary>
/// <param name="cell">The cell to visualize.</param>
/// <param name="tint">The color to tint it.</param>
public VisCellData(int cell, Color tint) {
Cell = cell;
Controller = null;
Tint = tint;
}
public int CompareTo(VisCellData other) {
if (other == null)
throw new ArgumentNullException(nameof(other));
return Cell.CompareTo(other.Cell);
}
/// <summary>
/// Creates the anim controller for this cell.
/// </summary>
/// <param name="sceneLayer">The layer on which to display the animation.</param>
public void CreateController(Grid.SceneLayer sceneLayer) {
Controller = FXHelpers.CreateEffect(ANIM_NAME, Grid.CellToPosCCC(Cell,
sceneLayer), null, false, sceneLayer, true);
Controller.destroyOnAnimComplete = false;
Controller.visibilityType = KAnimControllerBase.VisibilityType.Always;
Controller.gameObject.SetActive(true);
Controller.Play(PRE_ANIMS, KAnim.PlayMode.Loop);
Controller.TintColour = Tint;
}
/// <summary>
/// Destroys the anim controller for this cell.
/// </summary>
public void Destroy() {
if (Controller != null) {
Controller.destroyOnAnimComplete = true;
Controller.Play(POST_ANIM, KAnim.PlayMode.Once, 1f, 0f);
Controller = null;
}
}
public override bool Equals(object obj) {
return obj is VisCellData other && other.Cell == Cell && Tint.Equals(other.
Tint);
}
public override int GetHashCode() {
return Cell;
}
public override string ToString() {
return "CellData[cell={0:D},color={1}]".F(Cell, Tint);
}
}
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.
*/
namespace PeterHan.PLib.Buildings {
/// <summary>
/// Represents a pipe connection to a building.
/// </summary>
public class ConduitConnection {
/// <summary>
/// The conduit location.
/// </summary>
public CellOffset Location { get; }
/// <summary>
/// The conduit type.
/// </summary>
public ConduitType Type { get; }
public ConduitConnection(ConduitType type, CellOffset location) {
Location = location;
Type = type;
}
public override string ToString() {
return string.Format("Connection[Type={0},Location={1}]", Type, Location);
}
}
}

View File

@@ -0,0 +1,158 @@
/*
* 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.Collections.Generic;
using UnityEngine;
namespace PeterHan.PLib.Buildings {
/// <summary>
/// Utility methods for creating new buildings.
/// </summary>
public sealed partial class PBuilding {
/// <summary>
/// The default building category.
/// </summary>
private static readonly HashedString DEFAULT_CATEGORY = new HashedString("Base");
/// <summary>
/// Makes the building always operational.
/// </summary>
/// <param name="go">The game object to configure.</param>
private static void ApplyAlwaysOperational(GameObject go) {
// Remove default components that could make a building non-operational
if (go.TryGetComponent(out BuildingEnabledButton enabled))
Object.DestroyImmediate(enabled);
if (go.TryGetComponent(out Operational op))
Object.DestroyImmediate(op);
if (go.TryGetComponent(out LogicPorts lp))
Object.DestroyImmediate(lp);
}
/// <summary>
/// Creates a logic port, in a method compatible with both the new and old Automation
/// updates. The port will have the default strings which fit well with the
/// LogicOperationalController.
/// </summary>
/// <returns>A logic port compatible with both editions.</returns>
public static LogicPorts.Port CompatLogicPort(LogicPortSpriteType type,
CellOffset offset) {
return new LogicPorts.Port(LogicOperationalController.PORT_ID, offset,
STRINGS.UI.LOGIC_PORTS.CONTROL_OPERATIONAL,
STRINGS.UI.LOGIC_PORTS.CONTROL_OPERATIONAL_ACTIVE,
STRINGS.UI.LOGIC_PORTS.CONTROL_OPERATIONAL_INACTIVE, false, type);
}
/// <summary>
/// Adds the building to the plan menu.
/// </summary>
public void AddPlan() {
if (!addedPlan && Category.IsValid) {
bool add = false;
foreach (var menu in TUNING.BUILDINGS.PLANORDER)
if (menu.category == Category) {
AddPlanToCategory(menu);
add = true;
break;
}
if (!add)
PUtil.LogWarning("Unable to find build menu: " + Category);
addedPlan = true;
}
}
/// <summary>
/// Adds a building to a specific plan menu.
/// </summary>
/// <param name="menu">The menu to which to add the building.</param>
private void AddPlanToCategory(PlanScreen.PlanInfo menu) {
// Found category
var data = menu.buildingAndSubcategoryData;
if (data != null) {
string addID = AddAfter;
bool add = false;
if (addID != null) {
// Optionally choose the position
int n = data.Count;
for (int i = 0; i < n - 1 && !add; i++)
if (data[i].Key == addID) {
data.Insert(i + 1, new KeyValuePair<string, string>(ID,
SubCategory));
add = true;
}
}
if (!add)
data.Add(new KeyValuePair<string, string>(ID, SubCategory));
} else
PUtil.LogWarning("Build menu " + Category + " has invalid entries!");
}
/// <summary>
/// Adds the building strings to the strings list.
/// </summary>
public void AddStrings() {
if (!addedStrings) {
string prefix = "STRINGS.BUILDINGS.PREFABS." + ID.ToUpperInvariant() + ".";
string nameStr = prefix + "NAME";
if (Strings.TryGet(nameStr, out StringEntry localized))
Name = localized.String;
else
Strings.Add(nameStr, Name);
// Allow null values to be defined in LocString class adds / etc
if (Description != null)
Strings.Add(prefix + "DESC", Description);
if (EffectText != null)
Strings.Add(prefix + "EFFECT", EffectText);
addedStrings = true;
}
}
/// <summary>
/// Adds the building tech to the tech tree.
/// </summary>
public void AddTech() {
if (!addedTech && Tech != null) {
var technology = Db.Get().Techs?.TryGet(Tech);
if (technology != null)
technology.unlockedItemIDs?.Add(ID);
addedTech = true;
}
}
/// <summary>
/// Splits up logic input/output ports and configures the game object with them.
/// </summary>
/// <param name="go">The game object to configure.</param>
private void SplitLogicPorts(GameObject go) {
int n = LogicIO.Count;
var inputs = new List<LogicPorts.Port>(n);
var outputs = new List<LogicPorts.Port>(n);
foreach (var port in LogicIO)
if (port.spriteType == LogicPortSpriteType.Output)
outputs.Add(port);
else
inputs.Add(port);
// This works in both the old and new versions
var ports = go.AddOrGet<LogicPorts>();
if (inputs.Count > 0)
ports.inputPortInfo = inputs.ToArray();
if (outputs.Count > 0)
ports.outputPortInfo = outputs.ToArray();
}
}
}

View File

@@ -0,0 +1,479 @@
/*
* 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);
}
}
}

View File

@@ -0,0 +1,208 @@
/*
* 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 HarmonyLib;
using PeterHan.PLib.Core;
using PeterHan.PLib.PatchManager;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace PeterHan.PLib.Buildings {
/// <summary>
/// Manages PLib buildings to break down PBuilding into a more reasonable sized class.
/// </summary>
public sealed class PBuildingManager : PForwardedComponent {
/// <summary>
/// The version of this component. Uses the running PLib version.
/// </summary>
internal static readonly Version VERSION = new Version(PVersion.VERSION);
/// <summary>
/// The instantiated copy of this class.
/// </summary>
internal static PBuildingManager Instance { get; private set; }
/// <summary>
/// Immediately adds an <i>existing</i> building ID to an existing technology ID in the
/// tech tree.
///
/// Do <b>not</b> use this method on buildings registered through PBuilding as they
/// are added automatically.
///
/// This method must be used in a Db.Initialize postfix patch or RunAt.AfterDbInit
/// PPatchManager method/patch.
/// </summary>
/// <param name="tech">The technology tree node ID.</param>
/// <param name="id">The building ID to add to that node.</param>
public static void AddExistingBuildingToTech(string tech, string id) {
if (string.IsNullOrEmpty(tech))
throw new ArgumentNullException(nameof(tech));
if (string.IsNullOrEmpty(id))
throw new ArgumentNullException(nameof(id));
var technology = Db.Get().Techs?.TryGet(id);
if (technology != null)
technology.unlockedItemIDs?.Add(tech);
}
private static void CreateBuildingDef_Postfix(BuildingDef __result, string anim,
string id) {
var animFiles = __result?.AnimFiles;
if (animFiles != null && animFiles.Length > 0 && animFiles[0] == null)
Debug.LogWarningFormat("(when looking for KAnim named {0} on building {1})",
anim, id);
}
private static void CreateEquipmentDef_Postfix(EquipmentDef __result, string Anim,
string Id) {
var anim = __result?.Anim;
if (anim == null)
Debug.LogWarningFormat("(when looking for KAnim named {0} on equipment {1})",
Anim, Id);
}
/// <summary>
/// Logs a message encountered by the PLib building system.
/// </summary>
/// <param name="message">The debug message.</param>
internal static void LogBuildingDebug(string message) {
Debug.LogFormat("[PLibBuildings] {0}", message);
}
private static void LoadGeneratedBuildings_Prefix() {
Instance?.AddAllStrings();
}
/// <summary>
/// The buildings which need to be registered.
/// </summary>
private readonly ICollection<PBuilding> buildings;
public override Version Version => VERSION;
/// <summary>
/// Creates a building manager to register PLib buildings.
/// </summary>
public PBuildingManager() {
buildings = new List<PBuilding>(16);
}
/// <summary>
/// Adds the strings for every registered building in all mods to the database.
/// </summary>
private void AddAllStrings() {
InvokeAllProcess(0, null);
}
/// <summary>
/// Adds the strings for each registered building in this mod to the database.
/// </summary>
private void AddStrings() {
int n = buildings.Count;
if (n > 0) {
LogBuildingDebug("Register strings for {0:D} building(s) from {1}".F(n,
Assembly.GetExecutingAssembly().GetNameSafe() ?? "?"));
foreach (var building in buildings)
if (building != null) {
building.AddStrings();
building.AddPlan();
}
}
}
/// <summary>
/// Adds the techs for every registered building in all mods to the database.
/// </summary>
private void AddAllTechs() {
InvokeAllProcess(1, null);
}
/// <summary>
/// Adds the techs for each registered building in this mod to the database.
/// </summary>
private void AddTechs() {
int n = buildings.Count;
if (n > 0) {
LogBuildingDebug("Register techs for {0:D} building(s) from {1}".F(n,
Assembly.GetExecutingAssembly().GetNameSafe() ?? "?"));
foreach (var building in buildings)
if (building != null) {
building.AddTech();
}
}
}
/// <summary>
/// Registers a building to properly display its name, description, and tech tree
/// entry. PLib must be initialized using InitLibrary before using this method. Each
/// building should only be registered once, either in OnLoad or a post-load patch.
/// </summary>
/// <param name="building">The building to register.</param>
public void Register(PBuilding building) {
if (building == null)
throw new ArgumentNullException(nameof(building));
RegisterForForwarding();
// Must use object as the building table type
buildings.Add(building);
#if DEBUG
PUtil.LogDebug("Registered building: {0}".F(building.ID));
#endif
}
public override void Initialize(Harmony plibInstance) {
Instance = this;
// Non essential, do not crash on fail
try {
plibInstance.Patch(typeof(BuildingTemplates), nameof(BuildingTemplates.
CreateBuildingDef), postfix: PatchMethod(nameof(
CreateBuildingDef_Postfix)));
plibInstance.Patch(typeof(EquipmentTemplates), nameof(EquipmentTemplates.
CreateEquipmentDef), postfix: PatchMethod(nameof(
CreateEquipmentDef_Postfix)));
} catch (Exception e) {
#if DEBUG
PUtil.LogExcWarn(e);
#endif
}
plibInstance.Patch(typeof(GeneratedBuildings), nameof(GeneratedBuildings.
LoadGeneratedBuildings), prefix: PatchMethod(nameof(
LoadGeneratedBuildings_Prefix)));
// Avoid another Harmony patch by using PatchManager
var pm = new PPatchManager(plibInstance);
pm.RegisterPatch(RunAt.AfterDbInit, new BuildingTechRegistration());
}
public override void Process(uint operation, object _) {
if (operation == 0)
AddStrings();
else if (operation == 1)
AddTechs();
}
/// <summary>
/// A Patch Manager patch which registers all PBuilding technologies.
/// </summary>
private sealed class BuildingTechRegistration : IPatchMethodInstance {
public void Run(Harmony instance) {
Instance?.AddAllTechs();
}
}
}
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Title>PLib Buildings</Title>
<AssemblyTitle>PLib.Buildings</AssemblyTitle>
<Version>4.11.0.0</Version>
<UsesPLib>false</UsesPLib>
<RootNamespace>PeterHan.PLib.Buildings</RootNamespace>
<AssemblyVersion>4.11.0.0</AssemblyVersion>
<DistributeMod>false</DistributeMod>
<PLibCore>true</PLibCore>
<Platforms>Vanilla;Mergedown</Platforms>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\$(Platform)\Release\PLibBuildings.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,48 @@
/*
* 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;
namespace PeterHan.PLib.Buildings {
/// <summary>
/// Stores related information about a building's power requirements.
/// </summary>
public class PowerRequirement {
/// <summary>
/// The maximum building wattage.
/// </summary>
public float MaxWattage { get; }
/// <summary>
/// The location of the plug related to the foundation tile.
/// </summary>
public CellOffset PlugLocation { get; }
public PowerRequirement(float wattage, CellOffset plugLocation) {
if (wattage.IsNaNOrInfinity() || wattage < 0.0f)
throw new ArgumentException("wattage");
MaxWattage = wattage;
PlugLocation = plugLocation;
}
public override string ToString() {
return "Power[Watts={0:F0},Location={1}]".F(MaxWattage, PlugLocation);
}
}
}