{$UNDEF SCRIPT_ID}{$DEFINE SCRIPT_ID := '8dc108d0-7e16-472e-839e-dfd7f64e9f08'} {$DEFINE SCRIPT_REVISION := '7'} {$IFDEF WINDOWS}{$DEFINE SCRIPT_GUI}{$ENDIF} {$I SRL-B/osr.simba} {$I BashLib/osr.simba} {$I BashLib/optional/handlers/discord.simba} {$I BashLib/optional/handlers/aichat.simba} { ---------------------------------------------------------------------------------- ~~~ Need Support? Contact Me ~~~ Discord Server: https://discord.gg/qsmKs5uKfR Discord ID: big.aussie Email: thebigaussie@proton.me Github: https://github.com/BigAussie ---------------------------------------------------------------------------------- } var WEBHOOKURL: String = ''; ENABLEWEBHOOKS: Boolean = False; SENDHOURLYREPORTMSG: Boolean = True; SENDSESSIONSUMMARYMSG: Boolean = True; ENABLEWORLDHOPPING: Boolean = False; WORLDHOPINTERVAL: Integer = 45; OVERRIDEBREAKS: Boolean = False; CUSTOMBREAKINTERVAL: Integer = 60; CUSTOMBREAKDURATION: Integer = 5; PREFERPOPULATEDTREES: Boolean = True; PLAYER_DETECT_RADIUS: Int32 = 25; POWERCUT: Boolean = False; type EIronwoodState = ( CHOP_TREE, WAIT_CHOPPING, USE_SPEC, DROP_LOGS, BANK_LOGS, END_SCRIPT ); TIronwoodChopper = record(TBaseScript) State: EIronwoodState; IronwoodTrees: array[0..6] of TRSObjectV2; BankChest: TRSObjectV2; CurrentTreeIndex: Int32; IsChopping: Boolean; LogItem: TRSItem; LogPrice: Int32; LogsBanked: Int32; TotalLogsProfit: Int64; PreviousProfit: Int64; BankItemList: TRSItemArray; PrevXP: Int32; LastXPTime: UInt64; TreeTimer: TCountDown; LastReportTime: UInt64; LastImageClear: UInt64; StartXP: Int32; LastHourlyReportTime: UInt64; LastLevel: Int32; CurrentLevel: Int32; // World Hopping WorldHopTimer: TCountDown; NextWorldHopTime: UInt64; WorldHopsCompleted: Int32; LastWorldHopTime: Int64; end; var IronwoodBot: TIronwoodChopper; Discord: TDiscordClient; const // Sneaky weight bias here because const roughly 35% chance to do the 1st array tree, the closest to the bank (17% for the others) TREE_WEIGHT_BIAS: array[0..6] of Int32 = [5, 10, 1, 1, 1, 5, 5]; EYE_Y_FIXED: array [0..100] of Single = [ -29, -28, -28, -29, -28, -28, -28, -28, -28, -28, -28, -27, -28, -26, -26, -26, -26, -26, -26, -26, -26, -26, -26, -26, -26.5, -27, -27, -27, -27, -27, -27, -28, -27, -27, -27, -27, -27, -26, -26, -26, -25, -26, -26, -25, -25, -26, -25, -25, -25, -25.5, -26, -27, -27, -27, -25, -27, -26, -26, -25, -25, -26, -26, -26, -26, -25, -25, -25, -26, -25, -26, -26, -25, -25, -26, -25, -24, -26, -25, -24, -24, -26, -24, -24, -25, -26, -24, -25, -24, -25, -25, -25, -25, -25, -25, -26, -25, -25, -26, -25, -25, -25 ]; EYE_Z_FIXED: array [0..100] of Single = [ 58, 58, 58, 58, 58, 58, 59, 58, 59, 58, 58, 58, 58, 57, 57, 57, 57, 57, 57, 57, 57, 58, 57, 57, 57.5, 58, 58, 58, 58, 59, 58, 58, 59, 58, 58, 59, 58, 57, 57, 57, 57, 56, 58, 57, 57, 57, 57, 57, 58, 58, 58, 57, 59, 58, 59, 58, 58, 59, 59, 59, 59, 59, 59, 57, 58, 58, 57, 56, 57, 56, 56, 58, 59, 57, 58, 59, 58, 60, 59, 60, 59, 59, 59, 60, 58, 59, 61, 58, 57, 58, 58, 57, 58, 56, 56, 58, 56, 56, 58, 59, 60 ]; EYE_Y_RESIZABLE: array [0..100] of Single = [ -35, -35, -35, -35, -35, -36, -35, -35, -35, -35, -34, -34, -34, -33, -33, -33, -34, -33, -33, -33, -33, -33, -33, -33, -33.5, -34, -35, -34, -34, -34, -34, -34, -34, -35, -34, -34, -35, -33, -32, -33, -33, -32, -33, -33, -32, -33, -33, -33, -33, -33.5, -34, -30, -34, -34, -33, -34, -34, -34, -34, -34, -32, -34, -33, -32, -32, -32, -32, -33, -32, -32, -34, -32, -32, -34, -34.5, -35, -34, -34, -35, -30, -34, -35, -36, -34, -34, -35, -34, -34, -35, -35, -34, -35, -36, -35, -33, -35, -36, -32, -35, -34.5, -34 ]; EYE_Z_RESIZABLE: array [0..100] of Single = [ 73, 71, 72, 73, 72, 72, 73, 72, 72, 73, 72, 71, 72, 71, 70, 71, 71, 70, 70, 71, 70, 70, 70, 70, 71, 72, 72, 72, 72, 71, 72, 71, 71, 71, 71, 73, 71, 70, 72, 71, 70, 71, 71, 70, 71, 70, 70, 69, 70, 70.5, 71, 74, 72, 71, 70, 72, 71, 71, 72, 72, 72, 72, 71, 71, 71, 70, 70, 70, 70, 69, 70, 70, 71, 70, 70, 70, 71, 71, 70, 73, 71, 69, 69, 71, 70, 69, 71, 68, 68, 68, 68, 69, 67, 68, 70, 68, 67, 70, 69, 71, 73 ]; // MM2MS Fix backported by Bootie from Tor shit function TMM2MSProjector.Run(Coords: Vector3Array; Rotation: Vector3): TPointArray; override; var I: Int32; begin if not Self.Calculated then begin Self.Zoom := TMM2MSProjector.TransformZoom(Self.ZoomLevel); Self.ScaleMin := Self.SCALE_MIN * MainScreen.Height / 503; // fixed client height Self.ScaleMax := Self.SCALE_MAX * MainScreen.Height / 503; // fixed client height if RSClient.Mode = ERSClientMode.FIXED then Self.ViewMatrix := TMatrix4.LookAtRH([0, EYE_Y_FIXED[Self.ZoomLevel], EYE_Z_FIXED[Self.ZoomLevel]], [0, 0, 1.0], Vector3_UnitY) else Self.ViewMatrix := TMatrix4.LookAtRH([0, EYE_Y_RESIZABLE[Self.ZoomLevel], EYE_Z_RESIZABLE[Self.ZoomLevel]], [0, 0, 1.0], Vector3_UnitY); Self.ProjMatrix := TMatrix4.PerspectiveFovRH(Sqrt(2), 765 / 503 *1.03, 0.01, 1.0); // fixed client width & height Self.WorldMatrix := TMatrix4.RotationYawPitchRoll(Rotation.Y, Rotation.X, Rotation.Z) * Matrix_Identity; Self.TransMatrix := Self.WorldMatrix * Self.ViewMatrix * Self.ProjMatrix; Self.Calculated := True; end; SetLength(Result, Length(Coords)); for I := 0 to High(Coords) do with Self.Transform(Coords[I], Self.TransMatrix) do begin Result[I].X := Round(X); Result[I].Y := Round(Y); end; end; procedure TAntiban.Setup(); override; begin Self.Skills := [ERSSkill.WOODCUTTING, ERSSkill.TOTAL]; Self.MinZoom := 15; Self.MaxZoom := 30; inherited; if OVERRIDEBREAKS then begin WriteLn('[ANTIBAN] Custom break override enabled:'); WriteLn(' - Break Interval: ', CUSTOMBREAKINTERVAL, ' minutes'); WriteLn(' - Break Duration: ', CUSTOMBREAKDURATION, ' minutes'); // Clear the default breaks so we can use our own Self.Breaks := []; // now we add our custom breaks Self.AddBreak(CUSTOMBREAKINTERVAL * ONE_MINUTE, CUSTOMBREAKDURATION * ONE_MINUTE, 0.33, 0.15); WriteLn('[ANTIBAN] Custom breaks initialized successfully'); end else begin WriteLn('[ANTIBAN] Using WaspLib break system'); end; end; function FormatRoundedNumber(Number: Int64): String; begin if Number >= 1000000 then Result := FormatFloat('0.0M', Number / 1000000) else if Number >= 1000 then Result := FormatFloat('0K', Number / 1000) else Result := SRL.FormatNumber(Number); end; function TIronwoodChopper.GetRandomWorldHopTime(): UInt64; var BaseInterval: UInt64; RandomVariation: Integer; begin BaseInterval := WORLDHOPINTERVAL * 60000; // 10% random variation to the interval either side RandomVariation := Random(-10, 11); Result := Round(BaseInterval * (1.0 + (RandomVariation / 100.0))); WriteLn('Next world hop scheduled in: ' + SRL.MsToTime(Result, Time_Short) + ' (Base: ' + IntToStr(WORLDHOPINTERVAL) + 'min, Variation: ' + IntToStr(RandomVariation) + '%)'); end; function TIronwoodChopper.GetWorldHopTimeRange(): String; var MinTime, MaxTime: Integer; begin MinTime := Round(WORLDHOPINTERVAL * 0.9); MaxTime := Round(WORLDHOPINTERVAL * 1.1); Result := IntToStr(MinTime) + '-' + IntToStr(MaxTime) + ' min'; end; procedure TIronwoodChopper.SetupWorldHopping(); begin writeln('Setting up world hopping...'); if not ENABLEWORLDHOPPING then begin writeln('World hopping is disabled.'); Exit; end; if Length(Login.Players[Login.PlayerIndex].Worlds) < 2 then begin writeln('ERROR: Need at least 2 worlds in player setup for world hopping!'); writeln('Disabling world hopping.'); ENABLEWORLDHOPPING := False; Exit; end; Self.NextWorldHopTime := GetTickCount() + Self.GetRandomWorldHopTime(); writeln('World hopping enabled. Next hop in: ' + SRL.MsToTime(Self.NextWorldHopTime - GetTickCount(), Time_Short)); end; function TIronwoodChopper.ShouldHopWorld(): Boolean; begin if not ENABLEWORLDHOPPING then Exit(False); Result := GetTickCount() >= Self.NextWorldHopTime; end; procedure TIronwoodChopper.DoWorldHop(); var currentWorld: Int32; targetWorld: Int32; availableWorlds: TIntegerArray; playerWorlds: TIntegerArray; i: Int32; begin if not ENABLEWORLDHOPPING then Exit; WriteLn('Attempting world hop...'); if not Logout.Open() then begin writeln('Failed to open logout menu'); Self.NextWorldHopTime := GetTickCount() + 120000; Exit; end; if Logout.GetButton(ERSLogoutButton.WORLD_SWITCHER).Visible() then Logout.GetButton(ERSLogoutButton.WORLD_SWITCHER).Click(MOUSE_LEFT); WaitUntil((currentWorld := WorldHopper.GetCurrentWorld()) <> 0, 200, 12000); writeln('Current world: ' + ToStr(currentWorld)); playerWorlds := Login.Players[Login.PlayerIndex].Worlds; for i := 0 to High(playerWorlds) do begin if playerWorlds[i] <> currentWorld then availableWorlds += playerWorlds[i]; end; if Length(availableWorlds) = 0 then begin writeln('No other worlds available to hop to, disabling world hopping!'); ENABLEWORLDHOPPING := False; Exit; end; targetWorld := availableWorlds[Random(Length(availableWorlds))]; writeln('Randomly selected target world: ' + ToStr(targetWorld)); if WorldHopper.Hop([targetWorld]) then begin writeln('Successfully hopped to world ' + ToStr(targetWorld) + '!'); Inc(Self.WorldHopsCompleted); Self.LastWorldHopTime := GetTickCount(); Self.NextWorldHopTime := GetTickCount() + Self.GetRandomWorldHopTime(); writeln('Next world hop in: ' + SRL.MsToTime(Self.NextWorldHopTime - GetTickCount(), Time_Short)); end else begin writeln('World hop to ' + ToStr(targetWorld) + ' failed, trying again in 2 minutes'); Self.NextWorldHopTime := GetTickCount() + 120000; end; end; procedure TIronwoodChopper.SetupObjects(); var I: Int32; TreeCoords: TPointArray; BankChestCoords: TPointArray; begin WriteLn('[OBJECTS] Setting up objects...'); TreeCoords := [ [4734, 41178], [4718, 41166], [4746, 41162], [4750, 41150], [4722, 41130], [4734, 41120], [4706, 41118] ]; for I := 0 to 6 do begin with Self.IronwoodTrees[I] do begin Walker := @Map.Walker; Finder.Colors := [CTS2(4611417, 10, 0.33, 0.09)]; Finder.Grow := 8; SetupEx([2, 2, 12], [[TreeCoords[I].X, TreeCoords[I].Y]]); SetupUpText('Chop'); end; end; BankChestCoords := [[4728, 41182]]; with Self.BankChest do begin Walker := @Map.Walker; Finder.Colors := [CTS2(2052968, 3, 0.07, 1.15)]; Finder.Grow := 8; SetupEx([1, 1, 2], [[4680, 41174]]); SetupUpText('Bank'); end; WriteLn('[OBJECTS] Objects Setup.'); end; procedure TIronwoodChopper.CountPlayersNearTrees(var Counts: TIntegerArray); var PlayerDots: TPointArray; TreeMM: TPoint; I, J: Int32; Dist: Double; begin SetLength(Counts, 7); for I := 0 to 6 do Counts[I] := 0; PlayerDots := Minimap.GetDots(ERSMinimapDot.PLAYER); if Length(PlayerDots) = 0 then Exit; for I := 0 to 6 do begin TreeMM := Map.Map2MM(Self.IronwoodTrees[I].Coordinates[0]); if not Minimap.Bounds.Contains(TreeMM) then Continue; for J := 0 to High(PlayerDots) do begin Dist := TreeMM.DistanceTo(PlayerDots[J]); if Dist <= PLAYER_DETECT_RADIUS then Inc(Counts[I]); end; end; end; function TIronwoodChopper.GetWeightedTreeIndex(ExcludeIndex: Int32): Int32; var I: Int32; TotalWeight: Int32; Roll: Int32; EffectiveBias: Int32; PlayerCounts: TIntegerArray; BestPlayerCount: Int32; BestTreeIndex: Int32; CandidateCount: Int32; Candidates: array[0..6] of Int32; begin if PREFERPOPULATEDTREES then begin Self.CountPlayersNearTrees(PlayerCounts); BestPlayerCount := 0; for I := 0 to 6 do begin if (ExcludeIndex <> -1) and (I = ExcludeIndex) then Continue; if PlayerCounts[I] > BestPlayerCount then BestPlayerCount := PlayerCounts[I]; end; if BestPlayerCount > 0 then begin WriteLn('[FORESTRY] Player counts per tree: ', PlayerCounts[0], ', ', PlayerCounts[1], ', ', PlayerCounts[2], ', ', PlayerCounts[3], ', ', PlayerCounts[4], ', ', PlayerCounts[5], ', ', PlayerCounts[6]); CandidateCount := 0; for I := 0 to 6 do begin if (ExcludeIndex <> -1) and (I = ExcludeIndex) then Continue; if PlayerCounts[I] = BestPlayerCount then begin Candidates[CandidateCount] := I; Inc(CandidateCount); end; end; BestTreeIndex := Candidates[Random(CandidateCount)]; WriteLn('[FORESTRY] Chose tree ', BestTreeIndex, ' (', BestPlayerCount, ' players nearby)'); Result := BestTreeIndex; Exit; end; end; if Random(100) < 15 then begin repeat Result := Random(7); until (ExcludeIndex = -1) or (Result <> ExcludeIndex); Exit; end; TotalWeight := 0; for I := 0 to High(TREE_WEIGHT_BIAS) do begin if (ExcludeIndex <> -1) and (I = ExcludeIndex) then Continue; Inc(TotalWeight, TREE_WEIGHT_BIAS[I]); end; if TotalWeight = 0 then begin repeat Result := Random(7); until (ExcludeIndex = -1) or (Result <> ExcludeIndex); Exit; end; Roll := Random(TotalWeight); for I := 0 to High(TREE_WEIGHT_BIAS) do begin if (ExcludeIndex <> -1) and (I = ExcludeIndex) then Continue; EffectiveBias := TREE_WEIGHT_BIAS[I]; if Roll < EffectiveBias then begin Result := I; Exit; end; Dec(Roll, EffectiveBias); end; Result := 0; end; procedure TIronwoodChopper.SendTerminationNotification(); begin if not ENABLEWEBHOOKS then Exit; if SENDSESSIONSUMMARYMSG then Self.SendSessionSummary(); end; function TItemData.GetPrice(item: TRSItem): TJSONObject; override; var id: Integer; json: TJSONObject; begin if Self.Disabled then Exit; if (Self.ReloadTimer.Length = 0) or Self.ReloadTimer.IsPaused() or Self.ReloadTimer.IsFinished() then Self.SetupPrices(); if Self.Disabled then Exit; id := Self.GetTradeableID(item); if (id = -1) and ToStr(item).IsDigit() then id := StrToInt(ToStr(item)); if id > -1 then begin json := Self.PriceData.getJSONObject('data'); if json <> nil then Result := json.getJSONObject(ToStr(id)); end; end; procedure TIronwoodChopper.Init(MaxActions: UInt32; MaxTime: UInt64); override; begin inherited; if AIChatbot.Enabled then begin WriteLn('[AI Chatbot] Enabled - initializing'); AIChatbot.Init(); end else WriteLn('[AI Chatbot] Disabled'); Map.SetupChunkEx([33, 37, 35, 35], [0]); Self.SetupObjects(); Objects.Setup(Map.Objects(), @Map.Walker); Self.LogItem := '32907'; Self.LogPrice := ItemData.GetAverage('32907'); WriteLn('[IRONWOOD] Log price: ' + ToStr(Self.LogPrice)); Self.CurrentTreeIndex := Self.GetWeightedTreeIndex(-1); Self.IsChopping := False; Self.LogsBanked := 0; Self.TotalLogsProfit := 0; Self.PreviousProfit := 0; Self.BankItemList := [ Self.LogItem, 'Clue scroll (beginner)', 'Clue scroll (easy)', 'Clue scroll (medium)', 'Clue scroll (hard)', 'Clue scroll (elite)' ]; APIClient.TimeStamp := GetTickCount(); Self.PrevXP := XPBar.Read(); Self.StartXP := Self.PrevXP; Self.LastXPTime := 0; Self.TreeTimer.Setup(15000); Self.LastReportTime := 0; Self.LastImageClear := GetTickCount(); Self.LastHourlyReportTime := GetTickCount(); Self.CurrentLevel := Stats.GetLevel(ERSSkill.WOODCUTTING); Self.LastLevel := Self.CurrentLevel; Self.WorldHopsCompleted := 0; Self.LastWorldHopTime := 0; Self.SetupWorldHopping(); if ENABLEWEBHOOKS then AddOnTerminate(@Self.SendTerminationNotification); if (RSClient.Mode <> ERSClientMode.FIXED) then begin WriteLn('[FAILSAFE] ERROR: You are not using the FIXED CLASSIC screen mode. FIXED CLASSIC mode is the only supported mode for this script.'); TerminateScript('Screen mode not supported'); end; if PREFERPOPULATEDTREES then WriteLn('[IRONWOOD] Forestry mode: ENABLED - will prefer trees with other players') else WriteLn('[IRONWOOD] Forestry mode: DISABLED - using standard tree weights'); if POWERCUT then WriteLn('[IRONWOOD] Power cutting: ENABLED - logs will be dropped') else WriteLn('[IRONWOOD] Power cutting: DISABLED - logs will be banked'); WriteLn('[IRONWOOD] Starting on tree: ', Self.CurrentTreeIndex); end; function TIronwoodChopper.GetState(): EIronwoodState; begin if WL.Activity.IsFinished() then Exit(EIronwoodState.END_SCRIPT); if Inventory.IsFull() then begin Self.IsChopping := False; if POWERCUT and Inventory.ContainsItem(Self.LogItem) then Exit(EIronwoodState.DROP_LOGS) else Exit(EIronwoodState.BANK_LOGS); end; if Self.IsChopping then Exit(EIronwoodState.WAIT_CHOPPING); if Minimap.IsSpecWeapon() and (Minimap.GetSpecLevel() = 100) then Exit(EIronwoodState.USE_SPEC); Result := EIronwoodState.CHOP_TREE; end; function TIronwoodChopper.IsStillChopping(): Boolean; var CurrentXP: Int32; TimeSinceXP: UInt64; TreeStillPresent: Boolean; begin Result := False; CurrentXP := XPBar.Read(); if CurrentXP > Self.PrevXP then begin Self.PrevXP := CurrentXP; Self.LastXPTime := GetTickCount(); Self.TreeTimer.Restart(); WL.Activity.Restart(); Result := True; Exit; end; if Self.TreeTimer.IsFinished() or Inventory.IsFull() then begin Self.IsChopping := False; Exit; end; TreeStillPresent := Self.IronwoodTrees[Self.CurrentTreeIndex].IsVisible(); if not TreeStillPresent then begin TimeSinceXP := GetTickCount() - Self.LastXPTime; Self.IsChopping := False; Exit; end; Result := True; end; function TIronwoodChopper.WaitChopping(): Boolean; begin while Self.IsStillChopping() do begin if (not RSClient.IsLoggedIn) then Exit; Self.DoAntiban(); if XPBar.EarnedXP then WL.Activity.Restart(); if (GetTimeRunning() mod 1000) < 500 then Self.DrawStatusDisplay(); if (GetTimeRunning() mod 10000) < 1000 then Self.Report(); end; Result := True; end; procedure TIronwoodChopper.SwitchToNextTree(); var OldIndex: Int32; begin OldIndex := Self.CurrentTreeIndex; Self.CurrentTreeIndex := Self.GetWeightedTreeIndex(OldIndex); WriteLn('[CHOPPING] Switching tree ', OldIndex, ' -> ', Self.CurrentTreeIndex); end; function TIronwoodChopper.ChopTree(): Boolean; var TargetTree: TRSObjectV2; TargetCoord: TPoint; TargetTileRect: TRectangle; CoordVisible: Boolean; begin if (not RSClient.IsLoggedIn) then Exit; if Bank.IsOpen() then Bank.Close(); if IsChopping then Exit(True); Result := False; TargetTree := Self.IronwoodTrees[Self.CurrentTreeIndex]; TargetCoord := TargetTree.Coordinates[0]; TargetTileRect := Map.GetTileMS(TargetCoord, 0); CoordVisible := MainScreen.IsVisible(TargetTileRect.Mean); if TargetTree.IsVisible() then begin if TargetTree.Click() then begin WriteLn('[CHOPPING] Clicked tree, waiting for XP...'); if XPBar.WaitXP(60000) then begin Self.IsChopping := True; Self.LastXPTime := GetTickCount(); Self.PrevXP := XPBar.Read(); Self.TreeTimer.Restart(); WL.Activity.Restart(); Result := True; end else WriteLn('[CHOPPING] Clicked tree but no XP gained.'); end else WriteLn('[CHOPPING] Failed to click tree.'); Exit; end; if CoordVisible then begin Self.SwitchToNextTree(); Exit(False); end; if Map.Walker.WalkBlind(TargetCoord, 60) then begin if TargetTree.IsVisible() then begin if TargetTree.Click() then begin if XPBar.WaitXP(60000) then begin Self.IsChopping := True; Self.LastXPTime := GetTickCount(); Self.PrevXP := XPBar.Read(); Self.TreeTimer.Restart(); WL.Activity.Restart(); Result := True; end else WriteLn('[CHOPPING] Clicked tree but no XP gained.'); end else WriteLn('[CHOPPING] Failed to click'); end else begin WriteLn('[CHOPPING] Tree still not visible.'); Self.SwitchToNextTree(); end; end else WriteLn('[CHOPPING] Failed to walk to target tree.'); end; function TIronwoodChopper.OpenBank(): Boolean; var OpenTimer: TCountDown; begin OpenTimer.Setup(12000); repeat if Bank.IsOpen() then Exit(True); if Self.BankChest.IsVisible() and Self.BankChest.Click() then WaitUntil(Bank.IsOpen(), 65, 5000) else Map.Walker.WalkBlind(Self.BankChest.Coordinates[0], 15); until Bank.IsOpen() or OpenTimer.IsFinished(); Result := Bank.IsOpen(); end; function TIronwoodChopper.DropLogs(): Boolean; var LogCount: Int32; begin LogCount := Inventory.CountItem(Self.LogItem); WriteLn('[POWERCUT] Dropping ', LogCount, ' logs...'); Result := Inventory.ShiftDrop([Self.LogItem], Inventory.RandomPattern()); if Result then begin Inc(Self.LogsBanked, LogCount); WriteLn('[POWERCUT] Dropped ', LogCount, ' logs (Total: ', Self.LogsBanked, ').'); end else WriteLn('[POWERCUT] Failed to drop logs.'); end; function TIronwoodChopper.BankLogs(): Boolean; var LogCount: Int32; Item: TRSItem; begin if (not RSClient.IsLoggedIn) then Exit; Result := False; LogCount := Inventory.CountItem(Self.LogItem); if not Inventory.ContainsAny(Self.BankItemList) then begin WriteLn('[BANKING] No bankable items found in inventory.'); Exit(False); end; WriteLn('[BANKING] Banking items...'); if not Self.OpenBank() then begin WriteLn('[BANKING] Failed to open bank chest.'); Exit(False); end; WaitUntil(Bank.IsOpen(), 65, 10000); if not Bank.IsOpen() then begin WriteLn('[BANKING] Failed to open bank.'); Exit(False); end; for Item in Self.BankItemList do begin if Inventory.ContainsItem(Item) then begin Bank.DepositItem(Item, True); Wait(150, 300); end; end; WaitUntil(not Inventory.ContainsAny(Self.BankItemList), 65, 3000); if LogCount > 0 then begin Inc(Self.LogsBanked, LogCount); Inc(Self.TotalLogsProfit, Int64(LogCount) * Self.LogPrice); WriteLn('[BANKING] Deposited ', LogCount, ' logs (Total banked: ', Self.LogsBanked, ').'); end; Result := True; Bank.Close(); end; function TIronwoodChopper.GetCurrentActivity(): String; begin case Self.State of EIronwoodState.CHOP_TREE: Result := 'Finding Tree'; EIronwoodState.WAIT_CHOPPING: Result := 'Chopping Tree'; EIronwoodState.USE_SPEC: Result := 'Using Spec'; EIronwoodState.DROP_LOGS: Result := 'Dropping Logs'; EIronwoodState.BANK_LOGS: Result := 'Banking'; else Result := 'Unknown'; end; end; procedure TIronwoodChopper.DrawUIOverlay(drawBox: TBox; title, status, runtime, logs, logsPerHour, profit, profitPerHour: String); var yPos: Int32; begin RSClient.Image().DrawBoxFilled(drawBox, False, $2D2D30); RSClient.Image().DrawBoxFilled(drawBox.Expand(1), False, $FFFFFF); RSClient.Image().DrawBoxFilled(drawBox, False, $2D2D30); RSClient.Image().SetFontName('Arial'); RSClient.Image().SetFontSize(10); yPos := drawBox.Y1 + 6; RSClient.Image().SetFontSize(13); RSClient.Image().DrawText(title, Point(drawBox.X1 + 3, yPos), $00D4FF); yPos += 14; RSClient.Image().SetFontSize(11); RSClient.Image().DrawText('Status: ' + status, Point(drawBox.X1 + 3, yPos), $FFFFFF); yPos += 12; RSClient.Image().DrawText('Runtime: ' + runtime, Point(drawBox.X1 + 3, yPos), $FFFFFF); yPos += 12; RSClient.Image().DrawText('Total Logs: ' + logs, Point(drawBox.X1 + 3, yPos), $FFFFFF); yPos += 12; RSClient.Image().DrawText('Profit: ' + profit, Point(drawBox.X1 + 3, yPos), $FFFFFF); end; procedure TIronwoodChopper.DrawStatusDisplay(); var StatusBox: TBox; CurrentActivity, Runtime, Logs, LogsPerHourStr, ProfitStr, ProfitPerHourStr: String; RuntimeMS: Int64; LogsPerHour, ProfitPerHour: Int32; begin StatusBox := Box(10, 260, 200, 330); RuntimeMS := GetTimeRunning(); CurrentActivity := Self.GetCurrentActivity(); Runtime := SRL.MsToTime(RuntimeMS, Time_Short); Logs := ToStr(Self.LogsBanked); if RuntimeMS > 0 then begin LogsPerHour := Round(Self.LogsBanked / (RuntimeMS / 3600000)); ProfitPerHour := Round(Self.TotalLogsProfit / (RuntimeMS / 3600000)); end else begin LogsPerHour := 0; ProfitPerHour := 0; end; LogsPerHourStr := ToStr(LogsPerHour); ProfitStr := FormatRoundedNumber(Self.TotalLogsProfit); ProfitPerHourStr := FormatRoundedNumber(ProfitPerHour); Self.DrawUIOverlay( StatusBox, 'BigAussie''s Ironwood Chopper', CurrentActivity, Runtime, Logs, LogsPerHourStr, ProfitStr, ProfitPerHourStr ); end; procedure TIronwoodChopper.Report(); var CurrentTime: UInt64; Runtime: Int64; CurrentXP, XPGained: Int32; XPPerHour: Int32; LogsPerHour: Int32; Profit: Int64; ProfitPerHour: Int32; ProfitAPI: Int64; begin CurrentTime := GetTickCount(); if (CurrentTime - Self.LastReportTime) < 10000 then Exit; Self.LastReportTime := CurrentTime; Runtime := GetTimeRunning(); CurrentXP := XPBar.Read(); XPGained := CurrentXP - Self.StartXP; if Runtime > 0 then begin XPPerHour := Round(XPGained / (Runtime / 3600000)); LogsPerHour := Round(Self.LogsBanked / (Runtime / 3600000)); ProfitPerHour := Round(Self.TotalLogsProfit / (Runtime / 3600000)); end else begin XPPerHour := 0; LogsPerHour := 0; ProfitPerHour := 0; end; Profit := Self.TotalLogsProfit; ProfitAPI := (Profit - Self.PreviousProfit) * 2; Self.PreviousProfit := Profit; APIClient.UpdatePayload(0, ProfitAPI, 0); APIClient.SubmitStats(APIClient.GetUUID()); WriteLn('========================================'); WriteLn(' BigAussie''s Ironwood Chopper '); WriteLn('========================================'); WriteLn(' Runtime: ' + SRL.MsToTime(Runtime, Time_Short)); if POWERCUT then WriteLn(' Logs Dropped: ' + ToStr(Self.LogsBanked)) else WriteLn(' Logs Banked: ' + ToStr(Self.LogsBanked)); WriteLn(' XP Gain (session): ' + ToStr(XPGained)); WriteLn(' XP/hr: ' + ToStr(XPPerHour)); WriteLn(' Logs/hr: ' + ToStr(LogsPerHour)); WriteLn(' Profit: ' + FormatRoundedNumber(Profit)); WriteLn(' Profit/hr: ' + FormatRoundedNumber(ProfitPerHour)); if ENABLEWORLDHOPPING then begin WriteLn(' '); WriteLn(' World Hopping: ENABLED (' + Self.GetWorldHopTimeRange() + ')'); WriteLn(' Hops Completed: ' + IntToStr(Self.WorldHopsCompleted)); if Self.NextWorldHopTime > GetTickCount() then WriteLn(' Next Hop: ' + SRL.MsToTime(Self.NextWorldHopTime - GetTickCount(), Time_Short)) else WriteLn(' Next Hop: READY!'); end; WriteLn('========================================'); end; procedure TIronwoodChopper.SendHourlyReport(); var EmbedIdx: Int32; Runtime: Int64; XPGained: Int32; XPPerHour: Int32; LogsPerHour: Int32; begin if not (ENABLEWEBHOOKS and SENDHOURLYREPORTMSG) then Exit; Runtime := GetTimeRunning(); if Runtime <= 0 then Exit; XPGained := XPBar.Read() - Self.StartXP; XPPerHour := Round(XPGained / (Runtime / 3600000)); LogsPerHour := Round(Self.LogsBanked / (Runtime / 3600000)); try Discord.Webhook.Content := '**Hourly Progress Report** :chart_with_upwards_trend:'; EmbedIdx := Discord.Webhook.AddEmbed(); Discord.Webhook.Embeds[EmbedIdx].Title := 'BigAussie''s Ironwood Chopper'; Discord.Webhook.Embeds[EmbedIdx].Color := $FFD700; Discord.Webhook.Embeds[EmbedIdx].Description := 'Runtime: ' + SRL.MsToTime(Runtime, Time_Short) + LineEnding + 'XP Gained: ' + FormatRoundedNumber(XPGained) + LineEnding + 'XP/Hour: ' + FormatRoundedNumber(XPPerHour) + LineEnding + 'Logs Banked: ' + IntToStr(Self.LogsBanked) + LineEnding + 'Logs/Hour: ' + IntToStr(LogsPerHour) + LineEnding + 'Profit: ' + FormatRoundedNumber(Self.TotalLogsProfit) + LineEnding + 'Status: ' + Self.GetCurrentActivity(); if Discord.Send() then WriteLn('[Discord] Hourly report sent!') else WriteLn('[Discord] Failed to send hourly report: ' + Discord.LastError); except WriteLn('[Discord] Error sending hourly report: ' + GetExceptionMessage); end; end; procedure TIronwoodChopper.SendSessionSummary(); var EmbedIdx: Int32; Runtime: Int64; XPGained: Int32; XPPerHour: Int32; LogsPerHour: Int32; begin if not (ENABLEWEBHOOKS and SENDSESSIONSUMMARYMSG) then Exit; Runtime := GetTimeRunning(); if Runtime <= 0 then Runtime := 1; XPGained := XPBar.Read() - Self.StartXP; XPPerHour := Round(XPGained / (Runtime / 3600000)); LogsPerHour := Round(Self.LogsBanked / (Runtime / 3600000)); try Discord.Webhook.Content := '**Session Complete!** :deciduous_tree:'; EmbedIdx := Discord.Webhook.AddEmbed(); Discord.Webhook.Embeds[EmbedIdx].Title := 'BigAussie''s Ironwood Chopper - Session Summary'; Discord.Webhook.Embeds[EmbedIdx].Color := $1E90FF; Discord.Webhook.Embeds[EmbedIdx].Description := 'Runtime: ' + SRL.MsToTime(Runtime, Time_Short) + LineEnding + 'Final Level: ' + IntToStr(Stats.GetLevel(ERSSkill.WOODCUTTING)) + LineEnding + 'XP Gained: ' + FormatRoundedNumber(XPGained) + LineEnding + 'XP/Hour: ' + FormatRoundedNumber(XPPerHour) + LineEnding + 'Logs Banked: ' + IntToStr(Self.LogsBanked) + ' (' + IntToStr(LogsPerHour) + '/hr)' + LineEnding + 'Profit: ' + FormatRoundedNumber(Self.TotalLogsProfit); if Discord.SendScreenshot(False) then WriteLn('[Discord] Session summary sent!') else WriteLn('[Discord] Failed to send session summary: ' + Discord.LastError); except WriteLn('[Discord] Error sending session summary: ' + GetExceptionMessage); end; end; procedure TIronwoodChopper.CheckHourlyReport(); var CurrentTime: UInt64; begin if not (ENABLEWEBHOOKS and SENDHOURLYREPORTMSG) then Exit; CurrentTime := GetTickCount(); if (CurrentTime - Self.LastHourlyReportTime) >= 3600000 then begin Self.SendHourlyReport(); Self.LastHourlyReportTime := CurrentTime; end; end; procedure TIronwoodChopper.Run(MaxActions: UInt32; MaxTime: UInt64); begin Self.Init(MaxActions, MaxTime); //Self.DrawStatusDisplay(); repeat if (not RSClient.IsLoggedIn) then if Login.GetPlayer.User <> '' then Login.LoginPlayer; Self.State := Self.GetState(); Self.SetAction(ToStr(Self.State)); // Check for world hop (only when not actively chopping) if Self.ShouldHopWorld() and (Self.State <> EIronwoodState.WAIT_CHOPPING) then begin Self.DoWorldHop(); Continue; end; case Self.State of EIronwoodState.CHOP_TREE: Self.ChopTree(); EIronwoodState.WAIT_CHOPPING: Self.WaitChopping(); EIronwoodState.USE_SPEC: Minimap.EnableSpec(100); EIronwoodState.DROP_LOGS: Self.DropLogs(); EIronwoodState.BANK_LOGS: Self.BankLogs(); EIronwoodState.END_SCRIPT: Break; end; if (GetTickCount() - Self.LastImageClear) >= 30000 then begin try RSClient.Image.Clear; except end; Self.DrawStatusDisplay(); Self.LastImageClear := GetTickCount(); end; if (GetTimeRunning() mod 1000) < 500 then Self.DrawStatusDisplay(); if (GetTimeRunning() mod 10000) < 1000 then Self.Report(); Self.CheckHourlyReport(); until Self.ShouldStop(); Self.Report(); end; {$IFDEF SCRIPT_GUI} const IW_GUI_NAV = $F0EBE4; IW_GUI_BG = $FFFFFF; IW_GUI_STATUS = $F7F4F1; IW_GUI_DIVIDER = $D4C9BC; IW_GUI_CARD = $F7F4F1; IW_GUI_ACCENT = $E0C9AE; IW_GUI_NAV_IDLE = $E7DED4; IW_GUI_START = $C9B094; IW_GUI_TEXT = $1C1C1E; IW_GUI_MUTED = $6B6B70; IW_GUI_W = 1120; IW_GUI_H = 700; IW_GUI_NAV_W = 190; IW_GUI_STATUS_H = 56; IW_GUI_BTN_H = 42; IW_GUI_EDIT_W = 260; IW_GUI_COL2 = 308; IW_GUI_BOTTOM_INSET = 34; type TConfig = record(TScriptForm) NavPanel, ContentPanel, StatusPanel, DividerPanel, StatusDivider, StatusPad: TPanel; BrandLabel: TLabel; ScriptNav, AccountsNav, AntibanNav, AIChatNav: TImage; StartChip: TImage; ActiveNav: TImage; ScriptSection, AccountsSection, AntibanSection, AIChatSection: TPanel; PreferPopulatedTreesCheckBox, PowerCutCheckBox: TLabeledCheckBox; EnableWebhooksCheckBox, HourlyReportsCheckBox, SessionSummaryCheckBox: TLabeledCheckBox; EnableWorldHoppingCheckBox, OverrideBreaksCheckBox: TLabeledCheckBox; WebHookInput, WorldHopIntervalInput, CustomBreakIntervalInput, CustomBreakDurationInput: TLabeledEdit; MaxActionsInput, MaxTimeInput: TLabeledEdit; TestButton: TButton; WebhookInfo: TLabel; end; function TConfig.MakePanel( parent: TWinControl; left, top, width, height, color: Int32 ): TPanel; begin Result.Create(parent); Result.SetLeft(TControl.AdjustToDPI(left)); Result.SetTop(TControl.AdjustToDPI(top)); Result.SetWidth(TControl.AdjustToDPI(width)); Result.SetHeight(TControl.AdjustToDPI(height)); Result.SetColor(color); Result.SetBevelOuter(bvNone); end; procedure TConfig.PaintRoundChip( img: TImage; labelText: String; fill, parentBg: Int32; selected: Boolean = False; centered: Boolean = False ); var bmp: TBitmap; c: TCanvas; w, h, r, tw, th, tx: Int32; begin w := img.GetWidth(); h := img.GetHeight(); if (w <= 0) or (h <= 0) then Exit; r := TControl.AdjustToDPI(6); if r * 2 > h then r := h div 2; if r * 2 > w then r := w div 2; bmp := img.GetPicture().GetBitmap(); bmp.SetWidth(w); bmp.SetHeight(h); c := bmp.GetCanvas(); c.GetBrush().SetColor(parentBg); c.GetPen().SetColor(parentBg); c.FillRect(0, 0, w, h); c.GetBrush().SetColor(fill); c.GetPen().SetColor(fill); c.RoundRect(0, 0, w - 1, h - 1, r * 2, r * 2); c.GetBrush().SetColor(fill); c.GetFont().SetColor(IW_GUI_TEXT); c.GetFont().SetSize(11); if selected then c.GetFont().SetStyle([fsBold]) else c.GetFont().SetStyle([]); tw := c.TextWidth(labelText); th := c.TextHeight(labelText); if centered then tx := (w - tw) div 2 else tx := TControl.AdjustToDPI(16); c.TextOut(tx, (h - th) div 2, labelText); img.SetHint(labelText); img.SetShowHint(False); img.Invalidate(); end; function TConfig.MakeNavItem( parent: TWinControl; labelText: String; left, top: Int32; selected: Boolean = False ): TImage; var fill: Int32; begin Result.Create(parent); Result.SetLeft(TControl.AdjustToDPI(left)); Result.SetTop(TControl.AdjustToDPI(top)); Result.SetWidth(TControl.AdjustToDPI(IW_GUI_NAV_W - 28)); Result.SetHeight(TControl.AdjustToDPI(IW_GUI_BTN_H)); Result.SetCursor(crHandPoint); if selected then fill := IW_GUI_ACCENT else fill := IW_GUI_NAV_IDLE; Self.PaintRoundChip(Result, labelText, fill, IW_GUI_NAV, selected); end; function TConfig.MakeLabel( parent: TWinControl; labelText: String; left, top, fontSize, fontColor: Int32; bold: Boolean = False ): TLabel; begin Result.Create(parent); Result.SetLeft(TControl.AdjustToDPI(left)); Result.SetTop(TControl.AdjustToDPI(top)); Result.SetCaption(labelText); Result.GetFont().SetSize(fontSize); Result.GetFont().SetColor(fontColor); if bold then Result.GetFont().SetStyle([fsBold]); end; procedure TConfig.AddCheck( parent: TWinControl; var box: TLabeledCheckBox; ctrlName, labelText: String; left, top: Int32; checked: Boolean ); begin box.Create(parent); box.SetLeft(TControl.AdjustToDPI(left)); box.SetTop(TControl.AdjustToDPI(top)); box.SetName(ctrlName); box.SetFontColor(IW_GUI_TEXT); box.SetCaption(labelText); if checked then box.CheckBox.SetChecked(True); end; procedure TConfig.AddEdit( parent: TWinControl; var box: TLabeledEdit; ctrlName, labelText, text: String; left, top, width: Int32 ); begin box.Create(parent); box.SetLeft(TControl.AdjustToDPI(left)); box.SetTop(TControl.AdjustToDPI(top)); box.SetWidth(TControl.AdjustToDPI(width)); box.SetName(ctrlName); box.SetCaption(labelText); box.SetText(text); box.SetFontColor(IW_GUI_TEXT); end; procedure TConfig.AddHeader(parent: TPanel; titleText, subtitleText: String); var header: TPanel; headerW: Int32; begin headerW := parent.GetWidth(); if headerW <= 0 then headerW := TControl.AdjustToDPI(IW_GUI_W - IW_GUI_NAV_W - 2); header := Self.MakePanel(parent, 0, 0, 1, 88, IW_GUI_CARD); header.SetWidth(headerW); header.SetHeight(TControl.AdjustToDPI(88)); Self.MakeLabel(header, titleText, 24, 18, 18, IW_GUI_TEXT, True); Self.MakeLabel(header, subtitleText, 24, 52, 10, IW_GUI_MUTED, False); end; procedure TConfig.StyleNav(selected: TImage); begin Self.ActiveNav := selected; Self.PaintRoundChip(Self.ScriptNav, Self.ScriptNav.GetHint(), IW_GUI_NAV_IDLE, IW_GUI_NAV, False); Self.PaintRoundChip(Self.AccountsNav, Self.AccountsNav.GetHint(), IW_GUI_NAV_IDLE, IW_GUI_NAV, False); Self.PaintRoundChip(Self.AntibanNav, Self.AntibanNav.GetHint(), IW_GUI_NAV_IDLE, IW_GUI_NAV, False); Self.PaintRoundChip(Self.AIChatNav, Self.AIChatNav.GetHint(), IW_GUI_NAV_IDLE, IW_GUI_NAV, False); Self.PaintRoundChip(selected, selected.GetHint(), IW_GUI_ACCENT, IW_GUI_NAV, True); end; procedure TConfig.ShowSection(section: TPanel; selected: TImage); begin Self.ScriptSection.SetVisible(False); Self.AccountsSection.SetVisible(False); Self.AntibanSection.SetVisible(False); Self.AIChatSection.SetVisible(False); section.SetVisible(True); Self.StyleNav(selected); end; procedure TConfig.ShowScript({$H-}sender: TObject); {$H+} begin Self.ShowSection(Self.ScriptSection, Self.ScriptNav); end; procedure TConfig.ShowAccounts({$H-}sender: TObject); {$H+} begin Self.ShowSection(Self.AccountsSection, Self.AccountsNav); end; procedure TConfig.ShowAntiban({$H-}sender: TObject); {$H+} begin Self.ShowSection(Self.AntibanSection, Self.AntibanNav); end; procedure TConfig.ShowAIChat({$H-}sender: TObject); {$H+} begin Self.ShowSection(Self.AIChatSection, Self.AIChatNav); end; procedure TConfig.WebhooksCheckboxChanged(Sender: TObject); var ShowWebhook: Boolean; begin if Sender = nil then; ShowWebhook := Self.EnableWebhooksCheckBox.IsChecked(); Self.WebHookInput.SetVisible(ShowWebhook); Self.TestButton.SetVisible(ShowWebhook); Self.WebhookInfo.SetVisible(ShowWebhook); Self.HourlyReportsCheckBox.SetVisible(ShowWebhook); Self.SessionSummaryCheckBox.SetVisible(ShowWebhook); end; procedure TConfig.OpenWebhookHelp(Sender: TObject); begin if Sender = nil then; OpenWebPage('https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks'); end; procedure TConfig.TestButtonClick(Sender: TObject); begin if Self.WebHookInput.GetText = '' then begin ShowMessage('Please enter a Webhook URL'); Exit; end; Discord.Webhook.Content := 'Test message from BigAussie Ironwood Chopper'; Discord.Webhook.URL := Self.WebHookInput.GetText; if Discord.Send() then ShowMessage('Test message sent. Please check Discord.') else ShowMessage('Failed to send test message: ' + Discord.LastError); end; procedure TConfig.OverrideBreaksCheckboxChanged(Sender: TObject); begin if Sender = nil then; Self.CustomBreakIntervalInput.SetVisible(Self.OverrideBreaksCheckBox.IsChecked()); Self.CustomBreakDurationInput.SetVisible(Self.OverrideBreaksCheckBox.IsChecked()); end; procedure TConfig.WorldHoppingCheckboxChanged(Sender: TObject); begin if Sender = nil then; Self.WorldHopIntervalInput.SetVisible(Self.EnableWorldHoppingCheckBox.IsChecked()); end; procedure TConfig.StartScript(Sender: TObject); override; var Username: String; begin if Sender = nil then; if (Login.PlayerIndex < 0) or (Login.PlayerIndex > High(Login.Players)) then Username := 'NoUserSelected' else Username := Login.Players[Login.PlayerIndex].User; WEBHOOKURL := Self.WebHookInput.GetText; ENABLEWEBHOOKS := Self.EnableWebhooksCheckBox.IsChecked(); SENDHOURLYREPORTMSG := Self.HourlyReportsCheckBox.IsChecked(); SENDSESSIONSUMMARYMSG := Self.SessionSummaryCheckBox.IsChecked(); ENABLEWORLDHOPPING := Self.EnableWorldHoppingCheckBox.IsChecked(); WORLDHOPINTERVAL := StrToIntDef(Self.WorldHopIntervalInput.GetText(), 45); PREFERPOPULATEDTREES := Self.PreferPopulatedTreesCheckBox.IsChecked(); POWERCUT := Self.PowerCutCheckBox.IsChecked(); OVERRIDEBREAKS := Self.OverrideBreaksCheckBox.IsChecked(); CUSTOMBREAKINTERVAL := StrToIntDef(Self.CustomBreakIntervalInput.GetText(), 60); CUSTOMBREAKDURATION := StrToIntDef(Self.CustomBreakDurationInput.GetText(), 5); WLSettings.Put('max_actions', StrToIntDef(Self.MaxActionsInput.GetText(), 0)); WLSettings.Put('max_time', StrToIntDef(Self.MaxTimeInput.GetText(), 0)); WLSettings.SaveConfig(); WriteINI(Username + ' Ironwood Webhook Settings', 'WebhookURL', WEBHOOKURL, 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Webhook Settings', 'EnableWebhooks', BoolToStr(ENABLEWEBHOOKS, 'true', 'false'), 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Webhook Settings', 'HourlyReports', BoolToStr(SENDHOURLYREPORTMSG, 'true', 'false'), 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Webhook Settings', 'SessionSummary', BoolToStr(SENDSESSIONSUMMARYMSG, 'true', 'false'), 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Script Settings', 'EnableWorldHopping', BoolToStr(ENABLEWORLDHOPPING, 'true', 'false'), 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Script Settings', 'WorldHopInterval', IntToStr(WORLDHOPINTERVAL), 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Script Settings', 'PreferPopulatedTrees', BoolToStr(PREFERPOPULATEDTREES, 'true', 'false'), 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Script Settings', 'PowerCut', BoolToStr(POWERCUT, 'true', 'false'), 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Antiban Manager', 'OverrideBreaks', BoolToStr(OVERRIDEBREAKS, 'true', 'false'), 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Antiban Manager', 'CustomBreakInterval', IntToStr(CUSTOMBREAKINTERVAL), 'Configs/BAIronwoodSettings.ini'); WriteINI(Username + ' Ironwood Antiban Manager', 'CustomBreakDuration', IntToStr(CUSTOMBREAKDURATION), 'Configs/BAIronwoodSettings.ini'); Discord.SetWebhook(WEBHOOKURL); try Self._WarmBuildCache(); except end; AIChatbot.ApplyFromGUI(); Self.Form.SetOnClose(nil); Self.Form.Close(); end; procedure TConfig.BuildScriptSection( SavedPreferPopulatedTrees, SavedPowerCut: Boolean ); begin Self.AddHeader(Self.ScriptSection, 'Action', 'Chop ironwood trees in the Mining Guild.'); Self.AddCheck(Self.ScriptSection, Self.PreferPopulatedTreesCheckBox, 'cb_iw_prefer_populated', 'Prefer trees with players (forestry)', 24, 110, SavedPreferPopulatedTrees); Self.PreferPopulatedTreesCheckBox.SetTooltip('Prefer chopping trees that other players are already at for forestry worlds.'); Self.AddCheck(Self.ScriptSection, Self.PowerCutCheckBox, 'cb_iw_powercut', 'Power cut (drop logs)', 24, 150, SavedPowerCut); Self.PowerCutCheckBox.SetTooltip('Drop logs instead of banking. Clue scrolls will still be kept.'); end; procedure TConfig.BuildAccountsSection( SavedWebhookURL: String; SavedEnableWebhooks, SavedHourly, SavedSessionSummary: Boolean ); var am: TPanel; selectorPanel, userPanel, passPanel, pinPanel, worldsPanel, jagexPanel: TPanel; addBtn, deleteBtn, generateBtn: TButton; leftColW, passW, pinW, worldsLeft, btnW, btnGap: Int32; begin Self.AddHeader(Self.AccountsSection, 'Accounts', 'Login account and optional Discord webhook notifications.'); am := Self.CreateAccountManager(Self.AccountsSection); am.SetLeft(TControl.AdjustToDPI(16)); am.SetTop(TControl.AdjustToDPI(100)); am.SetColor(IW_GUI_BG); leftColW := TControl.AdjustToDPI(280); passW := Floor(leftColW * 0.6); pinW := Floor(leftColW * 0.3); selectorPanel := Self.Form.GetChild('am_selector_panel'); userPanel := Self.Form.GetChild('am_user_panel'); passPanel := Self.Form.GetChild('am_pass_panel'); pinPanel := Self.Form.GetChild('am_pin_panel'); worldsPanel := Self.Form.GetChild('am_worlds_panel'); jagexPanel := Self.Form.GetChild('am_jagex_panel'); if selectorPanel <> nil then selectorPanel.SetWidth(leftColW); if userPanel <> nil then userPanel.SetWidth(leftColW); if jagexPanel <> nil then jagexPanel.SetWidth(leftColW); if passPanel <> nil then passPanel.SetWidth(passW); if (pinPanel <> nil) and (selectorPanel <> nil) then begin pinPanel.SetWidth(pinW); pinPanel.SetLeft(selectorPanel.GetLeft() + leftColW - pinW); end; if (worldsPanel <> nil) and (selectorPanel <> nil) and (userPanel <> nil) then begin worldsLeft := selectorPanel.GetLeft() + leftColW + TControl.AdjustToDPI(48); worldsPanel.SetLeft(worldsLeft); worldsPanel.SetHeight(userPanel.GetBottom()); addBtn := Self.Form.GetChild('am_add_button'); deleteBtn := Self.Form.GetChild('am_delete_button'); generateBtn := Self.Form.GetChild('am_generate_worlds_button'); btnGap := TControl.AdjustToDPI(8); btnW := Floor((worldsPanel.GetWidth() - btnGap) / 2); if generateBtn <> nil then begin generateBtn.SetLeft(worldsLeft); generateBtn.SetWidth(worldsPanel.GetWidth()); end; if addBtn <> nil then begin addBtn.SetLeft(worldsLeft); addBtn.SetWidth(btnW); end; if deleteBtn <> nil then begin deleteBtn.SetLeft(worldsLeft + worldsPanel.GetWidth() - btnW - TControl.AdjustToDPI(1)); deleteBtn.SetWidth(btnW); end; if (addBtn <> nil) and (am.GetHeight() < addBtn.GetTop() + addBtn.GetHeight() + TControl.AdjustToDPI(8)) then am.SetHeight(addBtn.GetTop() + addBtn.GetHeight() + TControl.AdjustToDPI(8)); end; Self.ApplyJagexAccountVisibility(); Self.MakeLabel(Self.AccountsSection, 'Discord', 24, 340, 11, IW_GUI_TEXT, True); Self.AddCheck(Self.AccountsSection, Self.EnableWebhooksCheckBox, 'cb_iw_webhooks', 'Discord notifications', 24, 360, SavedEnableWebhooks); Self.EnableWebhooksCheckBox.SetTooltip('Send hourly progress and session summary messages to Discord.'); Self.EnableWebhooksCheckBox.CheckBox.SetOnChange(@Self.WebhooksCheckboxChanged); Self.AddCheck(Self.AccountsSection, Self.HourlyReportsCheckBox, 'cb_iw_hourly', 'Hourly progress reports', 24, 390, SavedHourly); Self.HourlyReportsCheckBox.SetTooltip('Send an hourly progress report to Discord while the script is running.'); Self.AddCheck(Self.AccountsSection, Self.SessionSummaryCheckBox, 'cb_iw_session', 'Session summary on stop', 24, 420, SavedSessionSummary); Self.SessionSummaryCheckBox.SetTooltip('Send a session summary message when the script stops.'); Self.WebhookInfo := Self.MakeLabel(Self.AccountsSection, 'Click here to learn how to generate a Discord webhook URL', 24, 460, 10, $C07020, False); Self.WebhookInfo.SetOnClick(@Self.OpenWebhookHelp); Self.WebhookInfo.SetCursor(crHandPoint); Self.WebhookInfo.SetTooltip('Opens a guide for creating a Discord webhook URL.'); Self.AddEdit(Self.AccountsSection, Self.WebHookInput, 'le_iw_webhook', 'Webhook URL', SavedWebhookURL, 24, 490, 520); Self.WebHookInput.SetPasswordChar('*'); Self.WebHookInput.SetTooltip('Paste your Discord webhook URL here. Kept hidden while typing.'); Self.TestButton.Create(Self.AccountsSection); Self.TestButton.SetLeft(TControl.AdjustToDPI(560)); Self.TestButton.SetTop(TControl.AdjustToDPI(508)); Self.TestButton.SetWidth(TControl.AdjustToDPI(80)); Self.TestButton.SetHeight(TControl.AdjustToDPI(28)); Self.TestButton.SetCaption('Test'); Self.TestButton.SetTooltip('Sends a test Discord message using the webhook URL above.'); Self.TestButton.SetOnClick(@Self.TestButtonClick); end; procedure TConfig.BuildAntibanSection( SavedEnableWorldHopping, SavedOverrideBreaks: Boolean; SavedWorldHopInterval, SavedCustomBreakInterval, SavedCustomBreakDuration, SavedMaxActions, SavedMaxTime: Integer ); begin Self.AddHeader(Self.AntibanSection, 'Antiban', 'Breaks, world hopping, and session limits.'); Self.AddEdit(Self.AntibanSection, Self.MaxActionsInput, 'le_iw_maxactions', 'Max actions (0 = unlimited)', IntToStr(SavedMaxActions), 24, 110, IW_GUI_EDIT_W); Self.MaxActionsInput.Edit.SetOnKeyPress(@Self.MaxActionsInput.Edit.NumberField); Self.MaxActionsInput.SetTooltip('Stop after this many completed actions. 0 = no limit.'); Self.AddEdit(Self.AntibanSection, Self.MaxTimeInput, 'le_iw_maxtime', 'Max time minutes (0 = unlimited)', IntToStr(SavedMaxTime), IW_GUI_COL2, 110, IW_GUI_EDIT_W); Self.MaxTimeInput.Edit.SetOnKeyPress(@Self.MaxTimeInput.Edit.NumberField); Self.MaxTimeInput.SetTooltip('Stop after this many minutes of runtime. 0 = no limit.'); Self.AddCheck(Self.AntibanSection, Self.OverrideBreaksCheckBox, 'cb_iw_override_breaks', 'Override default breaks', 24, 190, SavedOverrideBreaks); Self.OverrideBreaksCheckBox.SetTooltip('Replace default break timing with the custom interval and duration below.'); Self.OverrideBreaksCheckBox.CheckBox.SetOnChange(@Self.OverrideBreaksCheckboxChanged); Self.AddEdit(Self.AntibanSection, Self.CustomBreakIntervalInput, 'le_iw_break_interval', 'Break interval (minutes)', IntToStr(SavedCustomBreakInterval), 24, 230, IW_GUI_EDIT_W); Self.CustomBreakIntervalInput.Edit.SetOnKeyPress(@Self.CustomBreakIntervalInput.Edit.NumberField); Self.CustomBreakIntervalInput.SetTooltip('How often to take breaks when override is enabled. Includes randomness.'); Self.AddEdit(Self.AntibanSection, Self.CustomBreakDurationInput, 'le_iw_break_duration', 'Break duration (minutes)', IntToStr(SavedCustomBreakDuration), IW_GUI_COL2, 230, IW_GUI_EDIT_W); Self.CustomBreakDurationInput.Edit.SetOnKeyPress(@Self.CustomBreakDurationInput.Edit.NumberField); Self.CustomBreakDurationInput.SetTooltip('How long each break lasts when override is enabled. Includes randomness.'); Self.AddCheck(Self.AntibanSection, Self.EnableWorldHoppingCheckBox, 'cb_iw_worldhop', 'Enable world hopping', 24, 310, SavedEnableWorldHopping); Self.EnableWorldHoppingCheckBox.SetTooltip('Hop worlds on a timer. Needs at least 2 worlds saved on the account.'); Self.EnableWorldHoppingCheckBox.CheckBox.SetOnChange(@Self.WorldHoppingCheckboxChanged); Self.AddEdit(Self.AntibanSection, Self.WorldHopIntervalInput, 'le_iw_hop_interval', 'World hop interval (minutes)', IntToStr(SavedWorldHopInterval), 24, 350, IW_GUI_EDIT_W); Self.WorldHopIntervalInput.Edit.SetOnKeyPress(@Self.WorldHopIntervalInput.Edit.NumberField); Self.WorldHopIntervalInput.SetTooltip('Minutes between world hops. Small random variation is applied.'); end; procedure TConfig.BuildShell( SavedPreferPopulatedTrees, SavedPowerCut: Boolean; SavedWebhookURL: String; SavedEnableWebhooks, SavedHourly, SavedSessionSummary: Boolean; SavedEnableWorldHopping, SavedOverrideBreaks: Boolean; SavedWorldHopInterval, SavedCustomBreakInterval, SavedCustomBreakDuration, SavedMaxActions, SavedMaxTime: Integer ); var tab: TTabSheet; contentH, startW, startH, shellW, shellH, statusTop, statusH, mainLeft, mainW, navW, bottomInset: Int32; begin Self.AddTab('Ironwood Chopper'); tab := Self.Tabs[High(Self.Tabs)]; Self.Start.SetAlign(alNone); Self.Start.SetVisible(False); Self.Start.SetHeight(0); Self.Start.SetWidth(0); bottomInset := TControl.AdjustToDPI(IW_GUI_BOTTOM_INSET); navW := TControl.AdjustToDPI(IW_GUI_NAV_W); statusH := TControl.AdjustToDPI(IW_GUI_STATUS_H); shellW := Self.Size.X; shellH := Self.Size.Y; mainLeft := navW + TControl.AdjustToDPI(2); mainW := shellW - mainLeft; statusTop := shellH - statusH - bottomInset; contentH := statusTop; startW := TControl.AdjustToDPI(132); startH := TControl.AdjustToDPI(IW_GUI_BTN_H); Self.NavPanel.Create(tab); Self.NavPanel.SetLeft(0); Self.NavPanel.SetTop(0); Self.NavPanel.SetWidth(navW); Self.NavPanel.SetHeight(shellH); Self.NavPanel.SetColor(IW_GUI_NAV); Self.NavPanel.SetBevelOuter(bvNone); Self.DividerPanel.Create(tab); Self.DividerPanel.SetLeft(navW); Self.DividerPanel.SetTop(0); Self.DividerPanel.SetWidth(TControl.AdjustToDPI(2)); Self.DividerPanel.SetHeight(shellH); Self.DividerPanel.SetColor(IW_GUI_DIVIDER); Self.DividerPanel.SetBevelOuter(bvNone); Self.ContentPanel.Create(tab); Self.ContentPanel.SetLeft(mainLeft); Self.ContentPanel.SetTop(0); Self.ContentPanel.SetWidth(mainW); Self.ContentPanel.SetHeight(contentH); Self.ContentPanel.SetColor(IW_GUI_BG); Self.ContentPanel.SetBevelOuter(bvNone); Self.StatusDivider.Create(tab); Self.StatusDivider.SetLeft(mainLeft); Self.StatusDivider.SetTop(statusTop); Self.StatusDivider.SetWidth(mainW); Self.StatusDivider.SetHeight(TControl.AdjustToDPI(2)); Self.StatusDivider.SetColor(IW_GUI_DIVIDER); Self.StatusDivider.SetBevelOuter(bvNone); Self.StatusPanel.Create(tab); Self.StatusPanel.SetLeft(mainLeft); Self.StatusPanel.SetTop(statusTop + Self.StatusDivider.GetHeight()); Self.StatusPanel.SetWidth(mainW); Self.StatusPanel.SetHeight(statusH - Self.StatusDivider.GetHeight()); Self.StatusPanel.SetColor(IW_GUI_STATUS); Self.StatusPanel.SetBevelOuter(bvNone); Self.StatusPad.Create(tab); Self.StatusPad.SetLeft(mainLeft); Self.StatusPad.SetTop(Self.StatusPanel.GetTop() + Self.StatusPanel.GetHeight()); Self.StatusPad.SetWidth(mainW); Self.StatusPad.SetHeight(shellH - Self.StatusPad.GetTop()); Self.StatusPad.SetColor(IW_GUI_STATUS); Self.StatusPad.SetBevelOuter(bvNone); Self.BrandLabel := Self.MakeLabel(Self.NavPanel, 'Ironwood Chopper', 16, 28, 14, IW_GUI_TEXT, True); Self.MakeLabel(Self.NavPanel, 'BigAussie', 16, 54, 9, IW_GUI_MUTED, False); Self.ScriptNav := Self.MakeNavItem(Self.NavPanel, 'Action', 12, 100, True); Self.AccountsNav := Self.MakeNavItem(Self.NavPanel, 'Accounts', 12, 152); Self.AntibanNav := Self.MakeNavItem(Self.NavPanel, 'Antiban', 12, 204); Self.AIChatNav := Self.MakeNavItem(Self.NavPanel, 'AI Chat', 12, 256); Self.ScriptSection := Self.MakePanel(Self.ContentPanel, 0, 0, 100, 100, IW_GUI_BG); Self.AccountsSection := Self.MakePanel(Self.ContentPanel, 0, 0, 100, 100, IW_GUI_BG); Self.AntibanSection := Self.MakePanel(Self.ContentPanel, 0, 0, 100, 100, IW_GUI_BG); Self.AIChatSection := Self.MakePanel(Self.ContentPanel, 0, 0, 100, 100, IW_GUI_BG); Self.ScriptSection.SetWidth(mainW); Self.ScriptSection.SetHeight(contentH); Self.AccountsSection.SetWidth(mainW); Self.AccountsSection.SetHeight(contentH); Self.AntibanSection.SetWidth(mainW); Self.AntibanSection.SetHeight(contentH); Self.AIChatSection.SetWidth(mainW); Self.AIChatSection.SetHeight(contentH); Self.BuildScriptSection(SavedPreferPopulatedTrees, SavedPowerCut); Self.BuildAccountsSection(SavedWebhookURL, SavedEnableWebhooks, SavedHourly, SavedSessionSummary); Self.BuildAntibanSection(SavedEnableWorldHopping, SavedOverrideBreaks, SavedWorldHopInterval, SavedCustomBreakInterval, SavedCustomBreakDuration, SavedMaxActions, SavedMaxTime); AIChatbot.BuildSettings(Self.AIChatSection, mainW); Self.ScriptNav.SetOnClick(@Self.ShowScript); Self.AccountsNav.SetOnClick(@Self.ShowAccounts); Self.AntibanNav.SetOnClick(@Self.ShowAntiban); Self.AIChatNav.SetOnClick(@Self.ShowAIChat); Self.StartChip.Create(Self.StatusPanel); Self.StartChip.SetLeft(mainW - startW - TControl.AdjustToDPI(24)); Self.StartChip.SetTop((Self.StatusPanel.GetHeight() - startH) div 2 + 2); Self.StartChip.SetWidth(startW); Self.StartChip.SetHeight(startH); Self.StartChip.SetCursor(crHandPoint); Self.PaintRoundChip(Self.StartChip, 'Start', IW_GUI_START, IW_GUI_STATUS, True, True); Self.StartChip.SetOnClick(@Self.StartScript); Self.WebhooksCheckboxChanged(nil); Self.OverrideBreaksCheckboxChanged(nil); Self.WorldHoppingCheckboxChanged(nil); Self.ShowScript(Self.ScriptNav); end; procedure TConfig.Run(); override; var Username: String; SavedWebhookURL: String; SavedEnableWebhooks, SavedHourly, SavedSessionSummary: Boolean; SavedEnableWorldHopping, SavedOverrideBreaks: Boolean; SavedPreferPopulatedTrees, SavedPowerCut: Boolean; SavedWorldHopInterval, SavedCustomBreakInterval, SavedCustomBreakDuration: Integer; SavedMaxActions, SavedMaxTime: Integer; begin BashPromptIfUpdateAvailable(); if (Login.PlayerIndex < 0) or (Login.PlayerIndex > High(Login.Players)) then Username := 'NoUserSelected' else Username := Login.Players[Login.PlayerIndex].User; SavedWebhookURL := ReadINI(Username + ' Ironwood Webhook Settings', 'WebhookURL', 'Configs/BAIronwoodSettings.ini'); SavedEnableWebhooks := StrToBoolDef(ReadINI(Username + ' Ironwood Webhook Settings', 'EnableWebhooks', 'Configs/BAIronwoodSettings.ini'), False); SavedHourly := StrToBoolDef(ReadINI(Username + ' Ironwood Webhook Settings', 'HourlyReports', 'Configs/BAIronwoodSettings.ini'), True); SavedSessionSummary := StrToBoolDef(ReadINI(Username + ' Ironwood Webhook Settings', 'SessionSummary', 'Configs/BAIronwoodSettings.ini'), True); SavedEnableWorldHopping := StrToBoolDef(ReadINI(Username + ' Ironwood Script Settings', 'EnableWorldHopping', 'Configs/BAIronwoodSettings.ini'), False); SavedWorldHopInterval := StrToIntDef(ReadINI(Username + ' Ironwood Script Settings', 'WorldHopInterval', 'Configs/BAIronwoodSettings.ini'), 45); SavedPreferPopulatedTrees := StrToBoolDef(ReadINI(Username + ' Ironwood Script Settings', 'PreferPopulatedTrees', 'Configs/BAIronwoodSettings.ini'), True); SavedPowerCut := StrToBoolDef(ReadINI(Username + ' Ironwood Script Settings', 'PowerCut', 'Configs/BAIronwoodSettings.ini'), False); SavedOverrideBreaks := StrToBoolDef(ReadINI(Username + ' Ironwood Antiban Manager', 'OverrideBreaks', 'Configs/BAIronwoodSettings.ini'), False); SavedCustomBreakInterval := StrToIntDef(ReadINI(Username + ' Ironwood Antiban Manager', 'CustomBreakInterval', 'Configs/BAIronwoodSettings.ini'), 60); SavedCustomBreakDuration := StrToIntDef(ReadINI(Username + ' Ironwood Antiban Manager', 'CustomBreakDuration', 'Configs/BAIronwoodSettings.ini'), 5); if WLSettings.Has('max_actions') then SavedMaxActions := WLSettings.GetInt('max_actions') else SavedMaxActions := 0; if WLSettings.Has('max_time') then SavedMaxTime := WLSettings.GetInt('max_time') else SavedMaxTime := 0; AIChatbot.Setup('Gemini', ''); AIChatbot.Enabled := False; AIChatbot.LoadSettings(); AIChatbot.SetScriptDefaults('chopping ironwood'); Self.Setup('BigAussies Ironwood Chopper', [IW_GUI_W, IW_GUI_H], False); Self.BuildShell( SavedPreferPopulatedTrees, SavedPowerCut, SavedWebhookURL, SavedEnableWebhooks, SavedHourly, SavedSessionSummary, SavedEnableWorldHopping, SavedOverrideBreaks, SavedWorldHopInterval, SavedCustomBreakInterval, SavedCustomBreakDuration, SavedMaxActions, SavedMaxTime ); inherited; end; var Config: TConfig; {$ENDIF} begin Discord.Setup(); {$IFDEF SCRIPT_GUI} Sync(@Config.Run); {$ENDIF} IronwoodBot.Run(WLSettings.MaxActions, WLSettings.MaxTime); end.