content: package emu.grasscutter.game.systems;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Optional;
  import java.util.stream.Stream;
  
  import emu.grasscutter.Grasscutter;
  import emu.grasscutter.data.GameData;
  import emu.grasscutter.data.common.ItemParamData;
  import emu.grasscutter.data.excels.AvatarPromoteData;
  import emu.grasscutter.data.excels.AvatarSkillDepotData;
  import emu.grasscutter.data.excels.ItemData;
  import emu.grasscutter.data.excels.WeaponPromoteData;
  import emu.grasscutter.game.avatar.Avatar;
  import emu.grasscutter.game.inventory.GameItem;
  import emu.grasscutter.game.inventory.ItemType;
  import emu.grasscutter.game.inventory.MaterialType;
  import emu.grasscutter.game.player.Player;
  import emu.grasscutter.game.props.FightProperty;
  import emu.grasscutter.game.props.ItemUseOp;
  import emu.grasscutter.game.props.ItemUseAction.ItemUseAddExp;
  import emu.grasscutter.game.props.ItemUseAction.ItemUseAddReliquaryExp;
  import emu.grasscutter.game.props.ItemUseAction.ItemUseAddWeaponExp;
  import emu.grasscutter.game.props.ItemUseAction.UseItemParams;
  import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam;
  import emu.grasscutter.net.proto.MaterialInfoOuterClass.MaterialInfo;
  import emu.grasscutter.server.event.player.PlayerUseFoodEvent;
  import emu.grasscutter.server.game.BaseGameSystem;
  import emu.grasscutter.server.game.GameServer;
  import emu.grasscutter.server.packet.send.*;
  import emu.grasscutter.utils.Utils;
  import it.unimi.dsi.fastutil.ints.Int2FloatArrayMap;
  import it.unimi.dsi.fastutil.ints.Int2IntArrayMap;
  import it.unimi.dsi.fastutil.ints.Int2IntMap;
  import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
  import it.unimi.dsi.fastutil.ints.Int2IntRBTreeMap;
  import lombok.val;
  
  public class InventorySystem extends BaseGameSystem {
      private static final Int2IntMap weaponRefundMaterials = new Int2IntArrayMap();
      {
          // Use a sorted map, use exp as key to sort by exp
          // We want to have weaponRefundMaterials as (id, exp) in descending exp order
          var temp = new Int2IntRBTreeMap(Collections.reverseOrder());
          GameData.getItemDataMap().forEach((id, data) -> {
              if (data == null) return;
              if (data.getMaterialType() != MaterialType.MATERIAL_WEAPON_EXP_STONE) return;
              var actions = data.getItemUseActions();
              if (actions == null) return;
              for (var action : actions) {
                  if (action.getItemUseOp() == ItemUseOp.ITEM_USE_ADD_WEAPON_EXP) {
                      temp.putIfAbsent((int) ((ItemUseAddWeaponExp) action).getExp(), (int) id);
                      return;
                  }
              }
          });
          temp.forEach((exp, id) -> weaponRefundMaterials.putIfAbsent((int) id, (int) exp));
      }
  
      public InventorySystem(GameServer server) {
          super(server);
      }
  
      public void lockEquip(Player player, long targetEquipGuid, boolean isLocked) {
          GameItem equip = player.getInventory().getItemByGuid(targetEquipGuid);
  
          if (equip == null || !equip.getItemData().isEquip()) {
              return;
          }
  
          equip.setLocked(isLocked);
          equip.save();
  
          player.sendPacket(new PacketStoreItemChangeNotify(equip));
          player.sendPacket(new PacketSetEquipLockStateRsp(equip));
      }
  
      public void upgradeRelic(Player player, long targetGuid, List<Long> foodRelicList, List<ItemParam> list) {
          GameItem relic = player.getInventory().getItemByGuid(targetGuid);
  
          if (relic == null || relic.getItemType() != ItemType.ITEM_RELIQUARY) {
              return;
          }
  
          int moraCost = 0;
          int expGain = 0;
  
          List<GameItem> foodRelics = new ArrayList<GameItem>();
          for (long guid : foodRelicList) {
              // Add to delete queue
              GameItem food = player.getInventory().getItemByGuid(guid);
              if (food == null || !food.isDestroyable()) {
                  continue;
              }
              // Calculate mora cost
              int exp = food.getItemData().getBaseConvExp();
              moraCost += exp;
              expGain += exp;
              // Feeding artifact with exp already
              if (food.getTotalExp() > 0) {
                  expGain += (food.getTotalExp() * 4) / 5;
              }
              foodRelics.add(food);
          }
          List<ItemParamData> payList = new ArrayList<ItemParamData>();
          for (ItemParam itemParam : list) {
              int amount = itemParam.getCount();  // Previously this capped to inventory amount, but rejecting the payment makes more sense for an invalid order
              int gain = 0;
              var data = GameData.getItemDataMap().get(itemParam.getItemId());
              if (data != null) {
                  var actions = data.getItemUseActions();
                  if (actions != null) {
                      for (var action : actions) {
                          if (action.getItemUseOp() == ItemUseOp.ITEM_USE_ADD_RELIQUARY_EXP) {
                              gain += ((ItemUseAddReliquaryExp) action).getExp();
                          }
                      }
                  }
              }
              gain *= amount;
              expGain += gain;
              moraCost += gain;
              payList.add(new ItemParamData(itemParam.getItemId(), itemParam.getCount()));
          }
  
          // Make sure exp gain is valid
          if (expGain <= 0) {
              return;
          }
  
          // Confirm payment of materials and mora (assume food relics are payable afterwards)
          payList.add(new ItemParamData(202, moraCost));
          if (!player.getInventory().payItems(payList)) {
              return;
          }
  
          // Consume food relics
          player.getInventory().removeItems(foodRelics);
  
          // Implement random rate boost
          int rate = 1;
          int boost = Utils.randomRange(1, 100);
          if (boost == 100) {
              rate = 5;
          } else if (boost <= 9) {
              rate = 2;
          }
          expGain *= rate;
  
          // Now we upgrade
          int level = relic.getLevel();
          int oldLevel = level;
          int exp = relic.getExp();
          int totalExp = relic.getTotalExp();
          int reqExp = GameData.getRelicExpRequired(relic.getItemData().getRankLevel(), level);
          int upgrades = 0;
          List<Integer> oldAppendPropIdList = new ArrayList<>(relic.getAppendPropIdList());
  
          while (expGain > 0 && reqExp > 0 && level < relic.getItemData().getMaxLevel()) {
              // Do calculations
              int toGain = Math.min(expGain, reqExp - exp);
              exp += toGain;
              totalExp += toGain;
              expGain -= toGain;
              // Level up
              if (exp >= reqExp) {
                  // Exp
                  exp = 0;
                  level += 1;
                  // On relic levelup
                  if (relic.getItemData().getAddPropLevelSet() != null && relic.getItemData().getAddPropLevelSet().contains(level)) {
                      upgrades += 1;
                  }
                  // Set req exp
                  reqExp = GameData.getRelicExpRequired(relic.getItemData().getRankLevel(), level);
              }
          }
  
          relic.addAppendProps(upgrades);
  
          // Save
          relic.setLevel(level);
          relic.setExp(exp);
          relic.setTotalExp(totalExp);
          relic.save();
  
          // Avatar
          if (oldLevel != level) {
              Avatar avatar = relic.getEquipCharacter() > 0 ? player.getAvatars().getAvatarById(relic.getEquipCharacter()) : null;
              if (avatar != null) {
                  avatar.recalcStats();
              }
          }
  
          // Packet
          player.sendPacket(new PacketStoreItemChangeNotify(relic));
          player.sendPacket(new PacketReliquaryUpgradeRsp(relic, rate, oldLevel, oldAppendPropIdList));
      }
  
      public List<ItemParam> calcWeaponUpgradeReturnItems(Player player, long targetGuid, List<Long> foodWeaponGuidList, List<ItemParam> itemParamList) {
          GameItem weapon = player.getInventory().getItemByGuid(targetGuid);
  
          // Sanity checks
          if (weapon == null || weapon.getItemType() != ItemType.ITEM_WEAPON) {
              return null;
          }
  
          WeaponPromoteData promoteData = GameData.getWeaponPromoteData(weapon.getItemData().getWeaponPromoteId(), weapon.getPromoteLevel());
          if (promoteData == null) {
              return null;
          }
  
          // Get exp gain
          int expGain = foodWeaponGuidList.stream()
              .map(player.getInventory()::getItemByGuid)
              .filter(Objects::nonNull)
              .mapToInt(food -> food.getItemData().getWeaponBaseExp() + ((food.getTotalExp() * 4) / 5))
              .sum();
          // Stream::ofNullable version
          expGain += itemParamList.stream()
              .mapToInt(param -> {
                  int exp = Stream.ofNullable(GameData.getItemDataMap().get(param.getItemId()))
                      .map(ItemData::getItemUseActions)
                      .filter(Objects::nonNull)
                      .flatMap(Collection::stream)
                      .filter(action -> action.getItemUseOp() == ItemUseOp.ITEM_USE_ADD_WEAPON_EXP)
                      .mapToInt(action -> ((ItemUseAddWeaponExp) action).getExp())
                      .sum();
                  return exp * param.getCount();
              })
              .sum();
          // Optional::ofNullable version
          // expGain += itemParamList.stream()
          //     .mapToInt(param -> {
          //         int exp = Optional.ofNullable(GameData.getItemDataMap().get(param.getItemId()))
          //             .map(ItemData::getItemUseActions)
          //             .map(actions -> {
          //                 return actions.stream()
          //                     .filter(action -> action.getItemUseOp() == ItemUseOp.ITEM_USE_ADD_WEAPON_EXP)
          //                     .mapToInt(action -> ((ItemUseAddWeaponExp) action).getExp())
          //                     .sum();
          //             })
          //             .orElse(0);
          //         return exp * param.getCount();
          //     })
          //     .sum();
  
          // Try
          int maxLevel = promoteData.getUnlockMaxLevel();
          int level = weapon.getLevel();
          int exp = weapon.getExp();
          int reqExp = GameData.getWeaponExpRequired(weapon.getItemData().getRankLevel(), level);
  
          while (expGain > 0 && reqExp > 0 && level < maxLevel) {
              // Do calculations
              int toGain = Math.min(expGain, reqExp - exp);
              exp += toGain;
              expGain -= toGain;
              // Level up
              if (exp >= reqExp) {
                  // Exp
                  exp = 0;
                  level += 1;
                  // Set req exp
                  reqExp = GameData.getWeaponExpRequired(weapon.getItemData().getRankLevel(), level);
              }
          }
  
          return getLeftoverOres(expGain);
      }
  
  
      public void upgradeWeapon(Player player, long targetGuid, List<Long> foodWeaponGuidList, List<ItemParam> itemParamList) {
          GameItem weapon = player.getInventory().getItemByGuid(targetGuid);
  
          // Sanity checks
          if (weapon == null || weapon.getItemType() != ItemType.ITEM_WEAPON) {
              return;
          }
  
          WeaponPromoteData promoteData = GameData.getWeaponPromoteData(weapon.getItemData().getWeaponPromoteId(), weapon.getPromoteLevel());
          if (promoteData == null) {
              return;
          }
  
          // Get exp gain
          int expGain = 0, expGainFree = 0;
          List<GameItem> foodWeapons = new ArrayList<GameItem>();
          for (long guid : foodWeaponGuidList) {
              GameItem food = player.getInventory().getItemByGuid(guid);
              if (food == null || !food.isDestroyable()) {
                  continue;
              }
              expGain += food.getItemData().getWeaponBaseExp();
              if (food.getTotalExp() > 0) {
                  expGainFree += (food.getTotalExp() * 4) / 5;  // No tax :D
              }
              foodWeapons.add(food);
          }
          List<ItemParamData> payList = new ArrayList<ItemParamData>();
          for (ItemParam param : itemParamList) {
              int amount = param.getCount();  // Previously this capped to inventory amount, but rejecting the payment makes more sense for an invalid order
  
              var data = GameData.getItemDataMap().get(param.getItemId());
              if (data != null) {
                  var actions = data.getItemUseActions();
                  if (actions != null) {
                      for (var action : actions) {
                          if (action.getItemUseOp() == ItemUseOp.ITEM_USE_ADD_WEAPON_EXP) {
                              expGain += ((ItemUseAddWeaponExp) action).getExp() * amount;
                          }
                      }
                  }
              }
  
              payList.add(new ItemParamData(param.getItemId(), amount));
          }
  
          // Make sure exp gain is valid
          int moraCost = expGain / 10;
          expGain += expGainFree;
          if (expGain <= 0) {
              return;
          }
  
          // Confirm payment of materials and mora (assume food weapons are payable afterwards)
          payList.add(new ItemParamData(202, moraCost));
          if (!player.getInventory().payItems(payList)) {
              return;
          }
          player.getInventory().removeItems(foodWeapons);
  
          // Level up
          int maxLevel = promoteData.getUnlockMaxLevel();
          int level = weapon.getLevel();
          int oldLevel = level;
          int exp = weapon.getExp();
          int totalExp = weapon.getTotalExp();
          int reqExp = GameData.getWeaponExpRequired(weapon.getItemData().getRankLevel(), level);
  
          while (expGain > 0 && reqExp > 0 && level < maxLevel) {
              // Do calculations
              int toGain = Math.min(expGain, reqExp - exp);
              exp += toGain;
              totalExp += toGain;
              expGain -= toGain;
              // Level up
              if (exp >= reqExp) {
                  // Exp
                  exp = 0;
                  level += 1;
                  // Set req exp
                  reqExp = GameData.getWeaponExpRequired(weapon.getItemData().getRankLevel(), level);
              }
          }
  
          List<ItemParam> leftovers = getLeftoverOres(expGain);
          player.getInventory().addItemParams(leftovers);
  
          weapon.setLevel(level);
          weapon.setExp(exp);
          weapon.setTotalExp(totalExp);
          weapon.save();
  
          // Avatar
          if (oldLevel != level) {
              Avatar avatar = weapon.getEquipCharacter() > 0 ? player.getAvatars().getAvatarById(weapon.getEquipCharacter()) : null;
              if (avatar != null) {
                  avatar.recalcStats();
              }
          }
  
          // Packets
          player.sendPacket(new PacketStoreItemChangeNotify(weapon));
          player.sendPacket(new PacketWeaponUpgradeRsp(weapon, oldLevel, leftovers));
      }
  
      private List<ItemParam> getLeftoverOres(int leftover) {
          List<ItemParam> leftoverOreList = new ArrayList<>(3);
  
          for (var e : weaponRefundMaterials.int2IntEntrySet()) {
              int exp = e.getIntValue();
              int ores = leftover / exp;
              leftover = leftover % exp;
  
              if (ores > 0)
                  leftoverOreList.add(ItemParam.newBuilder().setItemId(e.getIntKey()).setCount(ores).build());
          }
  
          return leftoverOreList;
      }
  
      public void refineWeapon(Player player, long targetGuid, long feedGuid) {
          GameItem weapon = player.getInventory().getItemByGuid(targetGuid);
          GameItem feed = player.getInventory().getItemByGuid(feedGuid);
  
          // Sanity checks
          if (weapon == null || feed == null || !feed.isDestroyable()) {
              return;
          }
  
          if (weapon.getItemData().getAwakenMaterial() == 0) {
              if (weapon.getItemType() != ItemType.ITEM_WEAPON || weapon.getItemId() != feed.getItemId()) {
                  return;
              }
          } else {
              if (weapon.getItemType() != ItemType.ITEM_WEAPON || weapon.getItemData().getAwakenMaterial() != feed.getItemId()) {
                  return;
              }
          }
  
          if (weapon.getRefinement() >= 4 || weapon.getAffixes() == null || weapon.getAffixes().size() == 0) {
              return;
          }
  
          // Calculate
          int oldRefineLevel = weapon.getRefinement();
          int targetRefineLevel = Math.min(oldRefineLevel + feed.getRefinement() + 1, 4);
          int moraCost = 0;
  
          try {
              moraCost = weapon.getItemData().getAwakenCosts()[weapon.getRefinement()];
          } catch (Exception e) {
              return;
          }
  
          // Mora check
          if (player.getMora() >= moraCost) {
              player.setMora(player.getMora() - moraCost);
          } else {
              return;
          }
  
          // Consume weapon
          player.getInventory().removeItem(feed, 1);
  
          // Get
          weapon.setRefinement(targetRefineLevel);
          weapon.save();
  
          // Avatar
          Avatar avatar = weapon.getEquipCharacter() > 0 ? player.getAvatars().getAvatarById(weapon.getEquipCharacter()) : null;
          if (avatar != null) {
              avatar.recalcStats();
          }
  
          // Packets
          player.sendPacket(new PacketStoreItemChangeNotify(weapon));
          player.sendPacket(new PacketWeaponAwakenRsp(avatar, weapon, feed, oldRefineLevel));
      }
  
      public void promoteWeapon(Player player, long targetGuid) {
          GameItem weapon = player.getInventory().getItemByGuid(targetGuid);
  
          if (weapon == null || weapon.getItemType() != ItemType.ITEM_WEAPON) {
              return;
          }
  
          int nextPromoteLevel = weapon.getPromoteLevel() + 1;
          WeaponPromoteData currentPromoteData = GameData.getWeaponPromoteData(weapon.getItemData().getWeaponPromoteId(), weapon.getPromoteLevel());
          WeaponPromoteData nextPromoteData = GameData.getWeaponPromoteData(weapon.getItemData().getWeaponPromoteId(), nextPromoteLevel);
          if (currentPromoteData == null || nextPromoteData == null) {
              return;
          }
  
          // Level check
          if (weapon.getLevel() != currentPromoteData.getUnlockMaxLevel()) {
              return;
          }
  
          // Pay materials and mora if possible
          ItemParamData[] costs = nextPromoteData.getCostItems();  // Can this be null?
          if (nextPromoteData.getCoinCost() > 0) {
              costs = Arrays.copyOf(costs, costs.length + 1);
              costs[costs.length-1] = new ItemParamData(202, nextPromoteData.getCoinCost());
          }
          if (!player.getInventory().payItems(costs)) {
              return;
          }
  
          int oldPromoteLevel = weapon.getPromoteLevel();
          weapon.setPromoteLevel(nextPromoteLevel);
          weapon.save();
  
          // Avatar
          Avatar avatar = weapon.getEquipCharacter() > 0 ? player.getAvatars().getAvatarById(weapon.getEquipCharacter()) : null;
          if (avatar != null) {
              avatar.recalcStats();
          }
  
          // Packets
          player.sendPacket(new PacketStoreItemChangeNotify(weapon));
          player.sendPacket(new PacketWeaponPromoteRsp(weapon, oldPromoteLevel));
      }
  
      public void promoteAvatar(Player player, long guid) {
          Avatar avatar = player.getAvatars().getAvatarByGuid(guid);
  
          // Sanity checks
          if (avatar == null) {
              return;
          }
  
          int nextPromoteLevel = avatar.getPromoteLevel() + 1;
          AvatarPromoteData currentPromoteData = GameData.getAvatarPromoteData(avatar.getAvatarData().getAvatarPromoteId(), avatar.getPromoteLevel());
          AvatarPromoteData nextPromoteData = GameData.getAvatarPromoteData(avatar.getAvatarData().getAvatarPromoteId(), nextPromoteLevel);
          if (currentPromoteData == null || nextPromoteData == null) {
              return;
          }
  
          // Level check
          if (avatar.getLevel() != currentPromoteData.getUnlockMaxLevel()) {
              return;
          }
  
          // Pay materials and mora if possible
          ItemParamData[] costs = nextPromoteData.getCostItems();  // Can this be null?
          if (nextPromoteData.getCoinCost() > 0) {
              costs = Arrays.copyOf(costs, costs.length + 1);
              costs[costs.length-1] = new ItemParamData(202, nextPromoteData.getCoinCost());
          }
          if (!player.getInventory().payItems(costs)) {
              return;
          }
  
          // Update promote level
          avatar.setPromoteLevel(nextPromoteLevel);
  
          // Update proud skills
          Optional.ofNullable(GameData.getAvatarSkillDepotDataMap().get(avatar.getSkillDepotId()))
              .map(AvatarSkillDepotData::getInherentProudSkillOpens)
              .ifPresent(d -> d.stream()
                  .filter(openData -> openData.getProudSkillGroupId() > 0)
                  .filter(openData -> openData.getNeedAvatarPromoteLevel() == avatar.getPromoteLevel())
                  .mapToInt(openData -> (openData.getProudSkillGroupId() * 100) + 1)
                  .filter(GameData.getProudSkillDataMap()::containsKey)
                  .forEach(proudSkillId -> {
                      avatar.getProudSkillList().add(proudSkillId);
                      player.sendPacket(new PacketProudSkillChangeNotify(avatar));
                  })
          );
  
          // Packets
          player.sendPacket(new PacketAvatarPropNotify(avatar));
          player.sendPacket(new PacketAvatarPromoteRsp(avatar));
  
          // TODO Send entity prop update packet to world
          avatar.recalcStats(true);
          avatar.save();
      }
  
      public void upgradeAvatar(Player player, long guid, int itemId, int count) {
          Avatar avatar = player.getAvatars().getAvatarByGuid(guid);
  
          // Sanity checks
          if (avatar == null) {
              return;
          }
  
          AvatarPromoteData promoteData = GameData.getAvatarPromoteData(avatar.getAvatarData().getAvatarPromoteId(), avatar.getPromoteLevel());
          if (promoteData == null) {
              return;
          }
  
          // Calc exp
          int expGain = 0;
  
          var data = GameData.getItemDataMap().get(itemId);
          if (data != null) {
              var actions = data.getItemUseActions();
              if (actions != null) {
                  for (var action : actions) {
                      if (action.getItemUseOp() == ItemUseOp.ITEM_USE_ADD_EXP) {
                          expGain += ((ItemUseAddExp) action).getExp() * count;
                      }
                  }
              }
          }
  
          // Sanity check
          if (expGain <= 0) {
              return;
          }
  
          // Payment check
          int moraCost = expGain / 5;
          ItemParamData[] costItems = new ItemParamData[] {new ItemParamData(itemId, count), new ItemParamData(202, moraCost)};
          if (!player.getInventory().payItems(costItems)) {
              return;
          }
  
          // Level up
          upgradeAvatar(player, avatar, promoteData, expGain);
      }
  
      public void upgradeAvatar(Player player, Avatar avatar, int expGain) {
          AvatarPromoteData promoteData = GameData.getAvatarPromoteData(avatar.getAvatarData().getAvatarPromoteId(), avatar.getPromoteLevel());
          if (promoteData == null) {
              return;
          }
  
          upgradeAvatar(player, avatar, promoteData, expGain);
      }
  
      public void upgradeAvatar(Player player, Avatar avatar, AvatarPromoteData promoteData, int expGain) {
          int maxLevel = promoteData.getUnlockMaxLevel();
          int level = avatar.getLevel();
          int oldLevel = level;
          int exp = avatar.getExp();
          int reqExp = GameData.getAvatarLevelExpRequired(level);
  
          while (expGain > 0 && reqExp > 0 && level < maxLevel) {
              // Do calculations
              int toGain = Math.min(expGain, reqExp - exp);
              exp += toGain;
              expGain -= toGain;
              // Level up
              if (exp >= reqExp) {
                  // Exp
                  exp = 0;
                  level += 1;
                  // Set req exp
                  reqExp = GameData.getAvatarLevelExpRequired(level);
              }
          }
  
          // Old map for packet
          Map<Integer, Float> oldPropMap = avatar.getFightProperties();
          if (oldLevel != level) {
              // Deep copy if level has changed
              oldPropMap = new Int2FloatArrayMap(avatar.getFightProperties());
          }
  
          // Done
          avatar.setLevel(level);
          avatar.setExp(exp);
          avatar.recalcStats();
          avatar.save();
  
          // TODO Send entity prop update packet to world
  
          // Packets
          player.sendPacket(new PacketAvatarPropNotify(avatar));
          player.sendPacket(new PacketAvatarUpgradeRsp(avatar, oldLevel, oldPropMap));
      }
  
      public void upgradeAvatarFetterLevel(Player player, Avatar avatar, int expGain) {
          // May work. Not test.
          int maxLevel = 10; // Keep it until I think of a more "elegant" way
          int level = avatar.getFetterLevel();
          int exp = avatar.getFetterExp();
          int reqExp = GameData.getAvatarFetterLevelExpRequired(level);
  
          while (expGain > 0 && reqExp > 0 && level < maxLevel) {
              int toGain = Math.min(expGain, reqExp - exp);
              exp += toGain;
              expGain -= toGain;
              if (exp >= reqExp) {
                  exp = 0;
                  level += 1;
                  reqExp = GameData.getAvatarFetterLevelExpRequired(level);
              }
          }
  
          avatar.setFetterLevel(level);
          avatar.setFetterExp(exp);
          avatar.save();
  
          player.sendPacket(new PacketAvatarPropNotify(avatar));
          player.sendPacket(new PacketAvatarFetterDataNotify(avatar));
      }
  
      @Deprecated(forRemoval = true)
      public void upgradeAvatarSkill(Player player, long guid, int skillId) {
          // Sanity checks
          Avatar avatar = player.getAvatars().getAvatarByGuid(guid);
          if (avatar == null) return;
  
          avatar.upgradeSkill(skillId);
      }
  
      @Deprecated(forRemoval = true)
      public void unlockAvatarConstellation(Player player, long guid) {
          // Sanity check
          Avatar avatar = player.getAvatars().getAvatarByGuid(guid);
          if (avatar == null) return;
  
          avatar.unlockConstellation();
      }
  
      public void destroyMaterial(Player player, List<MaterialInfo> list) {
          // Return materials
          val returnMaterialMap = new Int2IntOpenHashMap();
          val inventory = player.getInventory();
  
          for (MaterialInfo info : list) {
              // Sanity check
              if (info.getCount() <= 0) {
                  continue;
              }
  
              GameItem item = inventory.getItemByGuid(info.getGuid());
              if (item == null || !item.isDestroyable()) {
                  continue;
              }
  
              // Remove
              int removeAmount = Math.min(info.getCount(), item.getCount());
              inventory.removeItem(item, removeAmount);
  
              // Delete material return items
              val data = item.getItemData();
              if (data.getDestroyReturnMaterial().length > 0) {
                  for (int i = 0; i < data.getDestroyReturnMaterial().length; i++) {
                      returnMaterialMap.addTo(data.getDestroyReturnMaterial()[i], data.getDestroyReturnMaterialCount()[i]);
                  }
              }
          }
  
          // Give back items
          if (returnMaterialMap.size() > 0) {
              returnMaterialMap.forEach((id, count) -> inventory.addItem(new GameItem(id, count)));
          }
  
          // Packets
          player.sendPacket(new PacketDestroyMaterialRsp(returnMaterialMap));
      }
  
      // Uses an item from the player's inventory.
      public synchronized GameItem useItem(Player player, long targetGuid, long itemGuid, int count, int optionId, boolean isEnterMpDungeonTeam) {
          Grasscutter.getLogger().debug("Attempting to use item from inventory");
          Avatar target = player.getAvatars().getAvatarByGuid(targetGuid);
          GameItem item = player.getInventory().getItemByGuid(itemGuid);
          if (item == null) return null;
          if (item.getCount() < count) return null;
          ItemData itemData = item.getItemData();
          if (itemData == null) return null;
  
          var params = new UseItemParams(player, itemData.getUseTarget(), target, count, optionId, isEnterMpDungeonTeam);
          params.usedItemId = item.getItemId();
          if (useItemDirect(itemData, params)) {
              player.getInventory().removeItem(item, count);
              var actions = itemData.getItemUseActions();
              if (actions != null)
                  actions.forEach(use -> use.postUseItem(params));
              Grasscutter.getLogger().debug("Item use succeeded!");
              return item;
          } else {
              Grasscutter.getLogger().debug("Item use failed!");
              return null;
          }
      }
  
      // Uses an item without checking the player's inventory.
      public synchronized boolean useItemDirect(ItemData itemData, UseItemParams params) {
          if (itemData == null) return false;
  
          // Ensure targeting conditions are satisfied
          val target = Optional.ofNullable(params.targetAvatar);
          switch (params.itemUseTarget) {
              case ITEM_USE_TARGET_NONE -> {}
              case ITEM_USE_TARGET_SPECIFY_AVATAR -> {
                  if (target.isEmpty()) return false;
              }
              case ITEM_USE_TARGET_SPECIFY_ALIVE_AVATAR -> {
                  if (target.map(a -> !a.getAsEntity().isAlive()).orElse(true)) return false;
              }
              case ITEM_USE_TARGET_SPECIFY_DEAD_AVATAR -> {
                  if (target.map(a -> a.getAsEntity().isAlive()).orElse(true)) return false;
              }
              case ITEM_USE_TARGET_CUR_AVATAR -> {}
              case ITEM_USE_TARGET_CUR_TEAM -> {}
          }
  
          int[] satiationParams = itemData.getSatiationParams();
          if (satiationParams != null && satiationParams.length > 0 && target.isPresent()) {
              // Invoke and call player use food event.
              var event = new PlayerUseFoodEvent(params.player, itemData, params.targetAvatar.getAsEntity()); event.call();
              if (event.isCanceled()) return false;
  
              float satiationIncrease = satiationParams[0] + ((float)satiationParams[1])/params.targetAvatar.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
              if (!params.targetAvatar.addSatiation(satiationIncrease)) {  // Make sure avatar can eat
                  return false;
              }
          }
  
          // Use
          var actions = itemData.getItemUseActions();
          Grasscutter.getLogger().debug("Using - actions - {}", actions);
          if (actions == null) return true;  // Maybe returning false would be more appropriate?
          return actions.stream()
                  .map(use -> use.useItem(params))
                  .reduce(false, (a,b) -> a || b);  // Don't short-circuit!!!
      }
  
      public static synchronized int checkPlayerAvatarConstellationLevel(Player player, int id) {
          // Try to accept itemId OR avatarId
          int avatarId = 0;
          if (GameData.getAvatarDataMap().containsKey(id)) {
              avatarId = id;
          } else {
              avatarId = Optional.ofNullable(GameData.getItemDataMap().get(id))
                  .map(itemData -> itemData.getItemUseActions())
                  .flatMap(actions ->
                      actions.stream()
                          .filter(action -> action.getItemUseOp() == ItemUseOp.ITEM_USE_GAIN_AVATAR)
                          .map(action -> ((emu.grasscutter.game.props.ItemUseAction.ItemUseGainAvatar) action).getI())
                          .findFirst())
                  .orElse(0);
          }
  
          if (avatarId == 0)
              return -2;  // Not an Avatar
  
          Avatar avatar = player.getAvatars().getAvatarById(avatarId);
          if (avatar == null)
              return -1;  // Doesn't have
  
          // Constellation
          int constLevel = avatar.getCoreProudSkillLevel();
          val avatarData = avatar.getSkillDepot();
          if (avatarData == null) {
              Grasscutter.getLogger().error("Attempted to check constellation level for UID"+player.getUid()+"'s avatar "+avatarId+" but avatar has no skillDepot!");
              return 0;
          }
          int constItemId = avatarData.getTalentCostItemId();
          GameItem constItem = player.getInventory().getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(constItemId);
          constLevel += Optional.ofNullable(constItem).map(GameItem::getCount).orElse(0);
          return constLevel;
      }
  }
  