440 lines
No EOL
17 KiB
C#
440 lines
No EOL
17 KiB
C#
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<Dictionary<short, AccountWarehouseItemDto>> _cachedWarehouseItems;
|
|
|
|
private readonly Dictionary<long, Dictionary<short, (AccountWarehouseItemDto dto, bool remove)>> _itemChanges = new();
|
|
private readonly SemaphoreSlim _itemChangesSemaphore = new(1, 1);
|
|
private readonly ConcurrentDictionary<long, SemaphoreSlim> _warehouseLocks = new();
|
|
|
|
public AccountWarehouseManager(IAccountWarehouseItemDao accountWarehouseItemDao, ILongKeyCachedRepository<Dictionary<short, AccountWarehouseItemDto>> cachedWarehouseItems)
|
|
{
|
|
_accountWarehouseItemDao = accountWarehouseItemDao;
|
|
_cachedWarehouseItems = cachedWarehouseItems;
|
|
}
|
|
|
|
public async Task<IEnumerable<AccountWarehouseItemDto>> GetWarehouse(long accountId)
|
|
{
|
|
SemaphoreSlim accountLock = GetAccountLock(accountId);
|
|
await accountLock.WaitAsync();
|
|
try
|
|
{
|
|
return (await GetAccountWarehouse(accountId))?.Values;
|
|
}
|
|
finally
|
|
{
|
|
accountLock.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<AccountWarehouseItemDto> 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<AddWarehouseItemResult> 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<short, AccountWarehouseItemDto> 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<WithdrawWarehouseItemResult> 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<short, AccountWarehouseItemDto> 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<ItemInstanceDTO>();
|
|
newItemInstance.Amount = amount;
|
|
|
|
return new WithdrawWarehouseItemResult
|
|
{
|
|
Success = true,
|
|
UpdatedItem = alreadyExistentItem.ItemInstance.Amount == 0 ? null : alreadyExistentItem,
|
|
WithdrawnItem = newItemInstance
|
|
};
|
|
}
|
|
finally
|
|
{
|
|
accountLock.Release();
|
|
}
|
|
}
|
|
|
|
public async Task<MoveWarehouseItemResult> 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<short, AccountWarehouseItemDto> 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<ItemInstanceDTO>(),
|
|
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<short, (AccountWarehouseItemDto dto, bool remove)> warehouseChanges) in _itemChanges)
|
|
{
|
|
List<AccountWarehouseItemDto> itemsToSave = new();
|
|
List<AccountWarehouseItemDto> 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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Not to be used outside SemaphoreSlim
|
|
/// </summary>
|
|
private void SetItemChange(AccountWarehouseItemDto dto, bool remove)
|
|
{
|
|
_itemChanges.GetOrSetDefault(dto.AccountId, new Dictionary<short, (AccountWarehouseItemDto dto, bool remove)>())[dto.Slot] = (dto, remove);
|
|
}
|
|
|
|
private async Task<Dictionary<short, AccountWarehouseItemDto>> GetAccountWarehouse(long accountId)
|
|
{
|
|
Dictionary<short, AccountWarehouseItemDto> cachedItems = _cachedWarehouseItems.Get(accountId);
|
|
if (cachedItems != null)
|
|
{
|
|
return cachedItems;
|
|
}
|
|
|
|
cachedItems = (await _accountWarehouseItemDao.GetByAccountIdAsync(accountId))?.ToDictionary(x => x.Slot);
|
|
_cachedWarehouseItems.Set(accountId, cachedItems ?? new Dictionary<short, AccountWarehouseItemDto>(), LifeTime);
|
|
return cachedItems;
|
|
}
|
|
}
|
|
} |