using System; using System.Collections.Generic; using BepInEx.Configuration; using HarmonyLib; using UnityEngine; using UXAssist.Common; using UXAssist.Common.ModFeatures; using UXAssist.Common.GameConstants; using GameLogicProc = UXAssist.Common.GameLogic; namespace UXAssist.Patches.Logistics; [ModFeature("Logistics", Order = 11)] public static class LogisticsPatch { public static ConfigEntry AutoConfigLogisticsEnabled; public static ConfigEntry AutoConfigLimitAutoReplenishCount; // Dispenser config public static ConfigEntry AutoConfigDispenserChargePower; // 3~30, display as 300000.0 * value public static ConfigEntry AutoConfigDispenserCourierCount; // 0~10 // Battlefield Analysis Base public static ConfigEntry AutoConfigBattleBaseChargePower; // 4~40, display as 300000.0 * value // PLS config public static ConfigEntry AutoConfigPLSChargePower; // 2~20, display as 3000000.0 * value public static ConfigEntry AutoConfigPLSMaxTripDrone; // 1~180, by degress public static ConfigEntry AutoConfigPLSDroneMinDeliver; // 0~10; 0 = 1%, 1~10 = 10% *value public static ConfigEntry AutoConfigPLSMinPilerValue; // 0~4; 0 = Maximum in tech, 1~4 = piler stacking count public static ConfigEntry AutoConfigPLSDroneCount; // 0~50 // ILS config public static ConfigEntry SetDefaultRemoteLogicToStorage; public static ConfigEntry AutoConfigILSChargePower; // 2~20, display as 15000000.0 * value public static ConfigEntry AutoConfigILSMaxTripDrone; // 1~180, by degress public static ConfigEntry AutoConfigILSMaxTripShip; // 1~41; 1~20 = value LY, 21-40 = 2*value-20LY, 41 = Unlimited public static ConfigEntry AutoConfigILSWarperDistance; // 2~21; 2~7 = value * 0.5 - 0.5AU, 8~16 = value - 4AU, 17~20 = value * 2 - 20AU, 21 = 60AU public static ConfigEntry AutoConfigILSDroneMinDeliver; // 0~10; 0 = 1%, 1~10 = 10% *value public static ConfigEntry AutoConfigILSShipMinDeliver; // 0~10; 0 = 1%, 1~10 = 10% *value public static ConfigEntry AutoConfigILSMinPilerValue; // 0~4; 0 = Maximum in tech, 1~4 = piler stacking count public static ConfigEntry AutoConfigILSIncludeOrbitCollector; public static ConfigEntry AutoConfigILSWarperNecessary; public static ConfigEntry AutoConfigILSDroneCount; // 0~100 public static ConfigEntry AutoConfigILSShipCount; // 0~10 // Vein collector config public static ConfigEntry AutoConfigVeinCollectorHarvestSpeed; // 0-20, 100% + 10% * value public static ConfigEntry AutoConfigVeinCollectorMinPilerValue; // 0~4; 0 = Maximum in tech, 1~4 = piler stacking count public static ConfigEntry OrbitalCollectorProductLimit; public static ConfigEntry LogisticsCapacityTweaksEnabled; public static ConfigEntry AllowOverflowInLogisticsEnabled; public static ConfigEntry GreaterPowerUsageInLogisticsEnabled; public static ConfigEntry LogisticsConstrolPanelImprovementEnabled; public static ConfigEntry RealtimeLogisticsInfoPanelEnabled; public static ConfigEntry RealtimeLogisticsInfoPanelBarsEnabled; public static void Init() { AutoConfigLogisticsEnabled.SettingChanged += (_, _) => AutoConfigLogistics.Enable(AutoConfigLogisticsEnabled.Value); AutoConfigLimitAutoReplenishCount.SettingChanged += (_, _) => AutoConfigLogistics.ToggleLimitAutoReplenishCount(); SetDefaultRemoteLogicToStorage.SettingChanged += (_, _) => AutoConfigLogisticsSetDefaultRemoteLogicToStorage.Enable(SetDefaultRemoteLogicToStorage.Value); LogisticsCapacityTweaksEnabled.SettingChanged += (_, _) => LogisticsCapacityTweaks.Enable(LogisticsCapacityTweaksEnabled.Value); AllowOverflowInLogisticsEnabled.SettingChanged += (_, _) => AllowOverflowInLogistics.Enable(AllowOverflowInLogisticsEnabled.Value); GreaterPowerUsageInLogisticsEnabled.SettingChanged += (_, _) => GreaterPowerUsageInLogistics.Enable(GreaterPowerUsageInLogisticsEnabled.Value); LogisticsConstrolPanelImprovementEnabled.SettingChanged += (_, _) => LogisticsConstrolPanelImprovement.Enable(LogisticsConstrolPanelImprovementEnabled.Value); RealtimeLogisticsInfoPanelEnabled.SettingChanged += (_, _) => RealtimeLogisticsInfoPanel.Enable(RealtimeLogisticsInfoPanelEnabled.Value); RealtimeLogisticsInfoPanelBarsEnabled.SettingChanged += (_, _) => RealtimeLogisticsInfoPanel.EnableBars(RealtimeLogisticsInfoPanelBarsEnabled.Value); } public static void Start() { AutoConfigLogistics.Enable(AutoConfigLogisticsEnabled.Value); AutoConfigLogisticsSetDefaultRemoteLogicToStorage.Enable(SetDefaultRemoteLogicToStorage.Value); LogisticsCapacityTweaks.Enable(LogisticsCapacityTweaksEnabled.Value); AllowOverflowInLogistics.Enable(AllowOverflowInLogisticsEnabled.Value); GreaterPowerUsageInLogistics.Enable(GreaterPowerUsageInLogisticsEnabled.Value); LogisticsConstrolPanelImprovement.Enable(LogisticsConstrolPanelImprovementEnabled.Value); RealtimeLogisticsInfoPanel.Enable(RealtimeLogisticsInfoPanelEnabled.Value); RealtimeLogisticsInfoPanel.EnableBars(RealtimeLogisticsInfoPanelBarsEnabled.Value); GameLogicProc.OnGameBegin += RealtimeLogisticsInfoPanel.OnGameBegin; GameLogicProc.OnGameEnd += RealtimeLogisticsInfoPanel.OnGameEnd; GameLogicProc.OnDataLoaded += RealtimeLogisticsInfoPanel.OnDataLoaded; } public static void Uninit() { GameLogicProc.OnDataLoaded -= RealtimeLogisticsInfoPanel.OnDataLoaded; GameLogicProc.OnGameEnd -= RealtimeLogisticsInfoPanel.OnGameEnd; GameLogicProc.OnGameBegin -= RealtimeLogisticsInfoPanel.OnGameBegin; AutoConfigLogistics.Enable(false); AutoConfigLogisticsSetDefaultRemoteLogicToStorage.Enable(false); LogisticsCapacityTweaks.Enable(false); AllowOverflowInLogistics.Enable(false); GreaterPowerUsageInLogistics.Enable(false); LogisticsConstrolPanelImprovement.Enable(false); RealtimeLogisticsInfoPanel.Enable(false); } public static void OnUpdate() => RealtimeInfoPanelPatch.OnUpdate(); public static void OnInputUpdate() { if (DSPGame.IsMenuDemo) return; if (VFInput.onGUI && LogisticsCapacityTweaksEnabled.Value) { LogisticsCapacityTweaks.UpdateInput(); } } #region Apply auto-config values to existing facilities on the current planet private enum StationKind { Pls, Ils, VeinCollector } // === Per-field setters (single source of truth, shared by auto-config-on-build and apply-to-planet) === internal static void StationSetChargePower(PlanetFactory factory, StationComponent station) => factory.powerSystem.consumerPool[station.pcId].workEnergyPerTick = (long)((station.isStellar ? 250000.0 * AutoConfigILSChargePower.Value : 50000.0 * AutoConfigPLSChargePower.Value) + 0.5); internal static void StationSetTripRangeDrones(PlanetFactory factory, StationComponent station) => station.tripRangeDrones = Math.Cos((station.isStellar ? AutoConfigILSMaxTripDrone.Value : AutoConfigPLSMaxTripDrone.Value) / 180.0 * Math.PI); internal static void StationSetDeliveryDrones(PlanetFactory factory, StationComponent station) { var v = station.isStellar ? AutoConfigILSDroneMinDeliver.Value : AutoConfigPLSDroneMinDeliver.Value; station.deliveryDrones = v == 0 ? 1 : v * 10; } internal static void StationSetPilerCount(PlanetFactory factory, StationComponent station) => station.pilerCount = station.isStellar ? AutoConfigILSMinPilerValue.Value : AutoConfigPLSMinPilerValue.Value; internal static void StationFillDrones(PlanetFactory factory, StationComponent station) { var target = station.isStellar ? AutoConfigILSDroneCount.Value : AutoConfigPLSDroneCount.Value; var toFill = Math.Max(0, target - station.idleDroneCount - station.workDroneCount); if (toFill > 0) station.idleDroneCount += GameMain.data.mainPlayer.package.TakeItem((int)KnownItemId.Drone, toFill, out _); } internal static void StationSetTripRangeShips(PlanetFactory factory, StationComponent station) => station.tripRangeShips = AutoConfigILSMaxTripShip.Value switch { <= 20 => AutoConfigILSMaxTripShip.Value, <= 40 => AutoConfigILSMaxTripShip.Value * 2 - 20, _ => 10000, } * 2400000.0; internal static void StationSetWarpDistance(PlanetFactory factory, StationComponent station) => station.warpEnableDist = AutoConfigILSWarperDistance.Value switch { <= 7 => AutoConfigILSWarperDistance.Value * 0.5 - 0.5, <= 16 => AutoConfigILSWarperDistance.Value - 4.0, <= 20 => AutoConfigILSWarperDistance.Value * 2 - 20.0, _ => 60.0, } * 40000.0; internal static void StationSetDeliveryShips(PlanetFactory factory, StationComponent station) { var v = AutoConfigILSShipMinDeliver.Value; station.deliveryShips = v == 0 ? 1 : v * 10; } internal static void StationFillShips(PlanetFactory factory, StationComponent station) { var toFill = Math.Max(0, AutoConfigILSShipCount.Value - station.idleShipCount - station.workShipCount); if (toFill > 0) station.idleShipCount += GameMain.data.mainPlayer.package.TakeItem((int)KnownItemId.Ship, toFill, out _); } internal static void StationSetIncludeOrbitCollector(PlanetFactory factory, StationComponent station) => station.includeOrbitCollector = AutoConfigILSIncludeOrbitCollector.Value; internal static void StationSetWarperNecessary(PlanetFactory factory, StationComponent station) => station.warperNecessary = AutoConfigILSWarperNecessary.Value; /* station.minerId may not be set yet on freshly built collectors, so resolve the minerId from the EntityData. */ internal static bool VeinCollectorSetHarvestSpeed(PlanetFactory factory, StationComponent station) { ref var entity = ref factory.entityPool[station.entityId]; if (entity.id != station.entityId || entity.minerId <= 0 || entity.minerId >= factory.factorySystem.minerCursor) return false; factory.factorySystem.minerPool[entity.minerId].speed = LogisticsConstants.MinMiningSpeedBase + AutoConfigVeinCollectorHarvestSpeed.Value * LogisticsConstants.MiningSpeedFineStep; return true; } internal static void VeinCollectorSetPilerCount(PlanetFactory factory, StationComponent station) => station.pilerCount = AutoConfigVeinCollectorMinPilerValue.Value; internal static void DispenserSetChargePower(PlanetFactory factory, DispenserComponent dispenser) => factory.powerSystem.consumerPool[dispenser.pcId].workEnergyPerTick = (long)(LogisticsConstants.DispenserChargePowerMultiplier * AutoConfigDispenserChargePower.Value + 0.5); internal static void DispenserFillCouriers(PlanetFactory factory, DispenserComponent dispenser) { var toFill = Math.Max(0, AutoConfigDispenserCourierCount.Value - dispenser.idleCourierCount - dispenser.workCourierCount); if (toFill > 0) dispenser.idleCourierCount += GameMain.data.mainPlayer.package.TakeItem((int)KnownItemId.Bot, toFill, out _); } internal static void BattleBaseSetChargePower(PlanetFactory factory, BattleBaseComponent battleBase) => factory.powerSystem.consumerPool[battleBase.pcId].workEnergyPerTick = (long)(LogisticsConstants.DispenserChargePowerMultiplier * AutoConfigBattleBaseChargePower.Value + 0.5); // === Per-facility "apply all settings" (also used as auto-config-on-build entry point) === internal static void DoConfigStation(PlanetFactory factory, StationComponent station) { if (station.isCollector) return; if (station.isVeinCollector) { if (VeinCollectorSetHarvestSpeed(factory, station)) VeinCollectorSetPilerCount(factory, station); return; } if (!station.isStellar) { StationSetChargePower(factory, station); StationSetTripRangeDrones(factory, station); StationSetDeliveryDrones(factory, station); StationSetPilerCount(factory, station); StationFillDrones(factory, station); return; } StationSetChargePower(factory, station); StationSetTripRangeDrones(factory, station); StationSetTripRangeShips(factory, station); StationSetWarpDistance(factory, station); StationSetDeliveryDrones(factory, station); StationSetDeliveryShips(factory, station); StationSetPilerCount(factory, station); StationSetIncludeOrbitCollector(factory, station); StationSetWarperNecessary(factory, station); StationFillDrones(factory, station); StationFillShips(factory, station); } // === Iterate over the current planet's facilities of a given kind === private static void ForEachStation(StationKind kind, Action action) { var factory = GameMain.localPlanet?.factory; var transport = factory?.transport; var stationPool = transport?.stationPool; if (stationPool == null) return; for (var i = transport.stationCursor - 1; i > 0; i--) { var station = stationPool[i]; if (station == null || station.id != i || station.isCollector) continue; var skip = kind switch { StationKind.VeinCollector => !station.isVeinCollector, StationKind.Ils => station.isVeinCollector || !station.isStellar, StationKind.Pls => station.isVeinCollector || station.isStellar, _ => true }; if (skip) continue; action(factory, station); } RefreshOpenLogisticsWindows(); } private static void ForEachDispenser(Action action) { var factory = GameMain.localPlanet?.factory; var transport = factory?.transport; var dispenserPool = transport?.dispenserPool; if (dispenserPool == null) return; for (var i = transport.dispenserCursor - 1; i > 0; i--) { var dispenser = dispenserPool[i]; if (dispenser == null || dispenser.id != i) continue; action(factory, dispenser); } RefreshOpenLogisticsWindows(); } private static void ForEachBattleBase(Action action) { var factory = GameMain.localPlanet?.factory; var battleBases = factory?.defenseSystem?.battleBases; if (battleBases?.buffer == null) return; for (var i = battleBases.cursor - 1; i > 0; i--) { var battleBase = battleBases.buffer[i]; if (battleBase == null || battleBase.id != i) continue; action(factory, battleBase); } RefreshOpenLogisticsWindows(); } // Re-populate any open logistic facility detail window so applied values show immediately. internal static void RefreshOpenLogisticsWindows() { var uiRoot = UIRoot.instance; if (!uiRoot) return; var uiGame = uiRoot.uiGame; if (!uiGame) return; var stationWindow = uiGame.stationWindow; if (stationWindow && stationWindow.active) stationWindow.OnStationIdChange(); var dispenserWindow = uiGame.dispenserWindow; if (dispenserWindow && dispenserWindow.active) dispenserWindow.OnDispenserIdChange(); var battleBaseWindow = uiGame.battleBaseWindow; if (battleBaseWindow && battleBaseWindow.active) battleBaseWindow.OnBattleBaseIdChange(); } // === Public entry points invoked by the config panel buttons === // Dispenser public static void ApplyDispenserChargePower() => ForEachDispenser(DispenserSetChargePower); public static void ApplyDispenserCourierCount() => ForEachDispenser(DispenserFillCouriers); public static void ApplyAllDispenser() => ForEachDispenser((f, d) => { DispenserSetChargePower(f, d); DispenserFillCouriers(f, d); }); // Battlefield Analysis Base public static void ApplyBattleBaseChargePower() => ForEachBattleBase(BattleBaseSetChargePower); public static void ApplyAllBattleBase() => ForEachBattleBase(BattleBaseSetChargePower); // PLS public static void ApplyPLSChargePower() => ForEachStation(StationKind.Pls, StationSetChargePower); public static void ApplyPLSTripRangeDrones() => ForEachStation(StationKind.Pls, StationSetTripRangeDrones); public static void ApplyPLSDroneMinDeliver() => ForEachStation(StationKind.Pls, StationSetDeliveryDrones); public static void ApplyPLSMinPilerValue() => ForEachStation(StationKind.Pls, StationSetPilerCount); public static void ApplyPLSDroneCount() => ForEachStation(StationKind.Pls, StationFillDrones); public static void ApplyAllPLS() => ForEachStation(StationKind.Pls, DoConfigStation); // ILS public static void ApplyILSChargePower() => ForEachStation(StationKind.Ils, StationSetChargePower); public static void ApplyILSTripRangeDrones() => ForEachStation(StationKind.Ils, StationSetTripRangeDrones); public static void ApplyILSTripRangeShips() => ForEachStation(StationKind.Ils, StationSetTripRangeShips); public static void ApplyILSWarpDistance() => ForEachStation(StationKind.Ils, StationSetWarpDistance); public static void ApplyILSDroneMinDeliver() => ForEachStation(StationKind.Ils, StationSetDeliveryDrones); public static void ApplyILSShipMinDeliver() => ForEachStation(StationKind.Ils, StationSetDeliveryShips); public static void ApplyILSMinPilerValue() => ForEachStation(StationKind.Ils, StationSetPilerCount); public static void ApplyILSDroneCount() => ForEachStation(StationKind.Ils, StationFillDrones); public static void ApplyILSShipCount() => ForEachStation(StationKind.Ils, StationFillShips); public static void ApplyILSIncludeOrbitCollector() => ForEachStation(StationKind.Ils, StationSetIncludeOrbitCollector); public static void ApplyILSWarperNecessary() => ForEachStation(StationKind.Ils, StationSetWarperNecessary); public static void ApplyAllILS() => ForEachStation(StationKind.Ils, DoConfigStation); // Vein Collector (Advanced Mining Machine) public static void ApplyVeinCollectorHarvestSpeed() => ForEachStation(StationKind.VeinCollector, (f, s) => VeinCollectorSetHarvestSpeed(f, s)); public static void ApplyVeinCollectorMinPilerValue() => ForEachStation(StationKind.VeinCollector, VeinCollectorSetPilerCount); public static void ApplyAllVeinCollector() => ForEachStation(StationKind.VeinCollector, DoConfigStation); public static void ApplyOrbitalCollectorProductLimitToUniverse() { var data = GameMain.data; var factories = data?.factories; if (factories == null) return; var limit = OrbitalCollectorProductLimit.Value; for (var factoryIndex = data.factoryCount - 1; factoryIndex >= 0; factoryIndex--) { var transport = factories[factoryIndex]?.transport; var stationPool = transport?.stationPool; if (stationPool == null) continue; for (var stationIndex = transport.stationCursor - 1; stationIndex > 0; stationIndex--) { var station = stationPool[stationIndex]; if (station == null || station.id != stationIndex || !station.isCollector) continue; var productCount = Math.Min(station.storage.Length, station.collectionIds?.Length ?? 0); for (var productIndex = 0; productIndex < productCount; productIndex++) station.storage[productIndex].max = limit; } } RefreshOpenLogisticsWindows(); } #endregion }