using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Mapster; using PhoenixLib.Caching; using PhoenixLib.Logging; using WingsAPI.Communication.Families; using WingsAPI.Data.Families; using WingsAPI.Game.Extensions.Families; using WingsAPI.Packets.Enums.Families; using WingsEmu.Core.Extensions; using WingsEmu.DTOs.Items; namespace FamilyServer.Managers { public class FamilyWarehouseManager : IFamilyWarehouseManager { private static readonly TimeSpan LifeTime = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.FamilyServerSaveIntervalMinutes) ?? "5") * 3); private readonly ILongKeyCachedRepository> _cachedFamilyItems; private readonly SemaphoreSlim _familyLock = new(1, 1); private readonly IFamilyService _familyService; private readonly IFamilyWarehouseItemDao _familyWarehouseItemDao; private readonly FamilyWarehouseLogManager _familyWarehouseLogManager; private readonly Dictionary> _itemChanges = new(); private readonly SemaphoreSlim _itemChangesSemaphore = new(1, 1); public FamilyWarehouseManager(IFamilyWarehouseItemDao familyWarehouseItemDao, ILongKeyCachedRepository> cachedFamilyItems, IFamilyService familyService, FamilyWarehouseLogManager familyWarehouseLogManager) { _familyWarehouseItemDao = familyWarehouseItemDao; _cachedFamilyItems = cachedFamilyItems; _familyService = familyService; _familyWarehouseLogManager = familyWarehouseLogManager; } public async Task> GetWarehouseLogs(long familyId, long? characterId = null) { if (!await CheckLogHistoryPermission(familyId, characterId)) { return null; } return await _familyWarehouseLogManager.GetLogs(familyId); } public async Task> GetWarehouse(long familyId, long? characterId = null) { if (!await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put)) { return null; } await _familyLock.WaitAsync(); try { return (await GetFamilyWarehouse(familyId))?.Values; } finally { _familyLock.Release(); } } public async Task GetWarehouseItem(long familyId, short slot, long? characterId = null) { if (!await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put)) { return null; } await _familyLock.WaitAsync(); try { return (await GetFamilyWarehouse(familyId))?.GetValueOrDefault(slot); } finally { _familyLock.Release(); } } public async Task AddWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToAdd, long? characterId = null, string characterName = null) { long familyId = warehouseItemDtoToAdd.FamilyId; if (warehouseItemDtoToAdd.ItemInstance.Amount is < 1 or > 999 || !await CheckSlot(familyId, warehouseItemDtoToAdd.Slot) || !await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put)) { return new AddWarehouseItemResult { Success = false }; } await _familyLock.WaitAsync(); try { Dictionary familyWarehouse = await GetFamilyWarehouse(familyId); if (familyWarehouse == null) { return new AddWarehouseItemResult { Success = false }; } FamilyWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToAdd.Slot); if (alreadyExistentItem == null) { familyWarehouse[warehouseItemDtoToAdd.Slot] = warehouseItemDtoToAdd; await SetItemChangeWithLock(warehouseItemDtoToAdd, false); await AddLog(familyId, warehouseItemDtoToAdd.ItemInstance, FamilyWarehouseLogEntryType.List, characterId, characterName); 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); await AddLog(familyId, warehouseItemDtoToAdd.ItemInstance, FamilyWarehouseLogEntryType.List, characterId, characterName); return new AddWarehouseItemResult { Success = true, UpdatedItem = alreadyExistentItem }; } finally { _familyLock.Release(); } } public async Task WithdrawWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToWithdraw, int amount, long? characterId = null, string characterName = null) { long familyId = warehouseItemDtoToWithdraw.FamilyId; if (amount is < 1 or > 999 || !await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.PutAndWithdraw)) { return new WithdrawWarehouseItemResult { Success = false }; } await _familyLock.WaitAsync(); try { Dictionary familyWarehouse = await GetFamilyWarehouse(familyId); if (familyWarehouse == null) { return new WithdrawWarehouseItemResult { Success = false }; } FamilyWarehouseItemDto 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; await AddLog(familyId, newItemInstance, FamilyWarehouseLogEntryType.Withdraw, characterId, characterName); return new WithdrawWarehouseItemResult { Success = true, UpdatedItem = alreadyExistentItem.ItemInstance.Amount == 0 ? null : alreadyExistentItem, WithdrawnItem = newItemInstance }; } finally { _familyLock.Release(); } } public async Task MoveWarehouseItem(FamilyWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot, long? characterId = null) { long familyId = warehouseItemDtoToMove.FamilyId; if (amount is < 1 or > 999 || !await CheckSlot(familyId, warehouseItemDtoToMove.Slot, newSlot) || !await CheckPutWithdrawPermission(familyId, characterId, FamilyWarehouseAuthorityType.Put)) { return new MoveWarehouseItemResult { Success = false }; } await _familyLock.WaitAsync(); try { Dictionary familyWarehouse = await GetFamilyWarehouse(familyId); if (familyWarehouse == null) { return new MoveWarehouseItemResult { Success = false }; } FamilyWarehouseItemDto toMoveItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToMove.Slot); FamilyWarehouseItemDto 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 FamilyWarehouseItemDto { FamilyId = familyId, 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 FamilyWarehouseItemDto { FamilyId = familyId, 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 { _familyLock.Release(); } } public async Task FlushWarehouseSaves() { if (_itemChanges.Count < 1) { return; } await _itemChangesSemaphore.WaitAsync(); try { List<(FamilyWarehouseItemDto dto, bool remove)> unsavedChanges = new(); var globalWatch = Stopwatch.StartNew(); foreach ((long familyId, Dictionary warehouseChanges) in _itemChanges) { List itemsToSave = new(); List itemsToRemove = new(); foreach ((short _, (FamilyWarehouseItemDto dto, bool remove)) in warehouseChanges) { (remove ? itemsToRemove : itemsToSave).Add(dto); } if (itemsToSave.Count > 0) { try { int countSavedItems = await _familyWarehouseItemDao.SaveAsync(itemsToSave); Log.Warn($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Saved {countSavedItems.ToString()} warehouseItems"); } catch (Exception e) { Log.Error( $"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.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 _familyWarehouseItemDao.DeleteAsync(itemsToRemove); Log.Warn($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Removed (at maximum) {itemsToRemove.Count.ToString()} warehouseItems"); } catch (Exception e) { Log.Error( $"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES][FAMILY_ID: {familyId.ToString()}] Error while trying to remove {itemsToRemove.Count.ToString()} warehouseItems. Re-queueing. ", e); unsavedChanges.AddRange(itemsToRemove.Select(x => (x, true))); } } globalWatch.Stop(); Log.Debug($"[FAMILY_WAREHOUSE_MANAGER][FLUSH_SAVES] Saving all warehouses took {globalWatch.ElapsedMilliseconds.ToString()}ms"); _itemChanges.Clear(); foreach ((FamilyWarehouseItemDto dto, bool remove) in unsavedChanges) { SetItemChange(dto, remove); } } finally { _itemChangesSemaphore.Release(); } await _familyWarehouseLogManager.FlushLogSaves(); } private async Task CheckSlot(long familyId, short slot, short? slot2 = null) { FamilyIdResponse family = await _familyService.GetFamilyByIdAsync(new FamilyIdRequest { FamilyId = familyId }); if (family == null) { return false; } short warehouseCapacity = family.Family.Upgrades.UpgradeValues.GetValueOrDefault(FamilyUpgradeType.INCREASE_FAMILY_WAREHOUSE); if (slot < 0 || warehouseCapacity <= slot) { return false; } if (slot2 < 0 || warehouseCapacity <= slot2) { return false; } return true; } private async Task CheckPutWithdrawPermission(long familyId, long? characterId, FamilyWarehouseAuthorityType authorityRequested) { if (characterId == null) { return true; } FamilyIdResponse family = await _familyService.GetFamilyByIdAsync(new FamilyIdRequest { FamilyId = familyId }); FamilyMembershipDto member = family?.Members.Find(x => x.CharacterId == characterId); return member != null && member.CheckPutWithdrawPermission(family.Family, authorityRequested); } private async Task CheckLogHistoryPermission(long familyId, long? characterId) { if (characterId == null) { return true; } FamilyIdResponse family = await _familyService.GetFamilyByIdAsync(new FamilyIdRequest { FamilyId = familyId }); FamilyMembershipDto member = family?.Members.Find(x => x.CharacterId == characterId); return member != null && member.CheckLogHistoryPermission(family.Family); } private async Task SetItemChangeWithLock(FamilyWarehouseItemDto dto, bool remove) { await _itemChangesSemaphore.WaitAsync(); try { SetItemChange(dto, remove); } finally { _itemChangesSemaphore.Release(); } } /// /// Not to be used outside SemaphoreSlim /// private void SetItemChange(FamilyWarehouseItemDto dto, bool remove) { _itemChanges.GetOrSetDefault(dto.FamilyId, new Dictionary())[dto.Slot] = (dto, remove); } private async Task> GetFamilyWarehouse(long familyId) { Dictionary cachedItems = _cachedFamilyItems.Get(familyId); if (cachedItems != null) { return cachedItems; } cachedItems = (await _familyWarehouseItemDao.GetByFamilyIdAsync(familyId))?.ToDictionary(x => x.Slot); _cachedFamilyItems.Set(familyId, cachedItems ?? new Dictionary(), LifeTime); return cachedItems; } private async Task AddLog(long familyId, ItemInstanceDTO item, FamilyWarehouseLogEntryType logEntryType, long? characterId, string characterName) { await _familyWarehouseLogManager.AddLog(familyId, new FamilyWarehouseLogEntryDto { CharacterId = characterId ?? -1, CharacterName = characterName, DateOfLog = DateTime.UtcNow, Type = logEntryType, ItemVnum = item.ItemVNum, Amount = item.Amount }); } } }