using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Mapster; using Microsoft.Extensions.Hosting; using PhoenixLib.Caching; using PhoenixLib.Logging; using WingsAPI.Data.Account; using WingsAPI.Data.Warehouse; using WingsEmu.Core.Extensions; using WingsEmu.DTOs.Items; namespace DatabaseServer.Managers { public class AccountWarehouseManager : BackgroundService, IAccountWarehouseManager { private static readonly TimeSpan Interval = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.DbServerSaveIntervalMinutes) ?? "5")); private static readonly TimeSpan LifeTime = Interval * 3; private readonly IAccountWarehouseItemDao _accountWarehouseItemDao; private readonly ILongKeyCachedRepository> _cachedWarehouseItems; private readonly Dictionary> _itemChanges = new(); private readonly SemaphoreSlim _itemChangesSemaphore = new(1, 1); private readonly ConcurrentDictionary _warehouseLocks = new(); public AccountWarehouseManager(IAccountWarehouseItemDao accountWarehouseItemDao, ILongKeyCachedRepository> cachedWarehouseItems) { _accountWarehouseItemDao = accountWarehouseItemDao; _cachedWarehouseItems = cachedWarehouseItems; } public async Task> GetWarehouse(long accountId) { SemaphoreSlim accountLock = GetAccountLock(accountId); await accountLock.WaitAsync(); try { return (await GetAccountWarehouse(accountId))?.Values; } finally { accountLock.Release(); } } public async Task GetWarehouseItem(long accountId, short slot) { SemaphoreSlim accountLock = GetAccountLock(accountId); await accountLock.WaitAsync(); try { return (await GetAccountWarehouse(accountId))?.GetValueOrDefault(slot); } finally { accountLock.Release(); } } public async Task AddWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToAdd) { long accountId = warehouseItemDtoToAdd.AccountId; if (warehouseItemDtoToAdd.ItemInstance.Amount < 1 || 999 < warehouseItemDtoToAdd.ItemInstance.Amount) { return new AddWarehouseItemResult { Success = false }; } SemaphoreSlim accountLock = GetAccountLock(accountId); await accountLock.WaitAsync(); try { Dictionary familyWarehouse = await GetAccountWarehouse(accountId); if (familyWarehouse == null) { return new AddWarehouseItemResult { Success = false }; } AccountWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToAdd.Slot); if (alreadyExistentItem == null) { familyWarehouse[warehouseItemDtoToAdd.Slot] = warehouseItemDtoToAdd; await SetItemChangeWithLock(warehouseItemDtoToAdd, false); return new AddWarehouseItemResult { Success = true, UpdatedItem = warehouseItemDtoToAdd }; } if (warehouseItemDtoToAdd.ItemInstance.ItemVNum != alreadyExistentItem.ItemInstance.ItemVNum) { return new AddWarehouseItemResult { Success = false }; } if (warehouseItemDtoToAdd.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || alreadyExistentItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || warehouseItemDtoToAdd.ItemInstance.Amount + alreadyExistentItem.ItemInstance.Amount > 999) { return new AddWarehouseItemResult { Success = false }; } alreadyExistentItem.ItemInstance.Amount += warehouseItemDtoToAdd.ItemInstance.Amount; await SetItemChangeWithLock(alreadyExistentItem, false); return new AddWarehouseItemResult { Success = true, UpdatedItem = alreadyExistentItem }; } finally { accountLock.Release(); } } public async Task WithdrawWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToWithdraw, int amount) { long accountId = warehouseItemDtoToWithdraw.AccountId; if (amount < 1 || 999 < amount) { return new WithdrawWarehouseItemResult { Success = false }; } SemaphoreSlim accountLock = GetAccountLock(accountId); await accountLock.WaitAsync(); try { Dictionary familyWarehouse = await GetAccountWarehouse(accountId); if (familyWarehouse == null) { return new WithdrawWarehouseItemResult { Success = false }; } AccountWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToWithdraw.Slot); if (alreadyExistentItem == null || alreadyExistentItem.ItemInstance.Amount < amount) { return new WithdrawWarehouseItemResult { Success = false }; } alreadyExistentItem.ItemInstance.Amount -= amount; bool toRemove = alreadyExistentItem.ItemInstance.Amount == 0; if (toRemove) { familyWarehouse.Remove(warehouseItemDtoToWithdraw.Slot); } await SetItemChangeWithLock(alreadyExistentItem, toRemove); ItemInstanceDTO newItemInstance = alreadyExistentItem.ItemInstance.Adapt(); newItemInstance.Amount = amount; return new WithdrawWarehouseItemResult { Success = true, UpdatedItem = alreadyExistentItem.ItemInstance.Amount == 0 ? null : alreadyExistentItem, WithdrawnItem = newItemInstance }; } finally { accountLock.Release(); } } public async Task MoveWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot) { long accountId = warehouseItemDtoToMove.AccountId; if (amount < 1 || 999 < amount) { return new MoveWarehouseItemResult { Success = false }; } SemaphoreSlim accountLock = GetAccountLock(accountId); await accountLock.WaitAsync(); try { Dictionary familyWarehouse = await GetAccountWarehouse(accountId); if (familyWarehouse == null) { return new MoveWarehouseItemResult { Success = false }; } AccountWarehouseItemDto toMoveItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToMove.Slot); AccountWarehouseItemDto toMergeItem = familyWarehouse.GetValueOrDefault(newSlot); if (toMoveItem == null || toMoveItem.ItemInstance.Amount < amount) { return new MoveWarehouseItemResult { Success = false }; } if (toMergeItem == null) { if (amount == toMoveItem.ItemInstance.Amount) { familyWarehouse.Remove(toMoveItem.Slot); await SetItemChangeWithLock(new AccountWarehouseItemDto { AccountId = accountId, Slot = toMoveItem.Slot }, true); toMoveItem.Slot = newSlot; familyWarehouse[toMoveItem.Slot] = toMoveItem; await SetItemChangeWithLock(toMoveItem, false); return new MoveWarehouseItemResult { Success = true, OldItem = null, NewItem = toMoveItem }; } toMoveItem.ItemInstance.Amount -= amount; await SetItemChangeWithLock(toMoveItem, false); var newItem = new AccountWarehouseItemDto { AccountId = accountId, ItemInstance = toMoveItem.ItemInstance.Adapt(), Slot = newSlot }; newItem.ItemInstance.Amount = amount; familyWarehouse[newItem.Slot] = newItem; await SetItemChangeWithLock(newItem, false); return new MoveWarehouseItemResult { Success = true, OldItem = toMoveItem, NewItem = newItem }; } if (toMoveItem.ItemInstance.ItemVNum != toMergeItem.ItemInstance.ItemVNum) { toMergeItem.Slot = toMoveItem.Slot; toMoveItem.Slot = newSlot; familyWarehouse[toMoveItem.Slot] = toMoveItem; await SetItemChangeWithLock(toMoveItem, false); familyWarehouse[toMergeItem.Slot] = toMergeItem; await SetItemChangeWithLock(toMergeItem, false); return new MoveWarehouseItemResult { Success = true, OldItem = toMergeItem, NewItem = toMoveItem }; } if (toMoveItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || toMergeItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || amount + toMergeItem.ItemInstance.Amount > 999) { return new MoveWarehouseItemResult { Success = false }; } toMoveItem.ItemInstance.Amount -= amount; toMergeItem.ItemInstance.Amount += amount; bool toRemove = toMoveItem.ItemInstance.Amount == 0; if (toRemove) { familyWarehouse.Remove(toMoveItem.Slot); } await SetItemChangeWithLock(toMoveItem, toRemove); await SetItemChangeWithLock(toMergeItem, false); return new MoveWarehouseItemResult { Success = true, OldItem = toRemove ? null : toMoveItem, NewItem = toMergeItem }; } finally { accountLock.Release(); } } public async Task FlushWarehouseSaves() { if (_itemChanges.Count < 1) { return; } await _itemChangesSemaphore.WaitAsync(); try { List<(AccountWarehouseItemDto dto, bool remove)> unsavedChanges = new(); var globalWatch = Stopwatch.StartNew(); foreach ((long accountId, Dictionary warehouseChanges) in _itemChanges) { List itemsToSave = new(); List itemsToRemove = new(); foreach ((short _, (AccountWarehouseItemDto dto, bool remove)) in warehouseChanges) { (remove ? itemsToRemove : itemsToSave).Add(dto); } if (itemsToSave.Count > 0) { try { int countSavedItems = await _accountWarehouseItemDao.SaveAsync(itemsToSave); Log.Warn($"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Saved {countSavedItems.ToString()} warehouseItems"); } catch (Exception e) { Log.Error( $"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Error while trying to save {itemsToSave.Count.ToString()} warehouseItems. Re-queueing. ", e); unsavedChanges.AddRange(itemsToSave.Select(x => (x, false))); } } if (itemsToRemove.Count < 1) { continue; } try { await _accountWarehouseItemDao.DeleteAsync(itemsToRemove); Log.Warn($"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Removed (at maximum) {itemsToRemove.Count.ToString()} warehouseItems"); } catch (Exception e) { Log.Error( $"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Error while trying to remove {itemsToRemove.Count.ToString()} warehouseItems. Re-queueing. ", e); unsavedChanges.AddRange(itemsToRemove.Select(x => (x, true))); } } globalWatch.Stop(); Log.Debug($"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES] Saving all warehouses took {globalWatch.ElapsedMilliseconds.ToString()}ms"); _itemChanges.Clear(); foreach ((AccountWarehouseItemDto dto, bool remove) in unsavedChanges) { SetItemChange(dto, remove); } } finally { _itemChangesSemaphore.Release(); } } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await FlushWarehouseSaves(); await Task.Delay(Interval, stoppingToken); } } private SemaphoreSlim GetAccountLock(long accountId) => _warehouseLocks.GetOrAdd(accountId, new SemaphoreSlim(1, 1)); private async Task SetItemChangeWithLock(AccountWarehouseItemDto dto, bool remove) { await _itemChangesSemaphore.WaitAsync(); try { SetItemChange(dto, remove); } finally { _itemChangesSemaphore.Release(); } } /// /// Not to be used outside SemaphoreSlim /// private void SetItemChange(AccountWarehouseItemDto dto, bool remove) { _itemChanges.GetOrSetDefault(dto.AccountId, new Dictionary())[dto.Slot] = (dto, remove); } private async Task> GetAccountWarehouse(long accountId) { Dictionary cachedItems = _cachedWarehouseItems.Get(accountId); if (cachedItems != null) { return cachedItems; } cachedItems = (await _accountWarehouseItemDao.GetByAccountIdAsync(accountId))?.ToDictionary(x => x.Slot); _cachedWarehouseItems.Set(accountId, cachedItems ?? new Dictionary(), LifeTime); return cachedItems; } } }