using GamePath;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UAI
{
///
/// Represents a Utility AI task for an entity to find and manage farm plots.
/// The entity will search for suitable farm plots based on whether it has seeds and the state of nearby plots (empty, needs maintenance, wilted).
/// It moves to the plot, applies a buff (simulating work), and then manages the plot (harvesting/planting) after the buff expires.
///
public class UAITaskFarmingV4 : UAITaskBase
{
// --- Constants ---
private const float DefaultTimeout = 100f; // Time in seconds before the task gives up if stuck.
private const float ApproachDistance = 1.1f; // How close the entity needs to be to the plot to start working.
private const float
SquaredApproachDistance = ApproachDistance * ApproachDistance; // Use squared distance for performance.
private const float RotationSpeed = 8f; // How fast the entity turns towards the plot.
private const int DefaultSearchRange = 50; // Default search radius for farm plots if not found nearby.
private const string DefaultSeedPattern = "planted*1"; // Default pattern to identify seeds in inventory.
// --- Configurable Parameters ---
private string _workBuff = string.Empty; // The buff applied while the entity is "working" on the plot.
private string
_seedIdentifier =
DefaultSeedPattern; // Pattern used to identify seeds in inventory (e.g., "planted*1", "seed*").
private int _searchRange = DefaultSearchRange; // Search radius for farm plots.
// --- Shared claim registry ---
// Tracks which farm plot positions are currently claimed by a farmer NPC.
// Static so all task instances share the same set; safe because UAI runs on Unity's main thread.
private static readonly HashSet _claimedPlots = new HashSet();
// --- Task State Variables ---
private Vector3 _targetFarmPlotPosition; // The position of the farm plot block being targeted.
private Vector3i _claimedPosition; // The position currently held in _claimedPlots by this instance.
private FarmPlotData _targetFarmPlotData; // Data associated with the target farm plot.
private bool _hasWorkBuffApplied = false; // Flag indicating if the work buff was applied in this cycle.
private float _currentTimeout; // Countdown timer for the task timeout.
private bool _hasSeedInInventory = false; // Cached check if the entity has seeds.
private string _seedPatternStartsWith = string.Empty; // Cache for seed pattern matching.
private string _seedPatternEndsWith = string.Empty; // Cache for seed pattern matching.
private string _cooldownBuff = "buffFarmerCoolDown";
// Resolved once in Start(): non-null when this NPC is hired by a player leader.
// When set, FindTargetFarmPlot restricts candidates to plots inside that player's land claim.
private PersistentPlayerData _leaderPlayerData;
///
/// Initializes parameters from the AI configuration.
/// Expected parameters: "buff" (required), "seed" (optional), "range" (optional).
///
public override void initializeParameters()
{
base.initializeParameters();
if (Parameters.TryGetValue("cooldownBuff", out var cooldownBuff) && !string.IsNullOrEmpty(cooldownBuff))
_cooldownBuff = cooldownBuff;
if (Parameters.TryGetValue("buff", out _workBuff)) { }
if (Parameters.TryGetValue("seed", out _seedIdentifier))
{
if (_seedIdentifier.Contains("*"))
{
var parts = _seedIdentifier.Split('*');
_seedPatternStartsWith = parts.Length > 0 ? parts[0] : string.Empty;
_seedPatternEndsWith = parts.Length > 1 ? parts[1] : string.Empty;
}
else
{
_seedPatternStartsWith = _seedIdentifier;
_seedPatternEndsWith = null; // Indicate exact match needed
}
}
if (Parameters.ContainsKey("range") && int.TryParse(Parameters["range"], out _searchRange))
{
}
}
///
/// Called when the task starts. Finds a suitable farm plot to target.
///
public override void Start(Context _context)
{
_hasWorkBuffApplied = false;
_currentTimeout = DefaultTimeout;
_targetFarmPlotData = null;
// Resolve the land-claim owner once per task invocation.
//
// Priority 1 — active hire: the NPC has a live Leader/Owner cvar pointing at a
// player who is currently in the world. Plots are restricted to that player's
// land claim.
//
// Priority 2 — FarmHere mode: the player issued "FarmHere", which zeroed the
// Leader/Owner cvars (removing the NPC from the party / stopping TP) but stored
// the player's entity ID in "FarmOwnerEntityId". The farming task honours that
// stored ID for land-claim checks so behaviour is identical to being hired.
//
// Priority 3 — un-hired NPC: no restriction, any plot is fair game.
_leaderPlayerData = null;
var leader = EntityUtilities.GetLeaderOrOwner(_context.Self.entityId);
if (leader != null)
{
_leaderPlayerData = GameManager.Instance?.GetPersistentPlayerList()
?.GetPlayerDataFromEntityID(leader.entityId);
}
else if (_context.Self.Buffs.HasCustomVar("FarmOwnerEntityId"))
{
var ownerId = (int)_context.Self.Buffs.GetCustomVar("FarmOwnerEntityId");
_leaderPlayerData = GameManager.Instance?.GetPersistentPlayerList()
?.GetPlayerDataFromEntityID(ownerId);
}
Vector3i currentPosition = new Vector3i(_context.Self.position);
_hasSeedInInventory = CheckHasSeed(_context);
// Attempt to find the best farm plot to work on.
_targetFarmPlotData = FindTargetFarmPlot(currentPosition, _hasSeedInInventory);
if (_targetFarmPlotData == null)
{
// Could not find any suitable plot. Maybe reset nearby plots if needed?
FarmPlotManager.Instance.ResetPlantsInRange(currentPosition); // Consider if this reset is always desired on failure.
_context.Self.PlayGiveUpSound();
_context.Self.Buffs.AddBuff(_cooldownBuff);
Stop(_context); // Stop the task as there's nothing to do.
return;
}
// Claim the plot immediately so other farmers skip it while this NPC is en route.
_claimedPosition = _targetFarmPlotData.GetBlockPos();
_claimedPlots.Add(_claimedPosition);
// Found a plot, set target position and pathfind.
_targetFarmPlotPosition = _targetFarmPlotData.GetBlockPos();
PathingUtils.FindPath(_context, _targetFarmPlotPosition,
false); // Assuming SCoreUtils handles path finding initiation.
_context.ActionData.Started = true;
_context.ActionData.Executing = true;
}
///
/// Called periodically while the task is active. Handles movement, applying the work buff,
/// and managing the farm plot after the work is done.
///
public override void Update(Context _context)
{
// Always look towards the target plot
_context.Self.SetLookPosition(_targetFarmPlotPosition);
_context.Self.RotateTo(_targetFarmPlotPosition.x + 0.5f, _targetFarmPlotPosition.y + 1f,
_targetFarmPlotPosition.z + 0.5f,
RotationSpeed, RotationSpeed);
// Check if the entity is facing the target direction (optional, but can prevent actions before turning)
if (!_context.Self.IsInFrontOfMe(_targetFarmPlotPosition))
{
return; // Wait until turned
}
if (_context.Self.moveHelper.BlockedTime > SCoreConstants.BlockedTime)
{
Log.Out(
$"UAITaskFarmingV4: Entity {_context.Self.entityId} is blocked while moving to farm plot.");
_currentTimeout = -1;
}
// --- Timeout Check ---
_currentTimeout -= Time.deltaTime;
if (_currentTimeout <= 0f)
{
// Task timed out (e.g., stuck). Clean up and stop.
Log.Out($"UAITaskFarmingV4 timed out for entity {_context.Self.entityId}");
_targetFarmPlotData.Visited = true;
BlockUtilitiesSDX.removeParticlesCenteredServer(new Vector3i(_targetFarmPlotPosition)); // Clean up particles if any
if (!string.IsNullOrEmpty(_workBuff))
_context.Self.Buffs.RemoveBuff(_workBuff); // Ensure buff is removed on timeout
Stop(_context);
return;
}
bool hasBuff = !string.IsNullOrEmpty(_workBuff) && _context.Self.Buffs.HasBuff(_workBuff);
// --- State Machine Logic ---
// State 1: Working (Buff Active)
if (hasBuff)
{
// Entity is currently "working". Wait for the buff to expire.
_hasWorkBuffApplied = true; // Ensure we know the buff was active
return;
}
// State 2: Finished Working (Buff Just Expired)
if (_hasWorkBuffApplied) // Buff was active previously, but isn't now.
{
// Work is complete. Handle harvesting/planting.
HandleHarvestingAndCleanup(_context);
Stop(_context); // Task complete for this plot.
return;
}
// State 3: Moving To Plot or Starting Work (Buff Not Active, Work Not Finished)
float sqrDist = (_targetFarmPlotPosition - _context.Self.position).sqrMagnitude;
if (sqrDist >= SquaredApproachDistance)
{
// Still too far, continue moving.
_context.Self.moveHelper.SetMoveTo(_targetFarmPlotPosition, false);
return;
}
// Arrived at the plot. Stop moving and apply the work buff.
_context.Self.moveHelper.Stop(); // Ensure entity stops moving
if (GameManager.IsDedicatedServer)
BlockUtilitiesSDX.addParticlesCenteredServer(string.Empty,
new Vector3i(_targetFarmPlotPosition)); // Add visual effect (DS: server→clients)
else
BlockUtilitiesSDX.addParticlesCentered(string.Empty,
new Vector3i(_targetFarmPlotPosition)); // Add visual effect
// Apply the work buff to start the "working" phase.
if (!string.IsNullOrEmpty(_workBuff))
{
_context.Self.Buffs.AddBuff(_workBuff);
_hasWorkBuffApplied = true; // Mark that we are now starting the work cycle
}
else
{
// If no buff is defined, we might skip straight to harvesting or just stop.
// Depending on desired behavior. For now, assume buff is required.
Log.Out("UAITaskFarmingV4: No work buff specified, cannot simulate work time.");
HandleHarvestingAndCleanup(_context); // Or maybe just Stop()?
Stop(_context);
}
}
///
/// Called when the task stops or is interrupted. Cleans up resources like particles.
///
public override void Stop(Context _context)
{
// Release the plot claim so other farmers can target it on their next cycle.
if (_claimedPosition != Vector3i.zero)
{
_claimedPlots.Remove(_claimedPosition);
_claimedPosition = Vector3i.zero;
}
// Always try to remove particles associated with this task's target location.
BlockUtilitiesSDX.removeParticlesCenteredServer(new Vector3i(_targetFarmPlotPosition));
_context.Self.setHomeArea(new Vector3i(_targetFarmPlotPosition), 10);
// Call the base Stop method for standard cleanup.
base.Stop(_context);
}
// --- Helper Methods ---
///
/// Checks if the entity has items matching the seed identifier in its inventory.
///
private bool CheckHasSeed(Context _context)
{
ItemStack[] items = null;
if (HarvestManager.Has(_context.Self.entityId))
items = HarvestManager.GetOrCreate(_context.Self.entityId).items;
else
items = _context.Self.bag?.items;
if (items == null) return false;
foreach (var itemStack in items)
{
if (itemStack.IsEmpty()) continue;
if (MatchesSeedPattern(itemStack.itemValue.ItemClass.GetItemName()) && itemStack.count > 0)
return true;
}
return false;
}
private bool MatchesSeedPattern(string itemName)
{
if (_seedPatternEndsWith == null)
return itemName.Equals(_seedPatternStartsWith, System.StringComparison.OrdinalIgnoreCase);
return itemName.StartsWith(_seedPatternStartsWith, System.StringComparison.OrdinalIgnoreCase) &&
itemName.EndsWith(_seedPatternEndsWith, System.StringComparison.OrdinalIgnoreCase);
}
///
/// Returns true if the candidate plot is already claimed by another farmer NPC.
///
private static bool IsClaimed(FarmPlotData plot)
{
return plot != null && _claimedPlots.Contains(plot.GetBlockPos());
}
///
/// Finds the most suitable farm plot based on entity position and whether it has seeds.
/// When the NPC is hired (has a player leader), only plots inside that player's land
/// claim are considered. Un-hired NPCs use any available plot as before.
///
/// FarmPlotData of the chosen plot, or null if none found.
private FarmPlotData FindTargetFarmPlot(Vector3i position, bool hasSeed)
{
FarmPlotData foundPlot = null;
// Priority 1: If holding seeds, look for nearby empty plots first.
if (hasSeed)
{
foundPlot = FarmPlotManager.Instance.GetFarmPlotsNearby(position, true);
if (IsClaimed(foundPlot) || !IsAllowed(foundPlot)) foundPlot = null;
if (foundPlot == null)
{
foreach (var candidate in FarmPlotManager.Instance.GetCloseFarmPlots(position))
{
if (!IsClaimed(candidate) && IsAllowed(candidate)) { foundPlot = candidate; break; }
}
}
}
// Priority 2: Look for plots needing maintenance (e.g., harvestable) nearby.
if (foundPlot == null)
{
foundPlot = FarmPlotManager.Instance.GetClosesUnmaintainedWithPlants(position);
if (IsClaimed(foundPlot) || !IsAllowed(foundPlot)) foundPlot = null;
}
if (foundPlot == null)
{
foundPlot = FarmPlotManager.Instance.GetClosesUnmaintained(position, _searchRange);
if (IsClaimed(foundPlot) || !IsAllowed(foundPlot)) foundPlot = null;
}
if (foundPlot == null)
{
foundPlot = FarmPlotManager.Instance.GetFarmPlotsNearbyWithPlants(position);
if (IsClaimed(foundPlot) || !IsAllowed(foundPlot)) foundPlot = null;
}
// Priority 3: Look for wilted plots (if applicable).
if (foundPlot == null)
{
foundPlot = FarmPlotManager.Instance.GetClosesFarmPlotsWilted(position);
if (IsClaimed(foundPlot) || !IsAllowed(foundPlot)) foundPlot = null;
}
return foundPlot;
}
///
/// Returns true if this NPC is allowed to work the given plot.
/// Un-hired NPCs ( is null) may work any plot.
/// Hired NPCs are restricted to plots inside their leader's land claim.
///
private bool IsAllowed(FarmPlotData plot)
{
if (plot == null) return false;
if (_leaderPlayerData == null) return true; // not hired — no restriction
return FarmLandClaimUtils.IsPlotInLandClaim(plot.GetBlockPos(), _leaderPlayerData);
}
///
/// Handles the logic for managing the farm plot after the work buff expires.
/// This typically involves harvesting items and potentially replanting.
///
private void HandleHarvestingAndCleanup(Context _context)
{
BlockUtilitiesSDX.removeParticlesCenteredServer(new Vector3i(_targetFarmPlotPosition));
if (_targetFarmPlotData == null)
{
Log.Out($"UAITaskFarmingV4: _targetFarmPlotData was null during HandleHarvesting for entity {_context.Self.entityId}.");
_hasWorkBuffApplied = false;
return;
}
var harvestedItemStacks = _targetFarmPlotData.Manage(_context.Self);
AdvLogging.DisplayLog("AdvancedTroubleshootingFeatures", "UAITaskFarmingV4",
$"Entity {_context.Self.entityId} harvested {harvestedItemStacks?.Count ?? 0} item type(s) from {_targetFarmPlotPosition}.");
// EntityAliveSDXV4 extends EntityTrader. TileEntityComposite.AddItem() always
// calls SetModified() internally, which for TileEntityTrader triggers a network
// packet that serialises both items[] and TraderData in one stream. Those two stores
// can diverge, causing EndOfStreamException on the client. Use HarvestManager — a
// plain TileEntityComposite per entity, never attached to the world tile-entity
// system — to store crops safely. The player opens it via the OpenInventory dialog cmd.
bool isTraderEntity = _context.Self is EntityTrader;
var lootContainer = isTraderEntity
? (SCoreLootContainer)null
: (_context.Self as EntityAliveSDX)?.lootContainer ?? (_context.Self as EntityAliveSDXV4)?.lootContainer;
if (harvestedItemStacks != null && harvestedItemStacks.Count > 0)
{
var random = GameManager.Instance.World.GetGameRandom();
bool addedItems = false;
foreach (var item in harvestedItemStacks)
{
// Skip items that fail the probability roll.
if (item.prob < 1f && random.RandomFloat > item.prob)
{
AdvLogging.DisplayLog("AdvancedTroubleshootingFeatures", "UAITaskFarmingV4",
$" Skipping '{item.name}' — prob roll failed (prob={item.prob:F2}).");
continue;
}
// Use a random count in [min, max]. Clamp min to 1 so a zero minCount
// never produces an empty ItemStack that AddItem silently rejects.
var count = random.RandomRange(
Utils.FastMax(1, item.minCount),
Utils.FastMax(1, item.maxCount) + 1);
var itemValue = ItemClass.GetItem(item.name, false);
if (itemValue == null || itemValue.Equals(ItemValue.None))
{
Log.Out($"UAITaskFarmingV4: Unknown item '{item.name}' in harvest drops for entity {_context.Self.entityId}. Skipping.");
continue;
}
var itemStack = new ItemStack(itemValue, count);
AdvLogging.DisplayLog("AdvancedTroubleshootingFeatures", "UAITaskFarmingV4",
$" Attempting to add {count}x '{item.name}' to entity {_context.Self.entityId} inventory.");
if (isTraderEntity)
{
// Store in HarvestManager — never touches the trader's lootContainer.
if (!HarvestManager.AddItem(_context.Self.entityId, itemStack))
{
AdvLogging.DisplayLog("AdvancedTroubleshootingFeatures", "UAITaskFarmingV4",
$" Harvest container full — dropping {count}x '{item.name}' on ground.");
_context.Self.world.GetGameManager().ItemDropServer(
itemStack, _context.Self.position, Vector3.zero);
}
else
{
AdvLogging.DisplayLog("AdvancedTroubleshootingFeatures", "UAITaskFarmingV4",
$" Added {count}x '{item.name}' to harvest container.");
addedItems = true;
}
}
else if (lootContainer != null && lootContainer.AddItem(itemStack))
{
AdvLogging.DisplayLog("AdvancedTroubleshootingFeatures", "UAITaskFarmingV4",
$" Added {count}x '{item.name}' to inventory.");
addedItems = true;
}
else
{
AdvLogging.DisplayLog("AdvancedTroubleshootingFeatures", "UAITaskFarmingV4",
$" Inventory full or null — dropping {count}x '{item.name}' on ground.");
_context.Self.world.GetGameManager().ItemDropServer(
itemStack, _context.Self.position, Vector3.zero);
}
}
if (addedItems)
{
_context.Self.PlayOneShot("item_plant_pickup", false);
if (isTraderEntity)
{
HarvestManager.Save();
}
else if (lootContainer != null)
{
lootContainer.items = StackSortUtil.CombineAndSortStacks(lootContainer.items, 0);
lootContainer.SetModified();
}
}
}
_targetFarmPlotData = null;
_hasWorkBuffApplied = false;
}
}
}