using System; using System.Collections.Generic; using System.Linq; using HydroponicsExpanded.Enums; using HydroponicsExpanded.ModExtension; using HydroponicsExpanded.Utility; using RimWorld; using UnityEngine; using Verse; using Verse.AI; using Verse.Sound; namespace HydroponicsExpanded { [StaticConstructorOnStartup] public class BuildingDenseHydroponicsBasin : Building_PlantGrower, IThingHolder, IPlantToGrowSettable { private ThingOwner _innerContainer; private int _capacity = 4; // Modified by CapacityExtension DefModExtension private float _highestGrowth = 0f; private CompPowerTrader _compPowerTrader; private HydroponicsStage _stage = HydroponicsStage.Sowing; public HydroponicsStage Stage { get => _stage; private set { switch (value) { case HydroponicsStage.Sowing: case HydroponicsStage.Harvest: _compPowerTrader.PowerOutput = -_compPowerTrader.Props.idlePowerDraw; break; case HydroponicsStage.Grow: _compPowerTrader.PowerOutput = -_compPowerTrader.Props.PowerConsumption; break; default: throw new ArgumentOutOfRangeException(); } _stage = value; } } public BuildingDenseHydroponicsBasin() { _innerContainer = new ThingOwner(this, false); } IEnumerable IPlantToGrowSettable.Cells { get { return this.OccupiedRect().Cells; } } private void SowTick() { bool capacityReached = _innerContainer.Count >= _capacity; // TODO: Why is this here? foreach (Plant plant in PlantsOnMe) { // Blighted plants will be destroyed and not added to the internal container. // Once capacity is reached, all plants will be ignored. if (capacityReached || plant.Blighted) { plant.Destroy(); continue; } // When plants are being sown, they are invisible, but we want to wait until they are sown before adding them to the internal container. // When plants are harvested, they are placed on top, but we don't want to take those. Therefore, we only want 'Growing' stage plants. if (plant.LifeStage != PlantLifeStage.Growing) continue; // Otherwise, we move the plant underground. plant.DeSpawn(); TryAcceptThing(plant); // Recalculate if capacity was reached capacityReached = _innerContainer.Count >= _capacity; } // If the maximum capacity is reached, then we should move to the growing stage. if (capacityReached) { // Some plants might have been skipped, so go back and kill anything still on top. foreach (Plant plant in PlantsOnMe) plant.Destroy(); SoundDefOf.CryptosleepCasket_Accept.PlayOneShot(new TargetInfo(Position, Map)); Stage = HydroponicsStage.Grow; // Some active sowing jobs may be in progress for pathing, and thus will sow plants AFTER the stage // transition is made. This results in some weird looking random plants very rarely. // In order to fix this, we'll just remove all pending sowing jobs that relate to THIS hydroponics. CancelActiveJobs(); } } private void GrowTick() { // If there are no plants stored, reset the growth % and move back to the sowing stage. if (_innerContainer.Count == 0) { Stage = HydroponicsStage.Sowing; _highestGrowth = 0f; return; } // Despite the name, this is just a power check. We don't want to grow the plants if there is no power. if (!base.CanAcceptSowNow()) return; // The first plant in the container is the 'tracked' plant. // This set/type check is for preventing NPE warnings. The previous .Count check implicitly makes this impossible, but the compiler doesn't know that. if (!(_innerContainer[0] is Plant growthTrackingPlant)) { Log.Message( $"Unexpected thing in BuildingDenseHydroponicsBasin.innerContainer ({_innerContainer.GetType()})"); return; } // Temperature & time of day check. float temperature = Position.GetTemperature(Map); if (temperature.Between(10f, 42f) && GenLocalDate.DayPercent(this).Between(0.25f, 0.8f)) { float growthAmount = 1f / (60_000f * growthTrackingPlant.def.plant.growDays) * 250f; // Debug gizmo can set growth to 100%, thus Math.min check here. growthTrackingPlant.Growth = Math.Min(1f, growthTrackingPlant.Growth + def.fertility * growthAmount); _highestGrowth = growthTrackingPlant.Growth; } // When growth is complete, move to the harvesting stage. // This is ran even if growing is not available as debug gizmo can push growth to 'mature' stage outside normal growing times. if (growthTrackingPlant.LifeStage == PlantLifeStage.Mature) Stage = HydroponicsStage.Harvest; } private void HarvestTick() { // var plantsLeft = _innerContainer.Count; // var potentialCellCount = this.OccupiedRect().Area; // Try to place every plant in the container in any cell. foreach (Thing nextInnerThing in _innerContainer) { var nextPlant = (Plant)nextInnerThing; int occupiedCells = 0; foreach (IntVec3 currentCell in this.OccupiedRect()) { List cellThings = Map.thingGrid.ThingsListAt(currentCell); // Skip this cell if it's occupied by another plant. if (cellThings.OfType().Any()) { occupiedCells++; continue; } nextPlant.Growth = 1f; _innerContainer.TryDrop(nextPlant, currentCell, Map, ThingPlaceMode.Direct, out _); break; } if (occupiedCells >= this.OccupiedRect().Area) break; } // Re-harvestable plants will be destroyed if we think they've been harvested recently. foreach (Plant plant in PlantsOnMe) { if (plant.def.plant.HarvestDestroys) continue; // Only consider re-harvestable plants eligible if they're still within 20% of their harvest growth level, // up to 90%. This may need tuning if there are harvestable plants that go to 90% growth. var minGrowth = plant.def.plant.harvestAfterGrowth; if (plant.Growth.Between(minGrowth, Math.Min(0.9f, minGrowth + 0.2f), inclusive: true)) plant.Destroy(); } // All plants have been harvested. Switch back to sowing stage. if (_innerContainer.Count == 0) Stage = HydroponicsStage.Sowing; } private void TickStage(HydroponicsStage stage) { switch (stage) { case HydroponicsStage.Sowing: SowTick(); break; case HydroponicsStage.Grow: GrowTick(); break; case HydroponicsStage.Harvest: HarvestTick(); break; default: throw new ArgumentOutOfRangeException( nameof(stage), "Unable to select stage tick for BuildingDenseHydroponicsBasin.TickRare."); } } public override void TickRare() { // Tick the current stage. HydroponicsStage initialStage = Stage; TickStage(Stage); // If the stage changed, re-run the next tick. This can allow for instant Grow -> Harvest transition. if (_stage != initialStage) TickStage(Stage); // Apply rotting damage to all plants while power is cut. if (!base.CanAcceptSowNow()) { foreach (Thing thing in _innerContainer) ((Plant)thing).TakeDamage(new DamageInfo(DamageDefOf.Rotting, 1f)); foreach (Plant plant in PlantsOnMe) plant.TakeDamage(new DamageInfo(DamageDefOf.Rotting, 1f)); } } /** * This method cancels all active sowing jobs that interact with this given hydroponics' growing zone. */ private void CancelActiveJobs() { CellRect region = this.OccupiedRect(); foreach (Pawn pawn in Map.mapPawns.AllPawnsSpawned) { // Prisoners can't do sow jobs (slaves could though) if (pawn.IsPrisoner) continue; foreach (Job job in pawn.jobs.AllJobs()) { // Only worry about sowing jobs if (job.def != JobDefOf.Sow) continue; // Only care if it's in our hydroponics region if (!region.Contains(job.targetA.Cell)) continue; Log.Message($"Canceled a Sow Job at {job.targetA.Cell} for {pawn.NameFullColored}"); // Cancel the job, make sure it doesn't get regenerated pawn.jobs.EndCurrentOrQueuedJob(job, JobCondition.Incompletable, false); } } } private static readonly Material HydroponicPoweredFillMaterial = SolidColorMaterials.SimpleSolidColorMaterial(new Color(0.2f, 0.85f, 0.2f)); private static readonly Material HydroponicUnpoweredFillMaterial = SolidColorMaterials.SimpleSolidColorMaterial(new Color(0.82f, 0f, 0f)); private static readonly Material HydroponicUnfilledMaterial = SolidColorMaterials.SimpleSolidColorMaterial(new Color(0.3f, 0.3f, 0.3f)); protected override void DrawAt(Vector3 drawLoc, bool flip=false) { base.DrawAt(drawLoc, flip); // Only draw growth percentage bar during Sowing stage if (_stage == HydroponicsStage.Grow) { var bar = new GenDraw.FillableBarRequest { center = drawLoc + Vector3.up * 0.1f, size = new Vector2(DrawSize.y - 0.4f, DrawSize.x - 0.4f), margin = 0.15f, fillPercent = _highestGrowth, // Switch to red when no power is provided. filledMat = base.CanAcceptSowNow() ? HydroponicPoweredFillMaterial : HydroponicUnpoweredFillMaterial, unfilledMat = HydroponicUnfilledMaterial }; Rot4 rotation = Rotation; rotation.Rotate(RotationDirection.Clockwise); bar.rotation = rotation; GenDraw.DrawFillableBar(bar); } } public override string GetInspectString() { string inspectString = base.GetInspectString(); // Include information about the current growth stage of the basin inspectString += "\n"; switch (_stage) { case HydroponicsStage.Sowing: inspectString += "HydroponicsExpanded.SowingStage".Translate(); break; case HydroponicsStage.Grow: inspectString += "HydroponicsExpanded.GrowStage".Translate(); break; case HydroponicsStage.Harvest: inspectString += "HydroponicsExpanded.HarvestStage".Translate(); break; default: throw new ArgumentOutOfRangeException(); } inspectString += "\n"; inspectString += "HydroponicsExpanded.OccupiedBays".Translate() + $": {_innerContainer.Count()} / {_capacity}"; if (_innerContainer.Count > 0) { inspectString += "\n"; inspectString += "HydroponicsExpanded.Growth".Translate() + $": {_highestGrowth * 100f:#0}%"; } return inspectString; } protected virtual bool TryAcceptThing(Thing thing) { // Make sure that the plant container can accept the plant Thing. if (!_innerContainer.CanAcceptAnyOf(thing)) return false; // If the thing does not have an owner, just add it. Perhaps it was created in memory? if (thing.holdingOwner == null) return _innerContainer.TryAdd(thing); // Thing has an owner, so use TryTransferToContainer. thing.holdingOwner.TryTransferToContainer(thing, _innerContainer, thing.stackCount); return true; } /** * Only allows sowing in the 'sowing' stage. Otherwise, the hydroponics bay is 'sealed' while * all the plants are growing. */ public new bool CanAcceptSowNow() { return base.CanAcceptSowNow() && _stage == HydroponicsStage.Sowing; } public override void SpawnSetup(Map map, bool respawningAfterLoad) { base.SpawnSetup(map, respawningAfterLoad); _compPowerTrader = GetComp(); var modExtension = def.GetModExtension(); if (modExtension != null) { _capacity = modExtension.capacity; } // Delete all plants underneath the hydroponics if (!respawningAfterLoad) foreach (Plant plant in PlantsOnMe) { plant.Destroy(); } } public void GetChildHolders(List outChildren) { ThingOwnerUtility.AppendThingHoldersFromThings(outChildren, GetDirectlyHeldThings()); } public ThingOwner GetDirectlyHeldThings() { // TODO: Why was the original mod returning 'null'? Is this the intended usage? Research. return _innerContainer; } public override void ExposeData() { base.ExposeData(); Scribe_Deep.Look(ref _innerContainer, "innerContainer", this); Scribe_Values.Look(ref _stage, "growingStage", HydroponicsStage.Grow); Scribe_Values.Look(ref _highestGrowth, "highestGrowth"); } public override void Destroy(DestroyMode mode = DestroyMode.Vanish) { _innerContainer.ClearAndDestroyContents(); base.Destroy(mode); } public override void DeSpawn(DestroyMode mode = DestroyMode.Vanish) { _innerContainer.ClearAndDestroyContents(); foreach (Plant item in PlantsOnMe) item.Destroy(); base.DeSpawn(mode); } public new void SetPlantDefToGrow(ThingDef plantDef) { base.SetPlantDefToGrow(plantDef); if (_stage != HydroponicsStage.Sowing) return; _innerContainer.ClearAndDestroyContents(); foreach (Plant item in PlantsOnMe) item.Destroy(); } public override IEnumerable GetGizmos() { // Literally useless, but in case hydroponics get a developer gizmo, here we go. foreach (Gizmo gizmo in base.GetGizmos()) { yield return gizmo; } // Don't show gizmos unless in developer mode. if (!DebugSettings.ShowDevGizmos) { yield break; } // Developer trick to grow the 'first' plant internally, allowing growth tracking. if (_innerContainer.Count > 0) { var growTrackedAction = new Command_Action { defaultLabel = "DEV: Grow tracked plant", action = delegate { var trackedPlant = (Plant)_innerContainer[0]; trackedPlant.Growth = 1f; } }; yield return growTrackedAction; } var clearContainedPlants = new Command_Action { defaultLabel = "DEV: Clear contained plants", action = delegate { var i = 0; while (_innerContainer.Count > 0) { var plant = (Plant)_innerContainer[0]; plant.Destroy(); i++; } Messages.Message(new Message($"{i} plants destroyed.", MessageTypeDefOf.SilentInput, this)); } }; yield return clearContainedPlants; } } }