<commit_before>unit KM_UnitTaskMining;
{$I KaM_Remake.inc}
interface
uses Math, SysUtils,
  KM_CommonClasses, KM_Defaults, KM_Points,
  KM_Units, KM_Units_Workplan, KM_Terrain,
  KM_ResWares;


{Perform resource mining}
type
  TTaskMining = class(TUnitTask)
  private
    fBeastID: Byte;
    fWorkPlan: TUnitWorkPlan;
    function ResourceExists: Boolean;
    function ChooseToCutOrPlant: TPlantAct;
    procedure FindAnotherWorkPlan;
  public
    constructor Create(aUnit: TKMUnit; aRes: TWareType);
    destructor Destroy; override;
    function WalkShouldAbandon:boolean; override;
    constructor Load(LoadStream: TKMemoryStream); override;
    function GetActivityText: UnicodeString;
    property WorkPlan: TUnitWorkPlan read fWorkPlan;
    function Execute: TTaskResult; override;
    procedure Save(SaveStream: TKMemoryStream); override;
  end;


implementation
uses KM_Houses, KM_HandsCollection, KM_Resource, KM_ResMapElements, KM_ResTexts, KM_ResHouses;


{ TTaskMining }
constructor TTaskMining.Create(aUnit: TKMUnit; aRes: TWareType);
begin
  inherited Create(aUnit);
  fTaskName := utn_Mining;
  fWorkPlan := TUnitWorkPlan.Create;
  fWorkPlan.OnWorkplanAllowed := TKMUnitCitizen(aUnit).WorkPlanProductValid;
  fBeastID  := 0;

  fWorkPlan.FindPlan( fUnit,
                      fUnit.GetHome.HouseType,
                      aRes,
                      KMPointBelow(aUnit.GetHome.GetEntrance),
                      ChooseToCutOrPlant
                      );
end;


destructor TTaskMining.Destroy;
begin
  if (not fUnit.GetHome.IsDestroyed) and (fUnit.GetHome.GetState = hst_Work) then
    fUnit.GetHome.SetState(hst_Idle); //Make sure we don't abandon and leave our house with "working" animations
  FreeAndNil(fWorkPlan);
  inherited;
end;


//Note: Phase is -1 because it will have been increased at the end of last Execute
function TTaskMining.WalkShouldAbandon:boolean;
begin
  Result := false;
  Assert(fUnit is TKMUnitCitizen);
  if fPhase = 2 then //Unit is walking to mine-position
    Result := gTerrain.TileIsLocked(WorkPlan.Loc) or //If someone takes our place
              not ResourceExists or //Resource has gone
              not TKMUnitCitizen(fUnit).CanWorkAt(WorkPlan.Loc, WorkPlan.GatheringScript);
end;


//Chose if we don't care or prefer specific activity
//depending on orders or clogged output
function TTaskMining.ChooseToCutOrPlant: TPlantAct;
begin
  Result := taAny;

  case fUnit.GetHome.HouseType of
    ht_Woodcutters: case TKMHouseWoodcutters(fUnit.GetHome).WoodcutterMode of
                      wcm_Chop:         Result := taCut;
                      wcm_ChopAndPlant: if fUnit.GetHome.CheckResOut(wt_Trunk) >= MAX_WARES_IN_HOUSE then
                                          Result := taPlant
                                        else
                                          Result := taAny;
                    end;
    ht_Farm:        if fUnit.GetHome.CheckResOut(wt_Corn) >= MAX_WARES_IN_HOUSE then
                      Result := taPlant
                    else
                      Result := taAny;
    else Result := taAny; //We don't care since other housetypes don't have concurent activities
  end;
end;


constructor TTaskMining.Load(LoadStream: TKMemoryStream);
begin
  inherited;
  fWorkPlan := TUnitWorkPlan.Create;
  fWorkPlan.Load(LoadStream);
  LoadStream.Read(fBeastID);
end;


function TTaskMining.GetActivityText: UnicodeString;
begin
  case WorkPlan.GatheringScript of
    gs_StoneCutter:     Result := gResTexts[TX_UNIT_TASK_STONE];
    gs_FarmerSow:       Result := gResTexts[TX_UNIT_TASK_SOW_CORN];
    gs_FarmerCorn:      Result := gResTexts[TX_UNIT_TASK_CUTTING_CORN];
    gs_FarmerWine:      Result := gResTexts[TX_UNIT_TASK_GRAPES];
    gs_FisherCatch:     Result := gResTexts[TX_UNIT_TASK_FISHING];
    gs_WoodCutterCut:   Result := gResTexts[TX_UNIT_TASK_CUT_TREE];
    gs_WoodCutterPlant: Result := gResTexts[TX_UNIT_TASK_PLANT_TREE];
    else                Result := 'Unknown';
  end;
end;


//Try to find alternative target for our WorkPlan
//Happens when we discover that resource is gone or is occupied by another busy unit
//Return false if new plan could not be found
procedure TTaskMining.FindAnotherWorkPlan;
var OldLoc: TKMPoint; OldDir:TKMDirection;
begin
  OldLoc := WorkPlan.Loc;
  OldDir := WorkPlan.WorkDir;

  //Tell the work plan to find a new resource of the same gathering script
  if WorkPlan.FindDifferentResource(fUnit, KMPointBelow(fUnit.GetHome.GetEntrance), OldLoc) then
  begin
    //Must always give us a new location (or same location but different direction)
    Assert((OldDir <> WorkPlan.WorkDir) or not KMSamePoint(OldLoc, WorkPlan.Loc));
    fPhase := 0; //Set the walk again (Will become 1 after this loop)
    fUnit.SetActionLockedStay(0, WorkPlan.ActionWalkTo);
  end else
  begin
    fPhase := 99; //Abandon as there is no other work plan available (Exit the task on next update)
    fUnit.SetActionLockedStay(0, WorkPlan.ActionWalkTo);
  end;
end;

<commit_msg>Improved TileIsLocked check for woodcutters to fix "two woodcutters chopping the same tree" bug which happens under very rare circumstances (shown in this video http://www.youtube.com/watch?v=CIKloQ2Xv90)<commit_after>unit KM_UnitTaskMining;
{$I KaM_Remake.inc}
interface
uses Math, SysUtils,
  KM_CommonClasses, KM_Defaults, KM_Points,
  KM_Units, KM_Units_Workplan, KM_Terrain,
  KM_ResWares;


{Perform resource mining}
type
  TTaskMining = class(TUnitTask)
  private
    fBeastID: Byte;
    fWorkPlan: TUnitWorkPlan;
    function ResourceExists: Boolean;
    function ResourceTileIsLocked: Boolean;
    function ChooseToCutOrPlant: TPlantAct;
    procedure FindAnotherWorkPlan;
  public
    constructor Create(aUnit: TKMUnit; aRes: TWareType);
    destructor Destroy; override;
    function WalkShouldAbandon:boolean; override;
    constructor Load(LoadStream: TKMemoryStream); override;
    function GetActivityText: UnicodeString;
    property WorkPlan: TUnitWorkPlan read fWorkPlan;
    function Execute: TTaskResult; override;
    procedure Save(SaveStream: TKMemoryStream); override;
  end;


implementation
uses KM_Houses, KM_HandsCollection, KM_Resource, KM_ResMapElements, KM_ResTexts, KM_ResHouses;


{ TTaskMining }
constructor TTaskMining.Create(aUnit: TKMUnit; aRes: TWareType);
begin
  inherited Create(aUnit);
  fTaskName := utn_Mining;
  fWorkPlan := TUnitWorkPlan.Create;
  fWorkPlan.OnWorkplanAllowed := TKMUnitCitizen(aUnit).WorkPlanProductValid;
  fBeastID  := 0;

  fWorkPlan.FindPlan( fUnit,
                      fUnit.GetHome.HouseType,
                      aRes,
                      KMPointBelow(aUnit.GetHome.GetEntrance),
                      ChooseToCutOrPlant
                      );
end;


destructor TTaskMining.Destroy;
begin
  if (not fUnit.GetHome.IsDestroyed) and (fUnit.GetHome.GetState = hst_Work) then
    fUnit.GetHome.SetState(hst_Idle); //Make sure we don't abandon and leave our house with "working" animations
  FreeAndNil(fWorkPlan);
  inherited;
end;


//Note: Phase is -1 because it will have been increased at the end of last Execute
function TTaskMining.WalkShouldAbandon:boolean;
begin
  Result := false;
  Assert(fUnit is TKMUnitCitizen);
  if fPhase = 2 then //Unit is walking to mine-position
    Result := ResourceTileIsLocked or //If someone takes our place
              not ResourceExists or //Resource has gone
              not TKMUnitCitizen(fUnit).CanWorkAt(WorkPlan.Loc, WorkPlan.GatheringScript);
end;


//Chose if we don't care or prefer specific activity
//depending on orders or clogged output
function TTaskMining.ChooseToCutOrPlant: TPlantAct;
begin
  Result := taAny;

  case fUnit.GetHome.HouseType of
    ht_Woodcutters: case TKMHouseWoodcutters(fUnit.GetHome).WoodcutterMode of
                      wcm_Chop:         Result := taCut;
                      wcm_ChopAndPlant: if fUnit.GetHome.CheckResOut(wt_Trunk) >= MAX_WARES_IN_HOUSE then
                                          Result := taPlant
                                        else
                                          Result := taAny;
                    end;
    ht_Farm:        if fUnit.GetHome.CheckResOut(wt_Corn) >= MAX_WARES_IN_HOUSE then
                      Result := taPlant
                    else
                      Result := taAny;
    else Result := taAny; //We don't care since other housetypes don't have concurent activities
  end;
end;


constructor TTaskMining.Load(LoadStream: TKMemoryStream);
begin
  inherited;
  fWorkPlan := TUnitWorkPlan.Create;
  fWorkPlan.Load(LoadStream);
  LoadStream.Read(fBeastID);
end;


function TTaskMining.GetActivityText: UnicodeString;
begin
  case WorkPlan.GatheringScript of
    gs_StoneCutter:     Result := gResTexts[TX_UNIT_TASK_STONE];
    gs_FarmerSow:       Result := gResTexts[TX_UNIT_TASK_SOW_CORN];
    gs_FarmerCorn:      Result := gResTexts[TX_UNIT_TASK_CUTTING_CORN];
    gs_FarmerWine:      Result := gResTexts[TX_UNIT_TASK_GRAPES];
    gs_FisherCatch:     Result := gResTexts[TX_UNIT_TASK_FISHING];
    gs_WoodCutterCut:   Result := gResTexts[TX_UNIT_TASK_CUT_TREE];
    gs_WoodCutterPlant: Result := gResTexts[TX_UNIT_TASK_PLANT_TREE];
    else                Result := 'Unknown';
  end;
end;


//Try to find alternative target for our WorkPlan
//Happens when we discover that resource is gone or is occupied by another busy unit
//Return false if new plan could not be found
procedure TTaskMining.FindAnotherWorkPlan;
var OldLoc: TKMPoint; OldDir:TKMDirection;
begin
  OldLoc := WorkPlan.Loc;
  OldDir := WorkPlan.WorkDir;

  //Tell the work plan to find a new resource of the same gathering script
  if WorkPlan.FindDifferentResource(fUnit, KMPointBelow(fUnit.GetHome.GetEntrance), OldLoc) then
  begin
    //Must always give us a new location (or same location but different direction)
    Assert((OldDir <> WorkPlan.WorkDir) or not KMSamePoint(OldLoc, WorkPlan.Loc));
    fPhase := 0; //Set the walk again (Will become 1 after this loop)
    fUnit.SetActionLockedStay(0, WorkPlan.ActionWalkTo);
  end else
  begin
    fPhase := 99; //Abandon as there is no other work plan available (Exit the task on next update)
    fUnit.SetActionLockedStay(0, WorkPlan.ActionWalkTo);
  end;
end;


function TTaskMining.ResourceTileIsLocked: Boolean;
var P: TKMPoint;
begin
  if WorkPlan.GatheringScript = gs_WoodCutterCut then
  begin
    P := KMGetVertexTile(WorkPlan.Loc, WorkPlan.WorkDir);
    //Check all tiles around the tree, like we do in TKMTerrain.FindTree
    Result := gTerrain.TileIsLocked(P)
      or ((P.X > 1) and gTerrain.TileIsLocked(KMPoint(P.X-1, P.Y))) //if K=1, K-1 will be off map
      or ((P.Y > 1) and gTerrain.TileIsLocked(KMPoint(P.X, P.Y-1)))
      or ((P.X > 1) and (P.Y > 1) and gTerrain.TileIsLocked(KMPoint(P.X-1, P.Y-1)))
  end
  else
    Result := gTerrain.TileIsLocked(WorkPlan.Loc);
end;


{'fraction_non_alphanumeric': 0.07839335180055401, 'fraction_numerical': 0.0028624192059095105, 'mean_word_length': 3.2275565964090553, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4800632', 'n_tokens_mistral': 3630, 'n_tokens_neox': 3443, 'n_words': 900}