diff --git a/src/main/java/emu/grasscutter/data/GameData.java b/src/main/java/emu/grasscutter/data/GameData.java index dd2d60a6b..43195e99a 100644 --- a/src/main/java/emu/grasscutter/data/GameData.java +++ b/src/main/java/emu/grasscutter/data/GameData.java @@ -76,6 +76,7 @@ public class GameData { private static final ArrayList codexReliquaryArrayList = new ArrayList<>(); private static final Int2ObjectMap fetterCharacterCardDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap rewardDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap worldAreaDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap worldLevelDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap dailyDungeonDataMap = new Int2ObjectOpenHashMap<>(); @@ -90,6 +91,7 @@ public class GameData { private static final Int2ObjectMap towerFloorDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap towerLevelDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap towerScheduleDataMap = new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap buffDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap forgeDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap homeWorldLevelDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap furnitureMakeConfigDataMap = new Int2ObjectOpenHashMap<>(); @@ -448,4 +450,8 @@ public class GameData { public static Int2ObjectMap getCookBonusDataMap() { return cookBonusDataMap; } + + public static Int2ObjectMap getBuffDataMap() { + return buffDataMap; + } } diff --git a/src/main/java/emu/grasscutter/data/common/ItemUseData.java b/src/main/java/emu/grasscutter/data/common/ItemUseData.java index 849bbb777..210739fe1 100644 --- a/src/main/java/emu/grasscutter/data/common/ItemUseData.java +++ b/src/main/java/emu/grasscutter/data/common/ItemUseData.java @@ -7,6 +7,9 @@ public class ItemUseData { private String[] useParam; public ItemUseOp getUseOp() { + if (useOp == null) { + useOp = ItemUseOp.ITEM_USE_NONE; + } return useOp; } diff --git a/src/main/java/emu/grasscutter/data/excels/BuffData.java b/src/main/java/emu/grasscutter/data/excels/BuffData.java new file mode 100644 index 000000000..a4ba64c18 --- /dev/null +++ b/src/main/java/emu/grasscutter/data/excels/BuffData.java @@ -0,0 +1,25 @@ +package emu.grasscutter.data.excels; + +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; +import emu.grasscutter.game.props.ServerBuffType; +import lombok.Getter; + +@ResourceType(name = "BuffExcelConfigData.json") +@Getter +public class BuffData extends GameResource { + private int groupId; + private int serverBuffId; + private float time; + private boolean isPersistent; + private ServerBuffType serverBuffType; + + @Override + public int getId() { + return this.serverBuffId; + } + + public void onLoad() { + this.serverBuffType = this.serverBuffType != null ? this.serverBuffType : ServerBuffType.SERVER_BUFF_NONE; + } +} diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index b12fcf36b..9580c907f 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -150,7 +150,8 @@ public class Player { @Getter private transient BattlePassManager battlePassManager; @Getter private transient CookingManager cookingManager; @Getter private transient ActivityManager activityManager; - + @Getter private transient PlayerBuffManager buffManager; + // Manager data (Save-able to the database) private PlayerProfile playerProfile; private TeamManager teamManager; @@ -195,6 +196,7 @@ public class Player { this.abilityManager = new AbilityManager(this); this.deforestationManager = new DeforestationManager(this); this.questManager = new QuestManager(this); + this.buffManager = new PlayerBuffManager(this); this.position = new Position(GameConstants.START_POSITION); this.rotation = new Position(0, 307, 0); this.sceneId = 3; @@ -1182,6 +1184,8 @@ public class Player { it.remove(); } } + // Handle buff + this.getBuffManager().onTick(); // Ping if (this.getWorld() != null) { // RTT notify - very important to send this often diff --git a/src/main/java/emu/grasscutter/game/player/PlayerBuffManager.java b/src/main/java/emu/grasscutter/game/player/PlayerBuffManager.java new file mode 100644 index 000000000..44c95dc3e --- /dev/null +++ b/src/main/java/emu/grasscutter/game/player/PlayerBuffManager.java @@ -0,0 +1,176 @@ +package emu.grasscutter.game.player; + +import java.util.LinkedList; +import java.util.List; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.BuffData; +import emu.grasscutter.net.proto.ServerBuffChangeNotifyOuterClass.ServerBuffChangeNotify.ServerBuffChangeType; +import emu.grasscutter.net.proto.ServerBuffOuterClass.ServerBuff; +import emu.grasscutter.server.packet.send.PacketServerBuffChangeNotify; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import lombok.AccessLevel; +import lombok.Getter; + +@Getter(AccessLevel.PRIVATE) +public class PlayerBuffManager extends BasePlayerManager { + private int nextBuffUid; + + private final List pendingBuffs; + private final Int2ObjectMap buffs; // Server buffs + + public PlayerBuffManager(Player player) { + super(player); + this.buffs = new Int2ObjectOpenHashMap<>(); + this.pendingBuffs = new LinkedList<>(); + } + + /** + * Gets a new uid for a server buff + * @return New integer buff uid + */ + private int getNextBuffUid() { + return ++nextBuffUid; + } + + /** + * Returns true if the player has a buff with this group id + * @param groupId Buff group id + * @return True if a buff with this group id exists + */ + public synchronized boolean hasBuff(int groupId) { + return this.getBuffs().containsKey(groupId); + } + + /** + * Clears all player buffs + */ + public synchronized void clearBuffs() { + // Remove from player + getPlayer().sendPacket( + new PacketServerBuffChangeNotify(getPlayer(), ServerBuffChangeType.SERVER_BUFF_CHANGE_TYPE_DEL_SERVER_BUFF, getBuffs().values()) + ); + + // Clear + getBuffs().clear(); + } + + /** + * Adds a server buff to the player. + * @param buffId Server buff id + * @return True if a buff was added + */ + public boolean addBuff(int buffId) { + return addBuff(buffId, -1f); + } + + /** + * Adds a server buff to the player. + * @param buffId Server buff id + * @param duration Duration of the buff in seconds. Set to 0 for an infinite buff. + * @return True if a buff was added + */ + public synchronized boolean addBuff(int buffId, float duration) { + // Get buff excel data + BuffData buffData = GameData.getBuffDataMap().get(buffId); + if (buffData == null) return false; + + // Set duration + if (duration < 0f) { + duration = buffData.getTime(); + } + + // Dont add buff if duration is equal or less than 0 + if (duration <= 0) { + return false; + } + + // Clear previous buff if it exists + if (this.hasBuff(buffData.getGroupId())) { + this.removeBuff(buffData.getGroupId()); + } + + // Create and store buff + PlayerBuff buff = new PlayerBuff(getNextBuffUid(), buffData, duration); + getBuffs().put(buff.getGroupId(), buff); + + // Packet + getPlayer().sendPacket(new PacketServerBuffChangeNotify(getPlayer(), ServerBuffChangeType.SERVER_BUFF_CHANGE_TYPE_ADD_SERVER_BUFF, buff)); + + return true; + } + + /** + * Removes a buff by its group id + * @param buffGroupId Server buff group id + * @return True if a buff was remove + */ + public synchronized boolean removeBuff(int buffGroupId) { + PlayerBuff buff = this.getBuffs().get(buffGroupId); + + if (buff != null) { + getPlayer().sendPacket( + new PacketServerBuffChangeNotify(getPlayer(), ServerBuffChangeType.SERVER_BUFF_CHANGE_TYPE_DEL_SERVER_BUFF, buff) + ); + return true; + } + + return false; + } + + public synchronized void onTick() { + // Skip if no buffs + if (getBuffs().size() == 0) return; + + long currentTime = System.currentTimeMillis(); + + // Add to pending buffs to remove if buff has expired + for (PlayerBuff buff : getBuffs().values()) { + if (currentTime > buff.getEndTime()) { + this.getPendingBuffs().add(buff); + } + } + + if (this.getPendingBuffs().size() > 0) { + // Send packet + getPlayer().sendPacket( + new PacketServerBuffChangeNotify(getPlayer(), ServerBuffChangeType.SERVER_BUFF_CHANGE_TYPE_DEL_SERVER_BUFF, this.pendingBuffs) + ); + + // Remove buff from player buff map + for (PlayerBuff buff : this.getPendingBuffs()) { + getBuffs().remove(buff.getGroupId()); + } + this.getPendingBuffs().clear(); + } + } + + @Getter + public static class PlayerBuff { + private final int uid; + private final BuffData buffData; + private final long endTime; + + public PlayerBuff(int uid, BuffData buffData, float duration) { + this.uid = uid; + this.buffData = buffData; + this.endTime = System.currentTimeMillis() + ((long) duration * 1000); + } + + public int getGroupId() { + return getBuffData().getGroupId(); + } + + public ServerBuff toProto() { + return ServerBuff.newBuilder() + .setServerBuffUid(this.getUid()) + .setServerBuffId(this.getBuffData().getId()) + .setServerBuffType(this.getBuffData().getServerBuffType().getValue()) + .setInstancedModifierId(1) + .build(); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/props/ServerBuffType.java b/src/main/java/emu/grasscutter/game/props/ServerBuffType.java new file mode 100644 index 000000000..0c6d9d7ee --- /dev/null +++ b/src/main/java/emu/grasscutter/game/props/ServerBuffType.java @@ -0,0 +1,34 @@ +package emu.grasscutter.game.props; + +import java.util.stream.Stream; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +public enum ServerBuffType { + SERVER_BUFF_NONE (0), + SERVER_BUFF_AVATAR (1), + SERVER_BUFF_TEAM (2), + SERVER_BUFF_TOWER (3); + + private final int value; + private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); + + static { + Stream.of(values()).forEach(e -> { + map.put(e.getValue(), e); + }); + } + + private ServerBuffType(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static ServerBuffType getTypeByValue(int value) { + return map.getOrDefault(value, SERVER_BUFF_NONE); + } +} diff --git a/src/main/java/emu/grasscutter/game/systems/InventorySystem.java b/src/main/java/emu/grasscutter/game/systems/InventorySystem.java index 2ad11f707..537f1c0b7 100644 --- a/src/main/java/emu/grasscutter/game/systems/InventorySystem.java +++ b/src/main/java/emu/grasscutter/game/systems/InventorySystem.java @@ -10,6 +10,7 @@ import emu.grasscutter.data.GameData; import emu.grasscutter.data.binout.OpenConfigEntry; import emu.grasscutter.data.binout.OpenConfigEntry.SkillPointModifier; import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.data.common.ItemUseData; import emu.grasscutter.data.excels.AvatarPromoteData; import emu.grasscutter.data.excels.AvatarSkillData; import emu.grasscutter.data.excels.AvatarSkillDepotData; @@ -821,6 +822,8 @@ public class InventorySystem extends BaseGameSystem { } used = player.getTeamManager().reviveAvatar(target) ? 1 : 0; + } else { + used = 1; } break; case MATERIAL_NOTICE_ADD_HP: @@ -940,9 +943,24 @@ public class InventorySystem extends BaseGameSystem { // If we used at least one item, or one of the methods called here reports using the item successfully, // we return the item to make UseItemRsp a success. if (used > 0) { + // Handle use params, mainly server buffs + for (ItemUseData useData : useItem.getItemData().getItemUse()) { + switch (useData.getUseOp()) { + case ITEM_USE_ADD_SERVER_BUFF -> { + int buffId = Integer.parseInt(useData.getUseParam()[0]); + float time = Float.parseFloat(useData.getUseParam()[1]); + + player.getBuffManager().addBuff(buffId, time); + } + default -> {} + } + } + + // Remove item from inventory since we used it player.getInventory().removeItem(useItem, used); return useItem; } + if (useSuccess) { return useItem; } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketServerBuffChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketServerBuffChangeNotify.java new file mode 100644 index 000000000..ff22f5891 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketServerBuffChangeNotify.java @@ -0,0 +1,44 @@ +package emu.grasscutter.server.packet.send; + +import java.util.Collection; + +import emu.grasscutter.game.entity.EntityAvatar; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.player.PlayerBuffManager.PlayerBuff; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.ServerBuffChangeNotifyOuterClass.ServerBuffChangeNotify; +import emu.grasscutter.net.proto.ServerBuffChangeNotifyOuterClass.ServerBuffChangeNotify.ServerBuffChangeType; + +public class PacketServerBuffChangeNotify extends BasePacket { + + public PacketServerBuffChangeNotify(Player player, ServerBuffChangeType changeType, PlayerBuff buff) { + super(PacketOpcodes.ServerBuffChangeNotify); + + var proto = ServerBuffChangeNotify.newBuilder(); + + for (EntityAvatar entity : player.getTeamManager().getActiveTeam()) { + proto.addAvatarGuidList(entity.getAvatar().getGuid()); + } + + proto.setServerBuffChangeType(changeType); + proto.addServerBuffList(buff.toProto()); + + this.setData(proto); + } + + public PacketServerBuffChangeNotify(Player player, ServerBuffChangeType changeType, Collection buffs) { + super(PacketOpcodes.ServerBuffChangeNotify); + + var proto = ServerBuffChangeNotify.newBuilder(); + + for (EntityAvatar entity : player.getTeamManager().getActiveTeam()) { + proto.addAvatarGuidList(entity.getAvatar().getGuid()); + } + + proto.setServerBuffChangeType(changeType); + proto.addAllServerBuffList(buffs.stream().map(PlayerBuff::toProto).toList()); + + this.setData(proto); + } +}