Skip to content

Commit

Permalink
More Batch Trade Logic for BDSP/SV
Browse files Browse the repository at this point in the history
  • Loading branch information
bdawg1989 committed Oct 30, 2024
1 parent 8355dd3 commit 6235dcd
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 118 deletions.
32 changes: 28 additions & 4 deletions SysBot.Pokemon.Discord/Helpers/DiscordTradeNotifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using PKHeX.Core;
using PKHeX.Core.AutoMod;
using PKHeX.Drawing.PokeSprite;
using SysBot.Pokemon.Helpers;
using System;
using System.Collections.Generic;
using System.Drawing;
Expand Down Expand Up @@ -112,10 +113,33 @@ public void TradeFinished(PokeRoutineExecutor<T> routine, PokeTradeDetail<T> inf
{
OnFinish?.Invoke(routine);
var tradedToUser = Data.Species;
var message = tradedToUser != 0 ? (info.IsMysteryMon ? "Enjoy your **Mystery Pokémon**!" : info.IsMysteryEgg ? "Enjoy your **Mystery Egg**!" : $"Enjoy your **{(Species)tradedToUser}**!") : "Trade finished!";
EmbedHelper.SendTradeFinishedEmbedAsync(Trader, message, Data, info.IsMysteryMon, info.IsMysteryEgg).ConfigureAwait(false);
if (result.Species != 0 && Hub.Config.Discord.ReturnPKMs)
Trader.SendPKMAsync(result, "Here's what you traded me!").ConfigureAwait(false);

if (info.TotalBatchTrades > 1)
{
// For batch trades, just send each Pokemon
if (Hub.Config.Discord.ReturnPKMs && result.Species != 0)
Trader.SendPKMAsync(result, "Here's what you traded me!").ConfigureAwait(false);

// Only send completion message on last trade
if (info.BatchTradeNumber == info.TotalBatchTrades)
{
var message = tradedToUser != 0 ?
(info.IsMysteryEgg ? "Enjoy your **Mystery Eggs**!" : $"Enjoy your **{(Species)tradedToUser}** and other Pokémon!") :
"Batch trades finished!";

EmbedHelper.SendTradeFinishedEmbedAsync(Trader, message, Data, info.IsMysteryMon, info.IsMysteryEgg).ConfigureAwait(false);
}
}
else
{
// Original single trade logic
var message = tradedToUser != 0 ?
(info.IsMysteryEgg ? "Enjoy your **Mystery Egg**!" : $"Enjoy your **{(Species)tradedToUser}**!") :
"Trade finished!";
EmbedHelper.SendTradeFinishedEmbedAsync(Trader, message, Data, info.IsMysteryMon, info.IsMysteryEgg).ConfigureAwait(false);
if (result.Species != 0 && Hub.Config.Discord.ReturnPKMs)
Trader.SendPKMAsync(result, "Here's what you traded me!").ConfigureAwait(false);
}
}

public void SendNotification(PokeRoutineExecutor<T> routine, PokeTradeDetail<T> info, string message)
Expand Down
3 changes: 2 additions & 1 deletion SysBot.Pokemon/Actions/PokeRoutineExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using static SysBot.Base.SwitchButton;
using System.Threading;
using System.Threading.Tasks;
using SysBot.Pokemon.Helpers;

namespace SysBot.Pokemon;

Expand All @@ -15,7 +16,7 @@ public abstract class PokeRoutineExecutor<T>(IConsoleBotManaged<IConsoleConnecti
where T : PKM, new()
{
private const ulong dmntID = 0x010000000000000d;

public readonly BatchTradeTracker<T> _batchTracker = new();
// Check if either Tesla or dmnt are active if the sanity check for Trainer Data fails, as these are common culprits.
private const ulong ovlloaderID = 0x420000000007e51a;

Expand Down
136 changes: 84 additions & 52 deletions SysBot.Pokemon/BDSP/BotTrade/PokeTradeBotBS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,43 +295,65 @@ private bool GetNextBatchTrade(PokeTradeDetail<PB8> currentTrade, out PokeTradeD
{
nextDetail = null;
var batchQueue = Hub.Queues.GetQueue(PokeRoutineType.Batch);
Log($"Searching for next trade after {currentTrade.BatchTradeNumber}/{currentTrade.TotalBatchTrades}");

Log($"Searching for next trade after {currentTrade.BatchTradeNumber}/{currentTrade.TotalBatchTrades} (ID: {currentTrade.UniqueTradeID})");
// Get all trades for this user
var userTrades = batchQueue.Queue.GetSnapshot()
.Select(x => x.Value)
.Where(x => x.Trainer.ID == currentTrade.Trainer.ID)
.OrderBy(x => x.BatchTradeNumber)
.ToList();

// Get a snapshot of all trades without dequeuing
var allTrades = batchQueue.Queue.GetSnapshot();

// Log the trades in the queue
foreach (var kvp in allTrades)
// Log what we found
foreach (var trade in userTrades)
{
var trade = kvp.Value;
Log($"Found trade in queue: #{trade.BatchTradeNumber}/{trade.TotalBatchTrades} (ID: {trade.UniqueTradeID}) for trainer {trade.Trainer.TrainerName}");
Log($"Found trade in queue: #{trade.BatchTradeNumber}/{trade.TotalBatchTrades} for trainer {trade.Trainer.TrainerName}");
}

// Find the next trade that matches our UniqueTradeID
var nextTrade = allTrades
.Where(x => x.Value.UniqueTradeID == currentTrade.UniqueTradeID && x.Value.BatchTradeNumber > currentTrade.BatchTradeNumber)
.OrderBy(x => x.Value.BatchTradeNumber)
.FirstOrDefault();
// Get the next sequential trade
nextDetail = userTrades.FirstOrDefault(x => x.BatchTradeNumber == currentTrade.BatchTradeNumber + 1);

if (nextTrade.Value != null)
if (nextDetail != null)
{
Log($"Found next trade {nextTrade.Value.BatchTradeNumber}/{nextTrade.Value.TotalBatchTrades}");
nextDetail = nextTrade.Value;
Log($"Selected next trade {nextDetail.BatchTradeNumber}/{nextDetail.TotalBatchTrades}");
return true;
}

Log($"No more trades found for batch ID: {currentTrade.UniqueTradeID}");
Log("No more trades found for this user");
return false;
}

private void CleanupAllBatchTradesFromQueue(PokeTradeDetail<PB8> detail)
{
var result = Hub.Queues.Info.ClearTrade(detail.Trainer.ID);
var batchQueue = Hub.Queues.GetQueue(PokeRoutineType.Batch);

// Clear any remaining trades for this batch from the queue
var remainingTrades = batchQueue.Queue.GetSnapshot()
.Where(x => x.Value.Trainer.ID == detail.Trainer.ID &&
x.Value.UniqueTradeID == detail.UniqueTradeID)
.ToList();

foreach (var trade in remainingTrades)
{
batchQueue.Queue.Remove(trade.Value);
}

Log($"Cleaned up batch trades for TrainerID: {detail.Trainer.ID}, UniqueTradeID: {detail.UniqueTradeID}");
}

private async Task HandleAbortedBatchTrade(PokeTradeDetail<PB8> detail, PokeRoutineType type, uint priority, PokeTradeResult result, CancellationToken token)
{
if (detail.TotalBatchTrades > 1)
{
// Send notification once before cleanup
detail.SendNotification(this, $"Trade {detail.BatchTradeNumber}/{detail.TotalBatchTrades} failed. Canceling remaining batch trades.");
Hub.Queues.Info.ClearTrade(detail.Trainer.ID);

CleanupAllBatchTradesFromQueue(detail);

// Mark this specific trade as canceled
detail.TradeCanceled(this, result);

await EnsureOutsideOfUnionRoom(token).ConfigureAwait(false);
}
else
Expand Down Expand Up @@ -1151,45 +1173,68 @@ private async Task<PokeTradeResult> PerformBatchTrade(SAV8BS sav, PokeTradeDetai
var received = await ReadPokemon(BoxStartOffset, BoxFormatSlotSize, token).ConfigureAwait(false);
if (SearchUtil.HashByDetails(received) == SearchUtil.HashByDetails(toSend) && received.Checksum == toSend.Checksum)
{
if (completedTrades > 0)
poke.SendNotification(this, "Trade not completed. Remaining batch trades canceled.");
poke.SendNotification(this, "Trade not completed properly.");
await EnsureOutsideOfUnionRoom(token).ConfigureAwait(false);
return PokeTradeResult.TrainerTooSlow;
}

completedTrades++;
UpdateCountsAndExport(poke, received, toSend);
LogSuccessfulTrades(poke, trainerNID, tradePartner.TrainerName);
completedTrades++;

// Complete this individual trade
poke.TradeFinished(this, received);
Hub.Queues.CompleteTrade(this, poke);
// Store received Pokemon
_batchTracker.AddReceivedPokemon(poke.Trainer.ID, received);

if (completedTrades < startingDetail.TotalBatchTrades)
if (completedTrades == startingDetail.TotalBatchTrades)
{
// Get next trade in batch
if (GetNextBatchTrade(poke, out var nextDetail))
// Get all collected Pokemon before cleaning anything up
var allReceived = _batchTracker.GetReceivedPokemon(poke.Trainer.ID);

// First send notification that trades are complete
poke.SendNotification(this, "All batch trades completed! Thank you for trading!");

// Then finish each trade with the corresponding received Pokemon
if (Hub.Config.Discord.ReturnPKMs)
{
if (nextDetail == null)
foreach (var pokemon in allReceived)
{
poke.SendNotification(this, "Error in batch sequence. Ending trades.");
await EnsureOutsideOfUnionRoom(token).ConfigureAwait(false);
return PokeTradeResult.Success;
poke.TradeFinished(this, pokemon); // This sends each Pokemon back to the user
}
}

// Finally do cleanup
Hub.Queues.CompleteTrade(this, poke);
CleanupAllBatchTradesFromQueue(poke);
_batchTracker.ClearReceivedPokemon(poke.Trainer.ID);
break;
}

// Setup next trade
poke = nextDetail;
poke.SendNotification(this, $"Trade {completedTrades} completed! Preparing next Pokémon ({nextDetail.BatchTradeNumber}/{nextDetail.TotalBatchTrades}). Please wait in the trade screen!");
await Task.Delay(3_000, token).ConfigureAwait(false);
continue;
if (GetNextBatchTrade(poke, out var nextDetail))
{
if (nextDetail == null)
{
poke.SendNotification(this, "Error in batch sequence. Ending trades.");
await EnsureOutsideOfUnionRoom(token).ConfigureAwait(false);
return PokeTradeResult.Success;
}

poke.SendNotification(this, $"Trade {completedTrades} completed! Preparing your next Pokémon ({nextDetail.BatchTradeNumber}/{nextDetail.TotalBatchTrades}). Please wait in the trade screen!");
poke = nextDetail;

await Click(A, 1_000, token).ConfigureAwait(false);
if (poke.TradeData.Species != 0)
{
await SetBoxPokemonAbsolute(BoxStartOffset, poke.TradeData, token, sav).ConfigureAwait(false);
}
continue;
}

Hub.Queues.Info.ClearTrade(poke.Trainer.ID);
poke.SendNotification(this, "All batch trades completed! Thank you for trading!");
await Task.Delay(3_000, token).ConfigureAwait(false);
poke.SendNotification(this, "Unable to find the next trade in sequence. Batch trade will be terminated.");
await EnsureOutsideOfUnionRoom(token).ConfigureAwait(false);
return PokeTradeResult.Success;
}

await EnsureOutsideOfUnionRoom(token).ConfigureAwait(false);
return PokeTradeResult.Success;
}

Expand All @@ -1198,9 +1243,6 @@ private async Task PerformTrade(SAV8BS sav, PokeTradeDetail<PB8> detail, PokeRou
PokeTradeResult result;
try
{
// Keep track of the first trade in batch for cleanup purposes
bool isFirstInBatch = detail.BatchTradeNumber == 1;

if (detail.Type == PokeTradeType.Batch)
result = await PerformBatchTrade(sav, detail, token).ConfigureAwait(false);
else
Expand All @@ -1209,16 +1251,6 @@ private async Task PerformTrade(SAV8BS sav, PokeTradeDetail<PB8> detail, PokeRou
if (result == PokeTradeResult.Success)
{
PB8? receivedPokemon = await ReadPokemon(BoxStartOffset, BoxFormatSlotSize, token).ConfigureAwait(false);

// Complete every trade individually
detail.TradeFinished(this, receivedPokemon);
Hub.Queues.CompleteTrade(this, detail);

// Only send completion notification on last trade
if (detail.BatchTradeNumber == detail.TotalBatchTrades)
{
detail.SendNotification(this, "All batch trades completed successfully!");
}
return;
}

Expand Down
72 changes: 52 additions & 20 deletions SysBot.Pokemon/Helpers/BatchTradeTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using System.Collections.Concurrent;
using System;
using System.Linq;

using System.Collections.Generic;
namespace SysBot.Pokemon.Helpers
{
public class BatchTradeTracker<T> where T : PKM, new()
Expand All @@ -11,19 +11,18 @@ namespace SysBot.Pokemon.Helpers
private readonly ConcurrentDictionary<(ulong TrainerId, int UniqueTradeID), string> _activeBatches = new();
private readonly TimeSpan _tradeTimeout = TimeSpan.FromMinutes(5);
private readonly ConcurrentDictionary<(ulong TrainerId, int UniqueTradeID), DateTime> _lastTradeTime = new();
private readonly ConcurrentDictionary<ulong, List<T>> _receivedPokemon = new();
private readonly object _claimLock = new(); // Add this line

public bool CanProcessBatchTrade(PokeTradeDetail<T> trade)
{
if (trade.TotalBatchTrades <= 1)
return true;

CleanupStaleEntries();
var key = (trade.Trainer.ID, trade.UniqueTradeID);

// If _nobodyishome is handling this batch yet, allow it
// If nobody is handling this batch yet, allow it
if (!_activeBatches.ContainsKey(key))
return true;

return true; // Allow all trades from this batch
}

Expand All @@ -34,31 +33,32 @@ public bool TryClaimBatchTrade(PokeTradeDetail<T> trade, string botName)

var key = (trade.Trainer.ID, trade.UniqueTradeID);

// If we already have this batch, make sure it's the same bot
if (_activeBatches.TryGetValue(key, out var existingBot))
lock (_claimLock) // Add this line
{
_lastTradeTime[key] = DateTime.Now;
return botName == existingBot;
}
// If we already have this batch, make sure it's the same bot
if (_activeBatches.TryGetValue(key, out var existingBot))
{
_lastTradeTime[key] = DateTime.Now;
return botName == existingBot;
}

// claim this batch
if (_activeBatches.TryAdd(key, botName))
{
_lastTradeTime[key] = DateTime.Now;
return true;
}
// Try to claim this batch
if (_activeBatches.TryAdd(key, botName))
{
_lastTradeTime[key] = DateTime.Now;
return true;
}

return false;
return false;
} // Add this line
}

public void CompleteBatchTrade(PokeTradeDetail<T> trade)
{
if (trade.TotalBatchTrades <= 1)
return;

var key = (trade.Trainer.ID, trade.UniqueTradeID);
_lastTradeTime[key] = DateTime.Now;

// Only remove tracking when it's the last trade
if (trade.BatchTradeNumber == trade.TotalBatchTrades)
{
Expand All @@ -74,12 +74,44 @@ private void CleanupStaleEntries()
.Where(x => now - x.Value > _tradeTimeout)
.Select(x => x.Key)
.ToList();

foreach (var key in staleKeys)
{
_activeBatches.TryRemove(key, out _);
_lastTradeTime.TryRemove(key, out _);
}
}

public void ClearReceivedPokemon(ulong trainerId)
{
_receivedPokemon.TryRemove(trainerId, out _);
}

public void AddReceivedPokemon(ulong trainerId, T pokemon)
{
if (!_receivedPokemon.ContainsKey(trainerId))
{
var newList = new List<T>();
_receivedPokemon.TryAdd(trainerId, newList);
}
if (_receivedPokemon.TryGetValue(trainerId, out var list))
{
lock (list)
{
list.Add(pokemon);
}
}
}

public List<T> GetReceivedPokemon(ulong trainerId)
{
if (_receivedPokemon.TryGetValue(trainerId, out var list))
{
lock (list)
{
return new List<T>(list); // Return copy of list
}
}
return new List<T>();
}
}
}
Loading

0 comments on commit 6235dcd

Please sign in to comment.