diff --git a/src/main/java/emu/grasscutter/data/excels/CompoundData.java b/src/main/java/emu/grasscutter/data/excels/CompoundData.java index d72cd28d1..0fa3ba6bb 100644 --- a/src/main/java/emu/grasscutter/data/excels/CompoundData.java +++ b/src/main/java/emu/grasscutter/data/excels/CompoundData.java @@ -1,24 +1,26 @@ -package emu.grasscutter.data.excels; - -import emu.grasscutter.data.GameResource; -import emu.grasscutter.data.ResourceType; -import emu.grasscutter.data.common.ItemParamData; -import java.util.List; -import lombok.Getter; - -@ResourceType( - name = {"CompoundExcelConfigData.json"}, - loadPriority = ResourceType.LoadPriority.LOW) -@Getter -public class CompoundData extends GameResource { - @Getter(onMethod_ = @Override) - private int id; - - private int groupID; - private int rankLevel; - private boolean isDefaultUnlocked; - private int costTime; - private int queueSize; - private List inputVec; - private List outputVec; -} +package emu.grasscutter.data.excels; + +import com.google.gson.annotations.SerializedName; +import emu.grasscutter.data.GameResource; +import emu.grasscutter.data.ResourceType; +import emu.grasscutter.data.common.ItemParamData; +import java.util.List; +import lombok.Getter; + +@ResourceType( + name = {"CompoundExcelConfigData.json"}, + loadPriority = ResourceType.LoadPriority.LOW) +@Getter +public class CompoundData extends GameResource { + @Getter(onMethod_ = @Override) + private int id; + + @SerializedName("groupID") + private int groupId; + private int rankLevel; + private boolean isDefaultUnlocked; + private int costTime; + private int queueSize; + private List inputVec; + private List outputVec; +} diff --git a/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java b/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java index 2ecfdc1ea..9fcb2258b 100644 --- a/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java +++ b/src/main/java/emu/grasscutter/game/dungeons/challenge/WorldChallenge.java @@ -1,178 +1,178 @@ -package emu.grasscutter.game.dungeons.challenge; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; -import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.props.WatcherTriggerType; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.scripts.data.SceneTrigger; -import emu.grasscutter.scripts.data.ScriptArgs; -import emu.grasscutter.server.packet.send.PacketDungeonChallengeBeginNotify; -import emu.grasscutter.server.packet.send.PacketDungeonChallengeFinishNotify; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class WorldChallenge { - private final Scene scene; - private final SceneGroup group; - private final int challengeId; - private final int challengeIndex; - private final List paramList; - private final int timeLimit; - private final List challengeTriggers; - private final int goal; - private final AtomicInteger score; - private boolean progress; - private boolean success; - private long startedAt; - private int finishedTime; - - public WorldChallenge( - Scene scene, - SceneGroup group, - int challengeId, - int challengeIndex, - List paramList, - int timeLimit, - int goal, - List challengeTriggers) { - this.scene = scene; - this.group = group; - this.challengeId = challengeId; - this.challengeIndex = challengeIndex; - this.paramList = paramList; - this.timeLimit = timeLimit; - this.challengeTriggers = challengeTriggers; - this.goal = goal; - this.score = new AtomicInteger(0); - } - - public boolean inProgress() { - return this.progress; - } - - public void onCheckTimeOut() { - if (!inProgress()) { - return; - } - if (timeLimit <= 0) { - return; - } - challengeTriggers.forEach(t -> t.onCheckTimeout(this)); - } - - public void start() { - if (inProgress()) { - Grasscutter.getLogger().info("Could not start a in progress challenge."); - return; - } - this.progress = true; - this.startedAt = System.currentTimeMillis(); - getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this)); - challengeTriggers.forEach(t -> t.onBegin(this)); - } - - public void done() { - if (!this.inProgress()) return; - this.finish(true); - - var scene = this.getScene(); - var dungeonManager = scene.getDungeonManager(); - if (dungeonManager != null && dungeonManager.getDungeonData() != null) { - scene - .getPlayers() - .forEach( - p -> - p.getActivityManager() - .triggerWatcher( - WatcherTriggerType.TRIGGER_FINISH_CHALLENGE, - String.valueOf(dungeonManager.getDungeonData().getId()), - String.valueOf(this.getGroup().id), - String.valueOf(this.getChallengeId()))); - } - - scene - .getScriptManager() - .callEvent( - // TODO record the time in PARAM2 and used in action - new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_SUCCESS) - .setParam2(finishedTime)); - this.getScene() - .triggerDungeonEvent( - DungeonPassConditionType.DUNGEON_COND_FINISH_CHALLENGE, - getChallengeId(), - getChallengeIndex()); - - this.challengeTriggers.forEach(t -> t.onFinish(this)); - } - - public void fail() { - if (!this.inProgress()) return; - this.finish(true); - - this.getScene() - .getScriptManager() - .callEvent(new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_FAIL)); - challengeTriggers.forEach(t -> t.onFinish(this)); - } - - private void finish(boolean success) { - this.progress = false; - this.success = success; - this.finishedTime = (int) ((System.currentTimeMillis() - this.startedAt) / 1000L); - getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this)); - } - - public int increaseScore() { - return score.incrementAndGet(); - } - - public void onMonsterDeath(EntityMonster monster) { - if (!inProgress()) { - return; - } - if (monster.getGroupId() != getGroup().id) { - return; - } - this.challengeTriggers.forEach(t -> t.onMonsterDeath(this, monster)); - } - - public void onGadgetDeath(EntityGadget gadget) { - if (!inProgress()) { - return; - } - if (gadget.getGroupId() != getGroup().id) { - return; - } - this.challengeTriggers.forEach(t -> t.onGadgetDeath(this, gadget)); - } - - public void onGroupTriggerDeath(SceneTrigger trigger) { - if (!this.inProgress()) return; - - var triggerGroup = trigger.getCurrentGroup(); - if (triggerGroup == null || triggerGroup.id != getGroup().id) { - return; - } - - this.challengeTriggers.forEach(t -> t.onGroupTrigger(this, trigger)); - } - - public void onGadgetDamage(EntityGadget gadget) { - if (!inProgress()) { - return; - } - if (gadget.getGroupId() != getGroup().id) { - return; - } - this.challengeTriggers.forEach(t -> t.onGadgetDamage(this, gadget)); - } -} +package emu.grasscutter.game.dungeons.challenge; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.dungeons.challenge.trigger.ChallengeTrigger; +import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.game.props.WatcherTriggerType; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.scripts.data.SceneTrigger; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.packet.send.PacketDungeonChallengeBeginNotify; +import emu.grasscutter.server.packet.send.PacketDungeonChallengeFinishNotify; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WorldChallenge { + private final Scene scene; + private final SceneGroup group; + private final int challengeId; + private final int challengeIndex; + private final List paramList; + private final int timeLimit; + private final List challengeTriggers; + private final int goal; + private final AtomicInteger score; + private boolean progress; + private boolean success; + private long startedAt; + private int finishedTime; + + public WorldChallenge( + Scene scene, + SceneGroup group, + int challengeId, + int challengeIndex, + List paramList, + int timeLimit, + int goal, + List challengeTriggers) { + this.scene = scene; + this.group = group; + this.challengeId = challengeId; + this.challengeIndex = challengeIndex; + this.paramList = paramList; + this.timeLimit = timeLimit; + this.challengeTriggers = challengeTriggers; + this.goal = goal; + this.score = new AtomicInteger(0); + } + + public boolean inProgress() { + return this.progress; + } + + public void onCheckTimeOut() { + if (!inProgress()) { + return; + } + if (timeLimit <= 0) { + return; + } + challengeTriggers.forEach(t -> t.onCheckTimeout(this)); + } + + public void start() { + if (inProgress()) { + Grasscutter.getLogger().info("Could not start a in progress challenge."); + return; + } + this.progress = true; + this.startedAt = System.currentTimeMillis(); + getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this)); + challengeTriggers.forEach(t -> t.onBegin(this)); + } + + public void done() { + if (!this.inProgress()) return; + this.finish(true); + + var scene = this.getScene(); + var dungeonManager = scene.getDungeonManager(); + if (dungeonManager != null && dungeonManager.getDungeonData() != null) { + scene + .getPlayers() + .forEach( + p -> + p.getActivityManager() + .triggerWatcher( + WatcherTriggerType.TRIGGER_FINISH_CHALLENGE, + String.valueOf(dungeonManager.getDungeonData().getId()), + String.valueOf(this.getGroup().id), + String.valueOf(this.getChallengeId()))); + } + + scene + .getScriptManager() + .callEvent( + // TODO record the time in PARAM2 and used in action + new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_SUCCESS) + .setParam2(finishedTime)); + this.getScene() + .triggerDungeonEvent( + DungeonPassConditionType.DUNGEON_COND_FINISH_CHALLENGE, + getChallengeId(), + getChallengeIndex()); + + this.challengeTriggers.forEach(t -> t.onFinish(this)); + } + + public void fail() { + if (!this.inProgress()) return; + this.finish(false); + + this.getScene() + .getScriptManager() + .callEvent(new ScriptArgs(this.getGroup().id, EventType.EVENT_CHALLENGE_FAIL)); + challengeTriggers.forEach(t -> t.onFinish(this)); + } + + private void finish(boolean success) { + this.progress = false; + this.success = success; + this.finishedTime = (int) ((this.scene.getSceneTimeSeconds() - this.startedAt)); + getScene().broadcastPacket(new PacketDungeonChallengeFinishNotify(this)); + } + + public int increaseScore() { + return score.incrementAndGet(); + } + + public void onMonsterDeath(EntityMonster monster) { + if (!inProgress()) { + return; + } + if (monster.getGroupId() != getGroup().id) { + return; + } + this.challengeTriggers.forEach(t -> t.onMonsterDeath(this, monster)); + } + + public void onGadgetDeath(EntityGadget gadget) { + if (!inProgress()) { + return; + } + if (gadget.getGroupId() != getGroup().id) { + return; + } + this.challengeTriggers.forEach(t -> t.onGadgetDeath(this, gadget)); + } + + public void onGroupTriggerDeath(SceneTrigger trigger) { + if (!this.inProgress()) return; + + var triggerGroup = trigger.getCurrentGroup(); + if (triggerGroup == null || triggerGroup.id != getGroup().id) { + return; + } + + this.challengeTriggers.forEach(t -> t.onGroupTrigger(this, trigger)); + } + + public void onGadgetDamage(EntityGadget gadget) { + if (!inProgress()) { + return; + } + if (gadget.getGroupId() != getGroup().id) { + return; + } + this.challengeTriggers.forEach(t -> t.onGadgetDamage(this, gadget)); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java index 4a03ea6a6..793813662 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityAvatar.java @@ -1,370 +1,367 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.GameConstants; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.avatar.AvatarData; -import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.inventory.EquipType; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.AbilityControlBlockOuterClass.AbilityControlBlock; -import emu.grasscutter.net.proto.AbilityEmbryoOuterClass.AbilityEmbryo; -import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; -import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; -import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason; -import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason; -import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; -import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; -import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; -import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; -import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; -import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; -import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; -import emu.grasscutter.net.proto.SceneAvatarInfoOuterClass.SceneAvatarInfo; -import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; -import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; -import emu.grasscutter.net.proto.VectorOuterClass.Vector; -import emu.grasscutter.server.event.player.PlayerMoveEvent; -import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify; -import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.ProtoHelper; -import emu.grasscutter.utils.Utils; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import lombok.Getter; -import lombok.val; - -public class EntityAvatar extends GameEntity { - @Getter private final Avatar avatar; - - @Getter private PlayerDieType killedType; - @Getter private int killedBy; - - public EntityAvatar(Avatar avatar) { - this(null, avatar); - } - - public EntityAvatar(Scene scene, Avatar avatar) { - super(scene); - - this.avatar = avatar; - this.avatar.setCurrentEnergy(); - - if (getScene() != null) { - this.id = getScene().getWorld().getNextEntityId(EntityIdType.AVATAR); - - var weapon = getAvatar().getWeapon(); - if (weapon != null) { - weapon.setWeaponEntityId(getScene().getWorld().getNextEntityId(EntityIdType.WEAPON)); - } - } - } - - @Override - public int getEntityTypeId() { - return this.getAvatar().getAvatarId(); - } - - public Player getPlayer() { - return this.avatar.getPlayer(); - } - - @Override - public Position getPosition() { - return getPlayer().getPosition(); - } - - @Override - public Position getRotation() { - return getPlayer().getRotation(); - } - - @Override - public boolean isAlive() { - return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f; - } - - @Override - public Int2FloatMap getFightProperties() { - return getAvatar().getFightProperties(); - } - - public int getWeaponEntityId() { - if (getAvatar().getWeapon() != null) { - return getAvatar().getWeapon().getWeaponEntityId(); - } - return 0; - } - - @Override - public void onDeath(int killerId) { - super.onDeath(killerId); // Invoke super class's onDeath() method. - - this.killedType = PlayerDieType.PLAYER_DIE_TYPE_KILL_BY_MONSTER; - this.killedBy = killerId; - clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE); - } - - public void onDeath(PlayerDieType dieType, int killerId) { - super.onDeath(killerId); // Invoke super class's onDeath() method. - - this.killedType = dieType; - this.killedBy = killerId; - clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE); - } - - @Override - public float heal(float amount) { - // Do not heal character if they are dead - if (!this.isAlive()) { - return 0f; - } - - float healed = super.heal(amount); - - if (healed > 0f) { - getScene() - .broadcastPacket( - new PacketEntityFightPropChangeReasonNotify( - this, - FightProperty.FIGHT_PROP_CUR_HP, - healed, - PropChangeReason.PROP_CHANGE_REASON_ABILITY, - ChangeHpReason.CHANGE_HP_REASON_ADD_ABILITY)); - } - - return healed; - } - - public void clearEnergy(ChangeEnergyReason reason) { - // Fight props. - val curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp(); - float curEnergy = this.getFightProperty(curEnergyProp); - - // Set energy to zero. - this.avatar.setCurrentEnergy(curEnergyProp, 0); - - // Send packets. - this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp)); - - if (reason == ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START) { - this.getScene() - .broadcastPacket( - new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, -curEnergy, reason)); - } - } - - /** - * Adds a fixed amount of energy to the current avatar. - * - * @param amount The amount of energy to add. - * @return True if the energy was added, false if the energy was not added. - */ - public boolean addEnergy(float amount) { - var curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp(); - var curEnergy = this.getFightProperty(curEnergyProp); - if (curEnergy == amount) return false; - - this.getAvatar().setCurrentEnergy(curEnergyProp, amount); - this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp)); - return true; - } - - public void addEnergy(float amount, PropChangeReason reason) { - this.addEnergy(amount, reason, false); - } - - public void addEnergy(float amount, PropChangeReason reason, boolean isFlat) { - // Get current and maximum energy for this avatar. - val elementType = this.getAvatar().getSkillDepot().getElementType(); - val curEnergyProp = elementType.getCurEnergyProp(); - val maxEnergyProp = elementType.getMaxEnergyProp(); - - float curEnergy = this.getFightProperty(curEnergyProp); - float maxEnergy = this.getFightProperty(maxEnergyProp); - - // Scale amount by energy recharge, if the amount is not flat. - if (!isFlat) { - amount *= this.getFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); - } - - // Determine the new energy value. - float newEnergy = Math.min(curEnergy + amount, maxEnergy); - - // Set energy and notify. - if (newEnergy != curEnergy) { - this.avatar.setCurrentEnergy(curEnergyProp, newEnergy); - - this.getScene() - .broadcastPacket(new PacketAvatarFightPropUpdateNotify(this.getAvatar(), curEnergyProp)); - this.getScene() - .broadcastPacket( - new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, newEnergy, reason)); - } - } - - public SceneAvatarInfo getSceneAvatarInfo() { - val avatar = this.getAvatar(); - val player = this.getPlayer(); - SceneAvatarInfo.Builder avatarInfo = - SceneAvatarInfo.newBuilder() - .setUid(player.getUid()) - .setAvatarId(avatar.getAvatarId()) - .setGuid(avatar.getGuid()) - .setPeerId(player.getPeerId()) - .addAllTalentIdList(avatar.getTalentIdList()) - .setCoreProudSkillLevel(avatar.getCoreProudSkillLevel()) - .putAllSkillLevelMap(avatar.getSkillLevelMap()) - .setSkillDepotId(avatar.getSkillDepotId()) - .addAllInherentProudSkillList(avatar.getProudSkillList()) - .putAllProudSkillExtraLevelMap(avatar.getProudSkillBonusMap()) - .addAllTeamResonanceList(player.getTeamManager().getTeamResonances()) - .setWearingFlycloakId(avatar.getFlyCloak()) - .setCostumeId(avatar.getCostume()) - .setBornTime(avatar.getBornTime()); - - for (GameItem item : avatar.getEquips().values()) { - if (item.getItemData().getEquipType() == EquipType.EQUIP_WEAPON) { - avatarInfo.setWeapon(item.createSceneWeaponInfo()); - } else { - avatarInfo.addReliquaryList(item.createSceneReliquaryInfo()); - } - avatarInfo.addEquipIdList(item.getItemId()); - } - - return avatarInfo.build(); - } - - @Override - public SceneEntityInfo toProto() { - EntityAuthorityInfo authority = - EntityAuthorityInfo.newBuilder() - .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) - .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) - .setAiInfo( - SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(Vector.newBuilder())) - .setBornPos(Vector.newBuilder()) - .build(); - - SceneEntityInfo.Builder entityInfo = - SceneEntityInfo.newBuilder() - .setEntityId(getId()) - .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_AVATAR) - .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) - .setEntityClientData(EntityClientData.newBuilder()) - .setEntityAuthorityInfo(authority) - .setLastMoveSceneTimeMs(this.getLastMoveSceneTimeMs()) - .setLastMoveReliableSeq(this.getLastMoveReliableSeq()) - .setLifeState(this.getLifeState().getValue()); - - if (this.getScene() != null) { - entityInfo.setMotionInfo(this.getMotionInfo()); - } - - this.addAllFightPropsToEntityInfo(entityInfo); - - PropPair pair = - PropPair.newBuilder() - .setType(PlayerProperty.PROP_LEVEL.getId()) - .setPropValue( - ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, getAvatar().getLevel())) - .build(); - entityInfo.addPropList(pair); - - entityInfo.setAvatar(this.getSceneAvatarInfo()); - - return entityInfo.build(); - } - - public AbilityControlBlock getAbilityControlBlock() { - AvatarData data = this.getAvatar().getAvatarData(); - AbilityControlBlock.Builder abilityControlBlock = AbilityControlBlock.newBuilder(); - int embryoId = 0; - - // Add avatar abilities - if (data.getAbilities() != null) { - for (int id : data.getAbilities()) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(id) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - } - // Add default abilities - for (int id : GameConstants.DEFAULT_ABILITY_HASHES) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(id) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - // Add team resonances - for (int id : this.getPlayer().getTeamManager().getTeamResonancesConfig()) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(id) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - // Add skill depot abilities - AvatarSkillDepotData skillDepot = - GameData.getAvatarSkillDepotDataMap().get(this.getAvatar().getSkillDepotId()); - if (skillDepot != null && skillDepot.getAbilities() != null) { - for (int id : skillDepot.getAbilities()) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(id) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - } - // Add equip abilities - if (this.getAvatar().getExtraAbilityEmbryos().size() > 0) { - for (String skill : this.getAvatar().getExtraAbilityEmbryos()) { - AbilityEmbryo emb = - AbilityEmbryo.newBuilder() - .setAbilityId(++embryoId) - .setAbilityNameHash(Utils.abilityHash(skill)) - .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) - .build(); - abilityControlBlock.addAbilityEmbryoList(emb); - } - } - - // - return abilityControlBlock.build(); - } - - /** - * Move this entity to a new position. Additionally invoke player move event. - * - * @param newPosition The new position. - * @param rotation The new rotation. - */ - @Override - public void move(Position newPosition, Position rotation) { - // Invoke player move event. - PlayerMoveEvent event = - new PlayerMoveEvent( - this.getPlayer(), PlayerMoveEvent.MoveType.PLAYER, this.getPosition(), newPosition); - event.call(); - - // Set position and rotation. - super.move(event.getDestination(), rotation); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.GameConstants; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.avatar.AvatarData; +import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.inventory.EquipType; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.AbilityControlBlockOuterClass.AbilityControlBlock; +import emu.grasscutter.net.proto.AbilityEmbryoOuterClass.AbilityEmbryo; +import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; +import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; +import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason; +import emu.grasscutter.net.proto.ChangeHpReasonOuterClass.ChangeHpReason; +import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; +import emu.grasscutter.net.proto.EntityClientDataOuterClass.EntityClientData; +import emu.grasscutter.net.proto.EntityRendererChangedInfoOuterClass.EntityRendererChangedInfo; +import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; +import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; +import emu.grasscutter.net.proto.PropPairOuterClass.PropPair; +import emu.grasscutter.net.proto.ProtEntityTypeOuterClass.ProtEntityType; +import emu.grasscutter.net.proto.SceneAvatarInfoOuterClass.SceneAvatarInfo; +import emu.grasscutter.net.proto.SceneEntityAiInfoOuterClass.SceneEntityAiInfo; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import emu.grasscutter.server.event.player.PlayerMoveEvent; +import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify; +import emu.grasscutter.server.packet.send.PacketEntityFightPropChangeReasonNotify; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.ProtoHelper; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2FloatMap; +import lombok.Getter; +import lombok.val; + +public class EntityAvatar extends GameEntity { + @Getter private final Avatar avatar; + + @Getter private PlayerDieType killedType; + @Getter private int killedBy; + + public EntityAvatar(Avatar avatar) { + this(null, avatar); + } + + public EntityAvatar(Scene scene, Avatar avatar) { + super(scene); + + this.avatar = avatar; + this.avatar.setCurrentEnergy(); + if (scene != null) this.id = getScene().getWorld().getNextEntityId(EntityIdType.AVATAR); + + GameItem weapon = this.getAvatar().getWeapon(); + if (weapon != null) { + weapon.setWeaponEntityId(getScene().getWorld().getNextEntityId(EntityIdType.WEAPON)); + } + } + + @Override + public int getEntityTypeId() { + return this.getAvatar().getAvatarId(); + } + + public Player getPlayer() { + return this.avatar.getPlayer(); + } + + @Override + public Position getPosition() { + return getPlayer().getPosition(); + } + + @Override + public Position getRotation() { + return getPlayer().getRotation(); + } + + @Override + public boolean isAlive() { + return this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) > 0f; + } + + @Override + public Int2FloatMap getFightProperties() { + return getAvatar().getFightProperties(); + } + + public int getWeaponEntityId() { + if (getAvatar().getWeapon() != null) { + return getAvatar().getWeapon().getWeaponEntityId(); + } + return 0; + } + + @Override + public void onDeath(int killerId) { + super.onDeath(killerId); // Invoke super class's onDeath() method. + + this.killedType = PlayerDieType.PLAYER_DIE_TYPE_KILL_BY_MONSTER; + this.killedBy = killerId; + clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE); + } + + public void onDeath(PlayerDieType dieType, int killerId) { + super.onDeath(killerId); // Invoke super class's onDeath() method. + + this.killedType = dieType; + this.killedBy = killerId; + clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_NONE); + } + + @Override + public float heal(float amount) { + // Do not heal character if they are dead + if (!this.isAlive()) { + return 0f; + } + + float healed = super.heal(amount); + + if (healed > 0f) { + getScene() + .broadcastPacket( + new PacketEntityFightPropChangeReasonNotify( + this, + FightProperty.FIGHT_PROP_CUR_HP, + healed, + PropChangeReason.PROP_CHANGE_REASON_ABILITY, + ChangeHpReason.CHANGE_HP_REASON_ADD_ABILITY)); + } + + return healed; + } + + public void clearEnergy(ChangeEnergyReason reason) { + // Fight props. + val curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp(); + float curEnergy = this.getFightProperty(curEnergyProp); + + // Set energy to zero. + this.avatar.setCurrentEnergy(curEnergyProp, 0); + + // Send packets. + this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp)); + + if (reason == ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START) { + this.getScene() + .broadcastPacket( + new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, -curEnergy, reason)); + } + } + + /** + * Adds a fixed amount of energy to the current avatar. + * + * @param amount The amount of energy to add. + * @return True if the energy was added, false if the energy was not added. + */ + public boolean addEnergy(float amount) { + var curEnergyProp = this.getAvatar().getSkillDepot().getElementType().getCurEnergyProp(); + var curEnergy = this.getFightProperty(curEnergyProp); + if (curEnergy == amount) return false; + + this.getAvatar().setCurrentEnergy(curEnergyProp, amount); + this.getScene().broadcastPacket(new PacketEntityFightPropUpdateNotify(this, curEnergyProp)); + return true; + } + + public void addEnergy(float amount, PropChangeReason reason) { + this.addEnergy(amount, reason, false); + } + + public void addEnergy(float amount, PropChangeReason reason, boolean isFlat) { + // Get current and maximum energy for this avatar. + val elementType = this.getAvatar().getSkillDepot().getElementType(); + val curEnergyProp = elementType.getCurEnergyProp(); + val maxEnergyProp = elementType.getMaxEnergyProp(); + + float curEnergy = this.getFightProperty(curEnergyProp); + float maxEnergy = this.getFightProperty(maxEnergyProp); + + // Scale amount by energy recharge, if the amount is not flat. + if (!isFlat) { + amount *= this.getFightProperty(FightProperty.FIGHT_PROP_CHARGE_EFFICIENCY); + } + + // Determine the new energy value. + float newEnergy = Math.min(curEnergy + amount, maxEnergy); + + // Set energy and notify. + if (newEnergy != curEnergy) { + this.avatar.setCurrentEnergy(curEnergyProp, newEnergy); + + this.getScene() + .broadcastPacket(new PacketAvatarFightPropUpdateNotify(this.getAvatar(), curEnergyProp)); + this.getScene() + .broadcastPacket( + new PacketEntityFightPropChangeReasonNotify(this, curEnergyProp, newEnergy, reason)); + } + } + + public SceneAvatarInfo getSceneAvatarInfo() { + val avatar = this.getAvatar(); + val player = this.getPlayer(); + SceneAvatarInfo.Builder avatarInfo = + SceneAvatarInfo.newBuilder() + .setUid(player.getUid()) + .setAvatarId(avatar.getAvatarId()) + .setGuid(avatar.getGuid()) + .setPeerId(player.getPeerId()) + .addAllTalentIdList(avatar.getTalentIdList()) + .setCoreProudSkillLevel(avatar.getCoreProudSkillLevel()) + .putAllSkillLevelMap(avatar.getSkillLevelMap()) + .setSkillDepotId(avatar.getSkillDepotId()) + .addAllInherentProudSkillList(avatar.getProudSkillList()) + .putAllProudSkillExtraLevelMap(avatar.getProudSkillBonusMap()) + .addAllTeamResonanceList(player.getTeamManager().getTeamResonances()) + .setWearingFlycloakId(avatar.getFlyCloak()) + .setCostumeId(avatar.getCostume()) + .setBornTime(avatar.getBornTime()); + + for (GameItem item : avatar.getEquips().values()) { + if (item.getItemData().getEquipType() == EquipType.EQUIP_WEAPON) { + avatarInfo.setWeapon(item.createSceneWeaponInfo()); + } else { + avatarInfo.addReliquaryList(item.createSceneReliquaryInfo()); + } + avatarInfo.addEquipIdList(item.getItemId()); + } + + return avatarInfo.build(); + } + + @Override + public SceneEntityInfo toProto() { + EntityAuthorityInfo authority = + EntityAuthorityInfo.newBuilder() + .setAbilityInfo(AbilitySyncStateInfo.newBuilder()) + .setRendererChangedInfo(EntityRendererChangedInfo.newBuilder()) + .setAiInfo( + SceneEntityAiInfo.newBuilder().setIsAiOpen(true).setBornPos(Vector.newBuilder())) + .setBornPos(Vector.newBuilder()) + .build(); + + SceneEntityInfo.Builder entityInfo = + SceneEntityInfo.newBuilder() + .setEntityId(getId()) + .setEntityType(ProtEntityType.PROT_ENTITY_TYPE_AVATAR) + .addAnimatorParaList(AnimatorParameterValueInfoPair.newBuilder()) + .setEntityClientData(EntityClientData.newBuilder()) + .setEntityAuthorityInfo(authority) + .setLastMoveSceneTimeMs(this.getLastMoveSceneTimeMs()) + .setLastMoveReliableSeq(this.getLastMoveReliableSeq()) + .setLifeState(this.getLifeState().getValue()); + + if (this.getScene() != null) { + entityInfo.setMotionInfo(this.getMotionInfo()); + } + + this.addAllFightPropsToEntityInfo(entityInfo); + + PropPair pair = + PropPair.newBuilder() + .setType(PlayerProperty.PROP_LEVEL.getId()) + .setPropValue( + ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, getAvatar().getLevel())) + .build(); + entityInfo.addPropList(pair); + + entityInfo.setAvatar(this.getSceneAvatarInfo()); + + return entityInfo.build(); + } + + public AbilityControlBlock getAbilityControlBlock() { + AvatarData data = this.getAvatar().getAvatarData(); + AbilityControlBlock.Builder abilityControlBlock = AbilityControlBlock.newBuilder(); + int embryoId = 0; + + // Add avatar abilities + if (data.getAbilities() != null) { + for (int id : data.getAbilities()) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(id) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + } + // Add default abilities + for (int id : GameConstants.DEFAULT_ABILITY_HASHES) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(id) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + // Add team resonances + for (int id : this.getPlayer().getTeamManager().getTeamResonancesConfig()) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(id) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + // Add skill depot abilities + AvatarSkillDepotData skillDepot = + GameData.getAvatarSkillDepotDataMap().get(this.getAvatar().getSkillDepotId()); + if (skillDepot != null && skillDepot.getAbilities() != null) { + for (int id : skillDepot.getAbilities()) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(id) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + } + // Add equip abilities + if (this.getAvatar().getExtraAbilityEmbryos().size() > 0) { + for (String skill : this.getAvatar().getExtraAbilityEmbryos()) { + AbilityEmbryo emb = + AbilityEmbryo.newBuilder() + .setAbilityId(++embryoId) + .setAbilityNameHash(Utils.abilityHash(skill)) + .setAbilityOverrideNameHash(GameConstants.DEFAULT_ABILITY_NAME) + .build(); + abilityControlBlock.addAbilityEmbryoList(emb); + } + } + + // + return abilityControlBlock.build(); + } + + /** + * Move this entity to a new position. Additionally invoke player move event. + * + * @param newPosition The new position. + * @param rotation The new rotation. + */ + @Override + public void move(Position newPosition, Position rotation) { + // Invoke player move event. + PlayerMoveEvent event = + new PlayerMoveEvent( + this.getPlayer(), PlayerMoveEvent.MoveType.PLAYER, this.getPosition(), newPosition); + event.call(); + + // Set position and rotation. + super.move(event.getDestination(), rotation); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java b/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java index 6bbc9f777..bdd067d1f 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityBaseGadget.java @@ -1,63 +1,91 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.data.binout.config.ConfigEntityGadget; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.utils.Position; -import lombok.Getter; - -public abstract class EntityBaseGadget extends GameEntity { - @Getter(onMethod_ = @Override) - protected final Position position; - - @Getter(onMethod_ = @Override) - protected final Position rotation; - - public EntityBaseGadget(Scene scene) { - this(scene, null, null); - } - - public EntityBaseGadget(Scene scene, Position position, Position rotation) { - super(scene); - this.position = position != null ? position.clone() : new Position(); - this.rotation = rotation != null ? rotation.clone() : new Position(); - } - - public abstract int getGadgetId(); - - @Override - public int getEntityTypeId() { - return this.getGadgetId(); - } - - @Override - public void onDeath(int killerId) { - super.onDeath(killerId); // Invoke super class's onDeath() method. - } - - protected void fillFightProps(ConfigEntityGadget configGadget) { - if (configGadget == null || configGadget.getCombat() == null) { - return; - } - var combatData = configGadget.getCombat(); - var combatProperties = combatData.getProperty(); - - var targetHp = combatProperties.getHP(); - setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, targetHp); - setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, targetHp); - if (combatProperties.isInvincible()) { - targetHp = Float.POSITIVE_INFINITY; - } - setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, targetHp); - - var atk = combatProperties.getAttack(); - setFightProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, atk); - setFightProperty(FightProperty.FIGHT_PROP_CUR_ATTACK, atk); - - var def = combatProperties.getDefence(); - setFightProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, def); - setFightProperty(FightProperty.FIGHT_PROP_CUR_DEFENSE, def); - - setLockHP(combatProperties.isLockHP()); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.data.binout.config.ConfigEntityGadget; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.event.entity.EntityDamageEvent; +import emu.grasscutter.utils.Position; +import lombok.Getter; + +import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_GADGET_HP_CHANGE; + +public abstract class EntityBaseGadget extends GameEntity { + @Getter(onMethod_ = @Override) + protected final Position position; + + @Getter(onMethod_ = @Override) + protected final Position rotation; + + public EntityBaseGadget(Scene scene) { + this(scene, null, null); + } + + public EntityBaseGadget(Scene scene, Position position, Position rotation) { + super(scene); + this.position = position != null ? position.clone() : new Position(); + this.rotation = rotation != null ? rotation.clone() : new Position(); + } + + public abstract int getGadgetId(); + + @Override + public int getEntityTypeId() { + return this.getGadgetId(); + } + + @Override + public void onDeath(int killerId) { + super.onDeath(killerId); // Invoke super class's onDeath() method. + + getScene() + .getPlayers() + .forEach( + p -> + p.getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_DESTROY_GADGET, this.getGadgetId())); + } + + @Override + public void runLuaCallbacks(EntityDamageEvent event) { + super.runLuaCallbacks(event); + getScene() + .getScriptManager() + .callEvent( + new ScriptArgs( + this.getGroupId(), + EVENT_SPECIFIC_GADGET_HP_CHANGE, + getConfigId(), + getGadgetId()) + .setSourceEntityId(getId()) + .setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) + .setEventSource(Integer.toString(getConfigId()))); + } + + protected void fillFightProps(ConfigEntityGadget configGadget) { + if (configGadget == null || configGadget.getCombat() == null) { + return; + } + var combatData = configGadget.getCombat(); + var combatProperties = combatData.getProperty(); + + var targetHp = combatProperties.getHP(); + setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, targetHp); + setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, targetHp); + if (combatProperties.isInvincible()) { + targetHp = Float.POSITIVE_INFINITY; + } + setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, targetHp); + + var atk = combatProperties.getAttack(); + setFightProperty(FightProperty.FIGHT_PROP_BASE_ATTACK, atk); + setFightProperty(FightProperty.FIGHT_PROP_CUR_ATTACK, atk); + + var def = combatProperties.getDefence(); + setFightProperty(FightProperty.FIGHT_PROP_BASE_DEFENSE, def); + setFightProperty(FightProperty.FIGHT_PROP_CUR_DEFENSE, def); + + setLockHP(combatProperties.isLockHP()); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityRegion.java b/src/main/java/emu/grasscutter/game/entity/EntityRegion.java index 15bca8125..5fe55ce63 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityRegion.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityRegion.java @@ -1,96 +1,96 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.SceneEntityInfoOuterClass; -import emu.grasscutter.scripts.data.SceneRegion; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import lombok.Getter; - -@Getter -public class EntityRegion extends GameEntity { - private final Position position; - private final Set entities; // Ids of entities inside this region - private final SceneRegion metaRegion; - private boolean hasNewEntities; - private boolean entityLeave; - - public EntityRegion(Scene scene, SceneRegion region) { - super(scene); - - this.id = getScene().getWorld().getNextEntityId(EntityIdType.REGION); - this.setGroupId(region.group.id); - this.setBlockId(region.group.block_id); - this.setConfigId(region.config_id); - this.position = region.pos.clone(); - this.entities = ConcurrentHashMap.newKeySet(); - this.metaRegion = region; - } - - @Override - public int getEntityTypeId() { - return this.metaRegion.config_id; - } - - public void addEntity(GameEntity entity) { - if (this.getEntities().contains(entity.getId())) { - return; - } - this.getEntities().add(entity.getId()); - this.hasNewEntities = true; - } - - public boolean hasNewEntities() { - return hasNewEntities; - } - - public void resetNewEntities() { - hasNewEntities = false; - } - - public void removeEntity(int entityId) { - this.getEntities().remove(entityId); - this.entityLeave = true; - } - - public void removeEntity(GameEntity entity) { - this.getEntities().remove(entity.getId()); - this.entityLeave = true; - } - - public boolean entityLeave() { - return this.entityLeave; - } - - public void resetEntityLeave() { - this.entityLeave = false; - } - - @Override - public Int2FloatMap getFightProperties() { - return null; - } - - @Override - public Position getPosition() { - return position; - } - - @Override - public Position getRotation() { - return null; - } - - @Override - public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { - /** The Region Entity would not be sent to client. */ - return null; - } - - public int getFirstEntityId() { - return entities.stream().findFirst().orElse(0); - } -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass; +import emu.grasscutter.scripts.data.SceneRegion; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2FloatMap; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import lombok.Getter; + +@Getter +public class EntityRegion extends GameEntity { + private final Position position; + private final Set entities; // Ids of entities inside this region + private final SceneRegion metaRegion; + private boolean hasNewEntities; + private boolean entityLeave; + + public EntityRegion(Scene scene, SceneRegion region) { + super(scene); + + this.id = getScene().getWorld().getNextEntityId(EntityIdType.REGION); + this.setGroupId(region.group.id); + this.setBlockId(region.group.block_id); + this.setConfigId(region.config_id); + this.position = region.pos.clone(); + this.entities = ConcurrentHashMap.newKeySet(); + this.metaRegion = region; + } + + @Override + public int getEntityTypeId() { + return this.metaRegion.config_id; + } + + public void addEntity(GameEntity entity) { + if (this.getEntities().contains(entity.getId())) { + return; + } + this.getEntities().add(entity.getId()); + this.hasNewEntities = true; + } + + public boolean hasNewEntities() { + return hasNewEntities; + } + + public void resetNewEntities() { + hasNewEntities = false; + } + + public void removeEntity(int entityId) { + this.getEntities().remove(entityId); + this.entityLeave = true; + } + + public void removeEntity(GameEntity entity) { + this.getEntities().remove(entity.getId()); + this.entityLeave = true; + } + + public boolean entityLeave() { + return this.entityLeave; + } + + public void resetEntityLeave() { + this.entityLeave = false; + } + + @Override + public Int2FloatMap getFightProperties() { + return null; + } + + @Override + public Position getPosition() { + return position; + } + + @Override + public Position getRotation() { + return null; + } + + @Override + public SceneEntityInfoOuterClass.SceneEntityInfo toProto() { + /** The Region Entity would not be sent to client. */ + return null; + } + + public int getFirstEntityId() { + return entities.stream().findFirst().orElse(0); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/GameEntity.java b/src/main/java/emu/grasscutter/game/entity/GameEntity.java index 7b0e87454..1ec6ed5bf 100644 --- a/src/main/java/emu/grasscutter/game/entity/GameEntity.java +++ b/src/main/java/emu/grasscutter/game/entity/GameEntity.java @@ -1,264 +1,271 @@ -package emu.grasscutter.game.entity; - -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ElementType; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.LifeState; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.game.world.SpawnDataEntry; -import emu.grasscutter.game.world.World; -import emu.grasscutter.net.proto.FightPropPairOuterClass.FightPropPair; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; -import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; -import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; -import emu.grasscutter.net.proto.VectorOuterClass.Vector; -import emu.grasscutter.scripts.data.controller.EntityController; -import emu.grasscutter.server.event.entity.EntityDamageEvent; -import emu.grasscutter.server.event.entity.EntityDeathEvent; -import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2FloatMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2FloatMap; -import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap; -import lombok.Getter; -import lombok.Setter; - -public abstract class GameEntity { - @Getter private final Scene scene; - @Getter protected int id; - @Getter @Setter private SpawnDataEntry spawnEntry; - - @Getter @Setter private int blockId; - @Getter @Setter private int configId; - @Getter @Setter private int groupId; - - @Getter @Setter private MotionState motionState; - @Getter @Setter private int lastMoveSceneTimeMs; - @Getter @Setter private int lastMoveReliableSeq; - - @Getter @Setter private boolean lockHP; - - // Lua controller for specific actions - @Getter @Setter private EntityController entityController; - @Getter private ElementType lastAttackType = ElementType.None; - - // Abilities - private Object2FloatMap metaOverrideMap; - private Int2ObjectMap metaModifiers; - - public GameEntity(Scene scene) { - this.scene = scene; - this.motionState = MotionState.MOTION_STATE_NONE; - } - - public int getEntityType() { - return this.getId() >> 24; - } - - public abstract int getEntityTypeId(); - - public World getWorld() { - return this.getScene().getWorld(); - } - - public boolean isAlive() { - return true; - } - - public LifeState getLifeState() { - return this.isAlive() ? LifeState.LIFE_ALIVE : LifeState.LIFE_DEAD; - } - - public Object2FloatMap getMetaOverrideMap() { - if (this.metaOverrideMap == null) { - this.metaOverrideMap = new Object2FloatOpenHashMap<>(); - } - return this.metaOverrideMap; - } - - public Int2ObjectMap getMetaModifiers() { - if (this.metaModifiers == null) { - this.metaModifiers = new Int2ObjectOpenHashMap<>(); - } - return this.metaModifiers; - } - - public abstract Int2FloatMap getFightProperties(); - - public abstract Position getPosition(); - - public abstract Position getRotation(); - - public void setFightProperty(FightProperty prop, float value) { - this.getFightProperties().put(prop.getId(), value); - } - - public void setFightProperty(int id, float value) { - this.getFightProperties().put(id, value); - } - - public void addFightProperty(FightProperty prop, float value) { - this.getFightProperties().put(prop.getId(), this.getFightProperty(prop) + value); - } - - public float getFightProperty(FightProperty prop) { - return this.getFightProperties().getOrDefault(prop.getId(), 0f); - } - - public boolean hasFightProperty(FightProperty prop) { - return this.getFightProperties().containsKey(prop.getId()); - } - - public void addAllFightPropsToEntityInfo(SceneEntityInfo.Builder entityInfo) { - this.getFightProperties() - .forEach( - (key, value) -> { - if (key == 0) return; - entityInfo.addFightPropList( - FightPropPair.newBuilder().setPropType(key).setPropValue(value).build()); - }); - } - - protected MotionInfo getMotionInfo() { - return MotionInfo.newBuilder() - .setPos(this.getPosition().toProto()) - .setRot(this.getRotation().toProto()) - .setSpeed(Vector.newBuilder()) - .setState(this.getMotionState()) - .build(); - } - - public float heal(float amount) { - if (this.getFightProperties() == null) { - return 0f; - } - - float curHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - float maxHp = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - - if (curHp >= maxHp) { - return 0f; - } - - float healed = Math.min(maxHp - curHp, amount); - this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, healed); - - this.getScene() - .broadcastPacket( - new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); - - return healed; - } - - public void damage(float amount) { - this.damage(amount, 0, ElementType.None); - } - - public void damage(float amount, int killerId, ElementType attackType) { - // Check if the entity has properties. - if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) { - return; - } - - // Invoke entity damage event. - EntityDamageEvent event = - new EntityDamageEvent(this, amount, attackType, this.getScene().getEntityById(killerId)); - event.call(); - if (event.isCanceled()) { - return; // If the event is canceled, do not damage the entity. - } - - float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); - if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) { - // Add negative HP to the current HP property. - this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage())); - } - - // Check if dead - boolean isDead = false; - if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) { - this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f); - isDead = true; - } - - // Packets - this.getScene() - .broadcastPacket( - new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); - - // Check if dead. - if (isDead) { - this.getScene().killEntity(this, killerId); - } - } - - /** - * Runs the Lua callbacks for {@link EntityDamageEvent}. - * - * @param event The damage event. - */ - public void runLuaCallbacks(EntityDamageEvent event) { - if (entityController != null) { - entityController.onBeHurt(this, event.getAttackElementType(), true); // todo is host handling - } - } - - /** - * Move this entity to a new position. - * - * @param position The new position. - * @param rotation The new rotation. - */ - public void move(Position position, Position rotation) { - // Set the position and rotation. - this.getPosition().set(position); - this.getRotation().set(rotation); - } - - /** - * Called when a player interacts with this entity - * - * @param player Player that is interacting with this entity - * @param interactReq Interact request protobuf data - */ - public void onInteract(Player player, GadgetInteractReq interactReq) {} - - /** Called when this entity is added to the world */ - public void onCreate() {} - - public void onRemoved() {} - - public void onTick(int sceneTime) { - if (entityController != null) { - entityController.onTimer(this, sceneTime); - } - } - - public int onClientExecuteRequest(int param1, int param2, int param3) { - if (entityController != null) { - return entityController.onClientExecuteRequest(this, param1, param2, param3); - } - return 0; - } - - /** - * Called when this entity dies - * - * @param killerId Entity id of the entity that killed this entity - */ - public void onDeath(int killerId) { - // Invoke entity death event. - EntityDeathEvent event = new EntityDeathEvent(this, killerId); - event.call(); - - // Run Lua callbacks. - if (entityController != null) { - entityController.onDie(this, getLastAttackType()); - } - } - - public abstract SceneEntityInfo toProto(); -} +package emu.grasscutter.game.entity; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ElementType; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.LifeState; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.game.world.SpawnDataEntry; +import emu.grasscutter.game.world.World; +import emu.grasscutter.net.proto.FightPropPairOuterClass.FightPropPair; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.net.proto.MotionInfoOuterClass.MotionInfo; +import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; +import emu.grasscutter.net.proto.SceneEntityInfoOuterClass.SceneEntityInfo; +import emu.grasscutter.net.proto.VectorOuterClass.Vector; +import emu.grasscutter.scripts.data.controller.EntityController; +import emu.grasscutter.server.event.entity.EntityDamageEvent; +import emu.grasscutter.server.event.entity.EntityDeathEvent; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2FloatMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2FloatMap; +import it.unimi.dsi.fastutil.objects.Object2FloatOpenHashMap; +import lombok.Getter; +import lombok.Setter; + +public abstract class GameEntity { + @Getter private final Scene scene; + @Getter protected int id; + @Getter @Setter private SpawnDataEntry spawnEntry; + + @Getter @Setter private int blockId; + @Getter @Setter private int configId; + @Getter @Setter private int groupId; + + @Getter @Setter private MotionState motionState; + @Getter @Setter private int lastMoveSceneTimeMs; + @Getter @Setter private int lastMoveReliableSeq; + + @Getter @Setter private boolean lockHP; + + // Lua controller for specific actions + @Getter @Setter private EntityController entityController; + @Getter private ElementType lastAttackType = ElementType.None; + + // Abilities + private Object2FloatMap metaOverrideMap; + private Int2ObjectMap metaModifiers; + + public GameEntity(Scene scene) { + this.scene = scene; + this.motionState = MotionState.MOTION_STATE_NONE; + } + + public int getEntityType() { + return this.getId() >> 24; + } + + public abstract int getEntityTypeId(); + + public World getWorld() { + return this.getScene().getWorld(); + } + + public boolean isAlive() { + return true; + } + + public LifeState getLifeState() { + return this.isAlive() ? LifeState.LIFE_ALIVE : LifeState.LIFE_DEAD; + } + + public Object2FloatMap getMetaOverrideMap() { + if (this.metaOverrideMap == null) { + this.metaOverrideMap = new Object2FloatOpenHashMap<>(); + } + return this.metaOverrideMap; + } + + public Int2ObjectMap getMetaModifiers() { + if (this.metaModifiers == null) { + this.metaModifiers = new Int2ObjectOpenHashMap<>(); + } + return this.metaModifiers; + } + + public abstract Int2FloatMap getFightProperties(); + + public abstract Position getPosition(); + + public abstract Position getRotation(); + + public void setFightProperty(FightProperty prop, float value) { + this.getFightProperties().put(prop.getId(), value); + } + + public void setFightProperty(int id, float value) { + this.getFightProperties().put(id, value); + } + + public void addFightProperty(FightProperty prop, float value) { + this.getFightProperties().put(prop.getId(), this.getFightProperty(prop) + value); + } + + public float getFightProperty(FightProperty prop) { + return this.getFightProperties().getOrDefault(prop.getId(), 0f); + } + + public boolean hasFightProperty(FightProperty prop) { + return this.getFightProperties().containsKey(prop.getId()); + } + + public void addAllFightPropsToEntityInfo(SceneEntityInfo.Builder entityInfo) { + this.getFightProperties() + .forEach( + (key, value) -> { + if (key == 0) return; + entityInfo.addFightPropList( + FightPropPair.newBuilder().setPropType(key).setPropValue(value).build()); + }); + } + + protected MotionInfo getMotionInfo() { + return MotionInfo.newBuilder() + .setPos(this.getPosition().toProto()) + .setRot(this.getRotation().toProto()) + .setSpeed(Vector.newBuilder()) + .setState(this.getMotionState()) + .build(); + } + + public float heal(float amount) { + if (this.getFightProperties() == null) { + return 0f; + } + + float curHp = this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + float maxHp = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + + if (curHp >= maxHp) { + return 0f; + } + + float healed = Math.min(maxHp - curHp, amount); + this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, healed); + + this.getScene() + .broadcastPacket( + new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); + + return healed; + } + + public void damage(float amount) { + this.damage(amount, 0, ElementType.None); + } + + public void damage(float amount, ElementType attackType) { + this.damage(amount, 0, attackType); + } + + public void damage(float amount, int killerId, ElementType attackType) { + // Check if the entity has properties. + if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) { + return; + } + + // Invoke entity damage event. + EntityDamageEvent event = + new EntityDamageEvent(this, amount, attackType, this.getScene().getEntityById(killerId)); + event.call(); + if (event.isCanceled()) { + return; // If the event is canceled, do not damage the entity. + } + + float curHp = getFightProperty(FightProperty.FIGHT_PROP_CUR_HP); + if (curHp != Float.POSITIVE_INFINITY && !lockHP || lockHP && curHp <= event.getDamage()) { + // Add negative HP to the current HP property. + this.addFightProperty(FightProperty.FIGHT_PROP_CUR_HP, -(event.getDamage())); + } + + this.lastAttackType = attackType; + + // Check if dead + boolean isDead = false; + if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) { + this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f); + isDead = true; + } + this.runLuaCallbacks(event); + + // Packets + this.getScene() + .broadcastPacket( + new PacketEntityFightPropUpdateNotify(this, FightProperty.FIGHT_PROP_CUR_HP)); + + // Check if dead. + if (isDead) { + this.getScene().killEntity(this, killerId); + } + } + + /** + * Runs the Lua callbacks for {@link EntityDamageEvent}. + * + * @param event The damage event. + */ + public void runLuaCallbacks(EntityDamageEvent event) { + if (entityController != null) { + entityController.onBeHurt(this, event.getAttackElementType(), true); // todo is host handling + } + } + + /** + * Move this entity to a new position. + * + * @param position The new position. + * @param rotation The new rotation. + */ + public void move(Position position, Position rotation) { + // Set the position and rotation. + this.getPosition().set(position); + this.getRotation().set(rotation); + } + + /** + * Called when a player interacts with this entity + * + * @param player Player that is interacting with this entity + * @param interactReq Interact request protobuf data + */ + public void onInteract(Player player, GadgetInteractReq interactReq) {} + + /** Called when this entity is added to the world */ + public void onCreate() {} + + public void onRemoved() {} + + public void onTick(int sceneTime) { + if (entityController != null) { + entityController.onTimer(this, sceneTime); + } + } + + public int onClientExecuteRequest(int param1, int param2, int param3) { + if (entityController != null) { + return entityController.onClientExecuteRequest(this, param1, param2, param3); + } + return 0; + } + + /** + * Called when this entity dies + * + * @param killerId Entity id of the entity that killed this entity + */ + public void onDeath(int killerId) { + // Invoke entity death event. + EntityDeathEvent event = new EntityDeathEvent(this, killerId); + event.call(); + + // Run Lua callbacks. + if (entityController != null) { + entityController.onDie(this, getLastAttackType()); + } + } + + public abstract SceneEntityInfo toProto(); +} diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetGatherObject.java b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetGatherObject.java index a37fd8125..273615a62 100644 --- a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetGatherObject.java +++ b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetGatherObject.java @@ -1,86 +1,98 @@ -package emu.grasscutter.game.entity.gadget; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.ItemData; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityItem; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.net.proto.GatherGadgetInfoOuterClass.GatherGadgetInfo; -import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType; -import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; -import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp; -import emu.grasscutter.utils.Utils; - -public class GadgetGatherObject extends GadgetContent { - private int itemId; - private boolean isForbidGuest; - - public GadgetGatherObject(EntityGadget gadget) { - super(gadget); - - if (gadget.getSpawnEntry() != null) { - this.itemId = gadget.getSpawnEntry().getGatherItemId(); - } - } - - public int getItemId() { - return this.itemId; - } - - public boolean isForbidGuest() { - return isForbidGuest; - } - - public boolean onInteract(Player player, GadgetInteractReq req) { - // Sanity check - ItemData itemData = GameData.getItemDataMap().get(getItemId()); - if (itemData == null) { - return false; - } - - GameItem item = new GameItem(itemData, 1); - player.getInventory().addItem(item, ActionReason.Gather); - - getGadget() - .getScene() - .broadcastPacket( - new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_GATHER)); - - return true; - } - - public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) { - GatherGadgetInfo gatherGadgetInfo = - GatherGadgetInfo.newBuilder() - .setItemId(this.getItemId()) - .setIsForbidGuest(this.isForbidGuest()) - .build(); - - gadgetInfo.setGatherGadget(gatherGadgetInfo); - } - - public void dropItems(Player player) { - Scene scene = getGadget().getScene(); - int times = Utils.randomRange(1, 2); - - for (int i = 0; i < times; i++) { - EntityItem item = - new EntityItem( - scene, - player, - GameData.getItemDataMap().get(itemId), - getGadget().getPosition().nearby2d(1f).addY(2f), - 1, - true); - - scene.addEntity(item); - } - - scene.killEntity(this.getGadget(), player.getTeamManager().getCurrentAvatarEntity().getId()); - // Todo: add record - } -} +package emu.grasscutter.game.entity.gadget; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.GatherData; +import emu.grasscutter.data.excels.ItemData; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.EntityItem; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.net.proto.GatherGadgetInfoOuterClass.GatherGadgetInfo; +import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType; +import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; +import emu.grasscutter.server.packet.send.PacketGadgetInteractRsp; +import emu.grasscutter.utils.Utils; + +public final class GadgetGatherObject extends GadgetContent { + private int itemId; + private boolean isForbidGuest; + + public GadgetGatherObject(EntityGadget gadget) { + super(gadget); + + // overwrites the default spawn handling + if (gadget.getSpawnEntry() != null) { + this.itemId = gadget.getSpawnEntry().getGatherItemId(); + return; + } + + GatherData gatherData = GameData.getGatherDataMap().get(gadget.getPointType()); + if (gatherData != null) { + this.itemId = gatherData.getItemId(); + this.isForbidGuest = gatherData.isForbidGuest(); + } else { + Grasscutter.getLogger().error("invalid gather object: {}", gadget.getConfigId()); + } + } + + public int getItemId() { + return this.itemId; + } + + public boolean isForbidGuest() { + return isForbidGuest; + } + + public boolean onInteract(Player player, GadgetInteractReq req) { + // Sanity check + ItemData itemData = GameData.getItemDataMap().get(getItemId()); + if (itemData == null) { + return false; + } + + GameItem item = new GameItem(itemData, 1); + player.getInventory().addItem(item, ActionReason.Gather); + + getGadget() + .getScene() + .broadcastPacket( + new PacketGadgetInteractRsp(getGadget(), InteractType.INTERACT_TYPE_GATHER)); + + return true; + } + + public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) { + GatherGadgetInfo gatherGadgetInfo = + GatherGadgetInfo.newBuilder() + .setItemId(this.getItemId()) + .setIsForbidGuest(this.isForbidGuest()) + .build(); + + gadgetInfo.setGatherGadget(gatherGadgetInfo); + } + + public void dropItems(Player player) { + Scene scene = getGadget().getScene(); + int times = Utils.randomRange(1, 2); + + for (int i = 0; i < times; i++) { + EntityItem item = + new EntityItem( + scene, + player, + GameData.getItemDataMap().get(itemId), + getGadget().getPosition().nearby2d(1f).addY(2f), + 1, + true); + + scene.addEntity(item); + } + + scene.killEntity(this.getGadget(), player.getTeamManager().getCurrentAvatarEntity().getId()); + // Todo: add record + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetGatherPoint.java b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetGatherPoint.java index b5f44152c..268be27bd 100644 --- a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetGatherPoint.java +++ b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetGatherPoint.java @@ -1,83 +1,65 @@ -package emu.grasscutter.game.entity.gadget; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.GatherData; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityItem; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.net.proto.GatherGadgetInfoOuterClass.GatherGadgetInfo; -import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; -import emu.grasscutter.utils.Utils; - -public class GadgetGatherPoint extends GadgetContent { - private final int itemId; - private boolean isForbidGuest; - - public GadgetGatherPoint(EntityGadget gadget) { - super(gadget); - - if (gadget.getSpawnEntry() != null) { - this.itemId = gadget.getSpawnEntry().getGatherItemId(); - } else { - GatherData gatherData = GameData.getGatherDataMap().get(gadget.getPointType()); - this.itemId = gatherData.getItemId(); - this.isForbidGuest = gatherData.isForbidGuest(); - } - } - - public int getItemId() { - return this.itemId; - } - - public boolean isForbidGuest() { - return isForbidGuest; - } - - public boolean onInteract(Player player, GadgetInteractReq req) { - GameItem item = new GameItem(getItemId(), 1); - - player.getInventory().addItem(item, ActionReason.Gather); - - return true; - } - - public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) { - GatherGadgetInfo gatherGadgetInfo = - GatherGadgetInfo.newBuilder() - .setItemId(this.getItemId()) - .setIsForbidGuest(this.isForbidGuest()) - .build(); - - gadgetInfo.setGatherGadget(gatherGadgetInfo); - } - - public void dropItems(Player player) { - Scene scene = getGadget().getScene(); - int times = Utils.randomRange(1, 2); - - for (int i = 0; i < times; i++) { - EntityItem item = - new EntityItem( - scene, - player, - GameData.getItemDataMap().get(itemId), - getGadget() - .getPosition() - .clone() - .addY(2f) - .addX(Utils.randomFloatRange(-1f, 1f)) - .addZ(Utils.randomFloatRange(-1f, 1f)), - 1, - true); - - scene.addEntity(item); - } - - scene.killEntity(this.getGadget(), player.getTeamManager().getCurrentAvatarEntity().getId()); - // Todo: add record - } -} +package emu.grasscutter.game.entity.gadget; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.GatherData; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.net.proto.GatherGadgetInfoOuterClass.GatherGadgetInfo; +import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; + +/** Spawner for the gather objects */ +public final class GadgetGatherPoint extends GadgetContent { + private final GatherData gatherData; + private final EntityGadget gatherObjectChild; + + public GadgetGatherPoint(EntityGadget gadget) { + super(gadget); + + this.gatherData = GameData.getGatherDataMap().get(gadget.getPointType()); + + var scene = gadget.getScene(); + gatherObjectChild = new EntityGadget(scene, gatherData.getGadgetId(), gadget.getBornPos()); + + gatherObjectChild.setBlockId(gadget.getBlockId()); + gatherObjectChild.setConfigId(gadget.getConfigId()); + gatherObjectChild.setGroupId(gadget.getGroupId()); + gatherObjectChild.getRotation().set(gadget.getRotation()); + gatherObjectChild.setState(gadget.getState()); + gatherObjectChild.setPointType(gadget.getPointType()); + gatherObjectChild.setMetaGadget(gadget.getMetaGadget()); + gatherObjectChild.buildContent(); + + gadget.getChildren().add(gatherObjectChild); + scene.addEntity(gatherObjectChild); + } + + public int getItemId() { + return this.gatherData.getItemId(); + } + + public boolean isForbidGuest() { + return this.gatherData.isForbidGuest(); + } + + public boolean onInteract(Player player, GadgetInteractReq req) { + GameItem item = new GameItem(getItemId(), 1); + + player.getInventory().addItem(item, ActionReason.Gather); + + return true; + } + + public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) { + // todo does official use this for the spawners? + GatherGadgetInfo gatherGadgetInfo = + GatherGadgetInfo.newBuilder() + .setItemId(this.getItemId()) + .setIsForbidGuest(this.isForbidGuest()) + .build(); + + gadgetInfo.setGatherGadget(gatherGadgetInfo); + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetWorktop.java b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetWorktop.java index 3a5976b9b..85534434b 100644 --- a/src/main/java/emu/grasscutter/game/entity/gadget/GadgetWorktop.java +++ b/src/main/java/emu/grasscutter/game/entity/gadget/GadgetWorktop.java @@ -1,65 +1,68 @@ -package emu.grasscutter.game.entity.gadget; - -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.gadget.worktop.WorktopWorktopOptionHandler; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; -import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; -import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq; -import emu.grasscutter.net.proto.WorktopInfoOuterClass.WorktopInfo; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; -import java.util.Arrays; - -public class GadgetWorktop extends GadgetContent { - private IntSet worktopOptions; - private WorktopWorktopOptionHandler handler; - - public GadgetWorktop(EntityGadget gadget) { - super(gadget); - } - - public IntSet getWorktopOptions() { - return worktopOptions; - } - - public void addWorktopOptions(int[] options) { - if (this.worktopOptions == null) { - this.worktopOptions = new IntOpenHashSet(); - } - Arrays.stream(options).forEach(this.worktopOptions::add); - } - - public void removeWorktopOption(int option) { - if (this.worktopOptions == null) { - return; - } - this.worktopOptions.remove(option); - } - - public boolean onInteract(Player player, GadgetInteractReq req) { - return false; - } - - public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) { - if (this.worktopOptions == null) { - return; - } - - WorktopInfo worktop = - WorktopInfo.newBuilder().addAllOptionList(this.getWorktopOptions()).build(); - - gadgetInfo.setWorktop(worktop); - } - - public void setOnSelectWorktopOptionEvent(WorktopWorktopOptionHandler handler) { - this.handler = handler; - } - - public boolean onSelectWorktopOption(SelectWorktopOptionReq req) { - if (this.handler != null) { - this.handler.onSelectWorktopOption(this, req.getOptionId()); - } - return false; - } -} +package emu.grasscutter.game.entity.gadget; + +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.gadget.worktop.WorktopWorktopOptionHandler; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq; +import emu.grasscutter.net.proto.SceneGadgetInfoOuterClass.SceneGadgetInfo; +import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq; +import emu.grasscutter.net.proto.WorktopInfoOuterClass.WorktopInfo; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import java.util.Arrays; + +public final class GadgetWorktop extends GadgetContent { + private IntSet worktopOptions; + private WorktopWorktopOptionHandler handler; + + public GadgetWorktop(EntityGadget gadget) { + super(gadget); + } + + public IntSet getWorktopOptions() { + if (this.worktopOptions == null) { + this.worktopOptions = new IntOpenHashSet(); + } + return worktopOptions; + } + + public void addWorktopOptions(int[] options) { + if (this.worktopOptions == null) { + this.worktopOptions = new IntOpenHashSet(); + } + Arrays.stream(options).forEach(this.worktopOptions::add); + } + + public void removeWorktopOption(int option) { + if (this.worktopOptions == null) { + return; + } + this.worktopOptions.remove(option); + } + + public boolean onInteract(Player player, GadgetInteractReq req) { + return false; + } + + public void onBuildProto(SceneGadgetInfo.Builder gadgetInfo) { + if (this.worktopOptions == null) { + return; + } + + WorktopInfo worktop = + WorktopInfo.newBuilder().addAllOptionList(this.getWorktopOptions()).build(); + + gadgetInfo.setWorktop(worktop); + } + + public void setOnSelectWorktopOptionEvent(WorktopWorktopOptionHandler handler) { + this.handler = handler; + } + + public boolean onSelectWorktopOption(SelectWorktopOptionReq req) { + if (this.handler != null) { + this.handler.onSelectWorktopOption(this, req.getOptionId()); + } + return false; + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/gadget/chest/BossChestInteractHandler.java b/src/main/java/emu/grasscutter/game/entity/gadget/chest/BossChestInteractHandler.java index b931bf514..587e2bf92 100644 --- a/src/main/java/emu/grasscutter/game/entity/gadget/chest/BossChestInteractHandler.java +++ b/src/main/java/emu/grasscutter/game/entity/gadget/chest/BossChestInteractHandler.java @@ -1,61 +1,67 @@ -package emu.grasscutter.game.entity.gadget.chest; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.common.ItemParamData; -import emu.grasscutter.game.entity.gadget.GadgetChest; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.server.packet.send.PacketGadgetAutoPickDropInfoNotify; -import java.util.ArrayList; -import java.util.List; - -public class BossChestInteractHandler implements ChestInteractHandler { - @Override - public boolean isTwoStep() { - return true; - } - - @Override - public boolean onInteract(GadgetChest chest, Player player) { - return this.onInteract(chest, player, false); - } - - public boolean onInteract(GadgetChest chest, Player player, boolean useCondensedResin) { - var blossomRewards = - player - .getScene() - .getBlossomManager() - .onReward(player, chest.getGadget(), useCondensedResin); - if (blossomRewards != null) { - player.getInventory().addItems(blossomRewards, ActionReason.OpenWorldBossChest); - player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(blossomRewards)); - return true; - } - - var worldDataManager = chest.getGadget().getScene().getWorld().getServer().getWorldDataSystem(); - var monster = - chest - .getGadget() - .getMetaGadget() - .group - .monsters - .get(chest.getGadget().getMetaGadget().boss_chest.monster_config_id); - var reward = worldDataManager.getRewardByBossId(monster.monster_id); - - if (reward == null) { - Grasscutter.getLogger() - .warn("Could not found the reward of boss monster {}", monster.monster_id); - return false; - } - List rewards = new ArrayList<>(); - for (ItemParamData param : reward.getPreviewItems()) { - rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1))); - } - - player.getInventory().addItems(rewards, ActionReason.OpenWorldBossChest); - player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(rewards)); - - return true; - } -} +package emu.grasscutter.game.entity.gadget.chest; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.game.entity.gadget.GadgetChest; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.server.packet.send.PacketGadgetAutoPickDropInfoNotify; +import java.util.ArrayList; +import java.util.List; + +public class BossChestInteractHandler implements ChestInteractHandler { + @Override + public boolean isTwoStep() { + return true; + } + + @Override + public boolean onInteract(GadgetChest chest, Player player) { + return this.onInteract(chest, player, false); + } + + public boolean onInteract(GadgetChest chest, Player player, boolean useCondensedResin) { + var blossomRewards = + player + .getScene() + .getBlossomManager() + .onReward(player, chest.getGadget(), useCondensedResin); + if (blossomRewards != null) { + player.getInventory().addItems(blossomRewards, ActionReason.OpenWorldBossChest); + player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(blossomRewards)); + return true; + } + + var worldDataManager = chest.getGadget().getScene().getWorld().getServer().getWorldDataSystem(); + var monster = + chest + .getGadget() + .getMetaGadget() + .group + .monsters + .get(chest.getGadget().getMetaGadget().boss_chest.monster_config_id); + var reward = worldDataManager.getRewardByBossId(monster.monster_id); + + if (reward == null) { + var dungeonManager = player.getScene().getDungeonManager(); + + if (dungeonManager != null) { + return dungeonManager.getStatueDrops( + player, useCondensedResin, chest.getGadget().getGroupId()); + } + Grasscutter.getLogger() + .warn("Could not found the reward of boss monster {}", monster.monster_id); + return false; + } + List rewards = new ArrayList<>(); + for (ItemParamData param : reward.getPreviewItems()) { + rewards.add(new GameItem(param.getId(), Math.max(param.getCount(), 1))); + } + + player.getInventory().addItems(rewards, ActionReason.OpenWorldBossChest); + player.sendPacket(new PacketGadgetAutoPickDropInfoNotify(rewards)); + + return true; + } +} diff --git a/src/main/java/emu/grasscutter/game/inventory/EquipType.java b/src/main/java/emu/grasscutter/game/inventory/EquipType.java index 128fe6589..1504cee34 100644 --- a/src/main/java/emu/grasscutter/game/inventory/EquipType.java +++ b/src/main/java/emu/grasscutter/game/inventory/EquipType.java @@ -1,47 +1,45 @@ -package emu.grasscutter.game.inventory; - -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; - -public enum EquipType { - EQUIP_NONE(0), - EQUIP_BRACER(1), - EQUIP_NECKLACE(2), - EQUIP_SHOES(3), - EQUIP_RING(4), - EQUIP_DRESS(5), - EQUIP_WEAPON(6); - - private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); - private static final Map stringMap = new HashMap<>(); - - static { - Stream.of(values()) - .forEach( - e -> { - map.put(e.getValue(), e); - stringMap.put(e.name(), e); - }); - } - - private final int value; - - EquipType(int value) { - this.value = value; - } - - public static EquipType getTypeByValue(int value) { - return map.getOrDefault(value, EQUIP_NONE); - } - - public static EquipType getTypeByName(String name) { - return stringMap.getOrDefault(name, EQUIP_NONE); - } - - public int getValue() { - return value; - } -} +package emu.grasscutter.game.inventory; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public enum EquipType { + EQUIP_NONE(0), + EQUIP_BRACER(1), + EQUIP_NECKLACE(2), + EQUIP_SHOES(3), + EQUIP_RING(4), + EQUIP_DRESS(5), + EQUIP_WEAPON(6); + + private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); + private static final Map stringMap = new HashMap<>(); + + static { + Stream.of(values()) + .forEach( + e -> { + map.put(e.getValue(), e); + stringMap.put(e.name(), e); + }); + } + + @Getter private final int value; + + EquipType(int value) { + this.value = value; + } + + public static EquipType getTypeByValue(int value) { + return map.getOrDefault(value, EQUIP_NONE); + } + + public static EquipType getTypeByName(String name) { + return stringMap.getOrDefault(name, EQUIP_NONE); + } +} diff --git a/src/main/java/emu/grasscutter/game/inventory/GameItem.java b/src/main/java/emu/grasscutter/game/inventory/GameItem.java index 421700775..049c2b2d8 100644 --- a/src/main/java/emu/grasscutter/game/inventory/GameItem.java +++ b/src/main/java/emu/grasscutter/game/inventory/GameItem.java @@ -1,363 +1,371 @@ -package emu.grasscutter.game.inventory; - -import dev.morphia.annotations.*; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.GameDepot; -import emu.grasscutter.data.common.ItemParamData; -import emu.grasscutter.data.excels.ItemData; -import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData; -import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; -import emu.grasscutter.net.proto.EquipOuterClass.Equip; -import emu.grasscutter.net.proto.FurnitureOuterClass.Furniture; -import emu.grasscutter.net.proto.ItemHintOuterClass.ItemHint; -import emu.grasscutter.net.proto.ItemOuterClass.Item; -import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; -import emu.grasscutter.net.proto.MaterialOuterClass.Material; -import emu.grasscutter.net.proto.ReliquaryOuterClass.Reliquary; -import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo; -import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo; -import emu.grasscutter.net.proto.WeaponOuterClass.Weapon; -import emu.grasscutter.utils.WeightedList; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import lombok.Getter; -import lombok.Setter; -import org.bson.types.ObjectId; - -@Entity(value = "items", useDiscriminator = false) -public class GameItem { - @Id private ObjectId id; - @Indexed private int ownerId; - @Getter @Setter private int itemId; - @Getter @Setter private int count; - - @Transient @Getter private long guid; // Player unique id - @Transient @Getter @Setter private ItemData itemData; - - // Equips - @Getter @Setter private int level; - @Getter @Setter private int exp; - @Getter @Setter private int totalExp; - @Getter @Setter private int promoteLevel; - @Getter @Setter private boolean locked; - - // Weapon - @Getter private List affixes; - @Getter @Setter private int refinement = 0; - - // Relic - @Getter @Setter private int mainPropId; - @Getter private List appendPropIdList; - - @Getter @Setter private int equipCharacter; - @Transient @Getter @Setter private int weaponEntityId; - - public GameItem() { - // Morphia only - } - - public GameItem(int itemId) { - this(GameData.getItemDataMap().get(itemId)); - } - - public GameItem(int itemId, int count) { - this(GameData.getItemDataMap().get(itemId), count); - } - - public GameItem(ItemParamData itemParamData) { - this(itemParamData.getId(), itemParamData.getCount()); - } - - public GameItem(ItemData data) { - this(data, 1); - } - - public GameItem(ItemData data, int count) { - this.itemId = data.getId(); - this.itemData = data; - - switch (data.getItemType()) { - case ITEM_VIRTUAL: - this.count = count; - break; - case ITEM_WEAPON: - this.count = 1; - this.level = Math.max(this.count, 1); // ?????????????????? - this.affixes = new ArrayList<>(2); - if (data.getSkillAffix() != null) { - for (int skillAffix : data.getSkillAffix()) { - if (skillAffix > 0) { - this.affixes.add(skillAffix); - } - } - } - break; - case ITEM_RELIQUARY: - this.count = 1; - this.level = 1; - this.appendPropIdList = new ArrayList<>(); - // Create main property - ReliquaryMainPropData mainPropData = - GameDepot.getRandomRelicMainProp(data.getMainPropDepotId()); - if (mainPropData != null) { - this.mainPropId = mainPropData.getId(); - } - // Create extra stats - this.addAppendProps(data.getAppendPropNum()); - break; - default: - this.count = Math.min(count, data.getStackLimit()); - } - } - - public static int getMinPromoteLevel(int level) { - if (level > 80) { - return 6; - } else if (level > 70) { - return 5; - } else if (level > 60) { - return 4; - } else if (level > 50) { - return 3; - } else if (level > 40) { - return 2; - } else if (level > 20) { - return 1; - } - return 0; - } - - public int getOwnerId() { - return ownerId; - } - - public void setOwner(Player player) { - this.ownerId = player.getUid(); - this.guid = player.getNextGameGuid(); - } - - public ObjectId getObjectId() { - return id; - } - - public ItemType getItemType() { - return this.itemData.getItemType(); - } - - public int getEquipSlot() { - return this.getItemData().getEquipType().getValue(); - } - - public boolean isEquipped() { - return this.getEquipCharacter() > 0; - } - - public boolean isDestroyable() { - return !this.isLocked() && !this.isEquipped(); - } - - public void addAppendProp() { - if (this.appendPropIdList == null) { - this.appendPropIdList = new ArrayList<>(); - } - - if (this.appendPropIdList.size() < 4) { - this.addNewAppendProp(); - } else { - this.upgradeRandomAppendProp(); - } - } - - public void addAppendProps(int quantity) { - int num = Math.max(quantity, 0); - for (int i = 0; i < num; i++) { - this.addAppendProp(); - } - } - - private Set getAppendFightProperties() { - Set props = new HashSet<>(); - // Previously this would check no more than the first four affixes, however custom artifacts may - // not respect this order. - for (int appendPropId : this.appendPropIdList) { - ReliquaryAffixData affixData = GameData.getReliquaryAffixDataMap().get(appendPropId); - if (affixData != null) { - props.add(affixData.getFightProp()); - } - } - return props; - } - - private void addNewAppendProp() { - List affixList = - GameDepot.getRelicAffixList(this.itemData.getAppendPropDepotId()); - - if (affixList == null) { - return; - } - - // Build blacklist - Dont add same stat as main/sub stat - Set blacklist = this.getAppendFightProperties(); - ReliquaryMainPropData mainPropData = - GameData.getReliquaryMainPropDataMap().get(this.mainPropId); - if (mainPropData != null) { - blacklist.add(mainPropData.getFightProp()); - } - - // Build random list - WeightedList randomList = new WeightedList<>(); - for (ReliquaryAffixData affix : affixList) { - if (!blacklist.contains(affix.getFightProp())) { - randomList.add(affix.getWeight(), affix); - } - } - - if (randomList.size() == 0) { - return; - } - - // Add random stat - ReliquaryAffixData affixData = randomList.next(); - this.appendPropIdList.add(affixData.getId()); - } - - private void upgradeRandomAppendProp() { - List affixList = - GameDepot.getRelicAffixList(this.itemData.getAppendPropDepotId()); - - if (affixList == null) { - return; - } - - // Build whitelist - Set whitelist = this.getAppendFightProperties(); - - // Build random list - WeightedList randomList = new WeightedList<>(); - for (ReliquaryAffixData affix : affixList) { - if (whitelist.contains(affix.getFightProp())) { - randomList.add(affix.getUpgradeWeight(), affix); - } - } - - // Add random stat - ReliquaryAffixData affixData = randomList.next(); - this.appendPropIdList.add(affixData.getId()); - } - - @PostLoad - public void onLoad() { - if (this.itemData == null) { - this.itemData = GameData.getItemDataMap().get(getItemId()); - } - } - - public void save() { - if (this.count > 0 && this.ownerId > 0) { - DatabaseHelper.saveItem(this); - } else if (this.getObjectId() != null) { - DatabaseHelper.deleteItem(this); - } - } - - public SceneWeaponInfo createSceneWeaponInfo() { - SceneWeaponInfo.Builder weaponInfo = - SceneWeaponInfo.newBuilder() - .setEntityId(this.getWeaponEntityId()) - .setItemId(this.getItemId()) - .setGuid(this.getGuid()) - .setLevel(this.getLevel()) - .setGadgetId(this.getItemData().getGadgetId()) - .setAbilityInfo(AbilitySyncStateInfo.newBuilder().setIsInited(getAffixes().size() > 0)); - - if (this.getAffixes() != null && this.getAffixes().size() > 0) { - for (int affix : this.getAffixes()) { - weaponInfo.putAffixMap(affix, this.getRefinement()); - } - } - - return weaponInfo.build(); - } - - public SceneReliquaryInfo createSceneReliquaryInfo() { - SceneReliquaryInfo relicInfo = - SceneReliquaryInfo.newBuilder() - .setItemId(this.getItemId()) - .setGuid(this.getGuid()) - .setLevel(this.getLevel()) - .build(); - - return relicInfo; - } - - public Weapon toWeaponProto() { - Weapon.Builder weapon = - Weapon.newBuilder() - .setLevel(this.getLevel()) - .setExp(this.getExp()) - .setPromoteLevel(this.getPromoteLevel()); - - if (this.getAffixes() != null && this.getAffixes().size() > 0) { - for (int affix : this.getAffixes()) { - weapon.putAffixMap(affix, this.getRefinement()); - } - } - - return weapon.build(); - } - - public Reliquary toReliquaryProto() { - Reliquary.Builder relic = - Reliquary.newBuilder() - .setLevel(this.getLevel()) - .setExp(this.getExp()) - .setPromoteLevel(this.getPromoteLevel()) - .setMainPropId(this.getMainPropId()) - .addAllAppendPropIdList(this.getAppendPropIdList()); - - return relic.build(); - } - - public Item toProto() { - Item.Builder proto = Item.newBuilder().setGuid(this.getGuid()).setItemId(this.getItemId()); - - switch (getItemType()) { - case ITEM_WEAPON: - Weapon weapon = this.toWeaponProto(); - proto.setEquip(Equip.newBuilder().setWeapon(weapon).setIsLocked(this.isLocked()).build()); - break; - case ITEM_RELIQUARY: - Reliquary relic = this.toReliquaryProto(); - proto.setEquip(Equip.newBuilder().setReliquary(relic).setIsLocked(this.isLocked()).build()); - break; - case ITEM_FURNITURE: - Furniture furniture = Furniture.newBuilder().setCount(getCount()).build(); - proto.setFurniture(furniture); - break; - default: - Material material = Material.newBuilder().setCount(getCount()).build(); - proto.setMaterial(material); - break; - } - - return proto.build(); - } - - public ItemHint toItemHintProto() { - return ItemHint.newBuilder() - .setItemId(getItemId()) - .setCount(getCount()) - .setIsNew(false) - .build(); - } - - public ItemParam toItemParam() { - return ItemParam.newBuilder().setItemId(this.getItemId()).setCount(this.getCount()).build(); - } -} +package emu.grasscutter.game.inventory; + +import dev.morphia.annotations.*; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.GameDepot; +import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.data.excels.ItemData; +import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData; +import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; +import emu.grasscutter.net.proto.EquipOuterClass.Equip; +import emu.grasscutter.net.proto.FurnitureOuterClass.Furniture; +import emu.grasscutter.net.proto.ItemHintOuterClass.ItemHint; +import emu.grasscutter.net.proto.ItemOuterClass.Item; +import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; +import emu.grasscutter.net.proto.MaterialOuterClass.Material; +import emu.grasscutter.net.proto.ReliquaryOuterClass.Reliquary; +import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo; +import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo; +import emu.grasscutter.net.proto.WeaponOuterClass.Weapon; +import emu.grasscutter.utils.WeightedList; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.Getter; +import lombok.Setter; +import org.bson.types.ObjectId; + +@Entity(value = "items", useDiscriminator = false) +public class GameItem { + @Id private ObjectId id; + @Indexed private int ownerId; + @Getter @Setter private int itemId; + @Getter @Setter private int count; + + @Transient @Getter private long guid; // Player unique id + @Transient @Getter @Setter private ItemData itemData; + + // Equips + @Getter @Setter private int level; + @Getter @Setter private int exp; + @Getter @Setter private int totalExp; + @Getter @Setter private int promoteLevel; + @Getter @Setter private boolean locked; + + // Weapon + @Getter private List affixes; + @Getter @Setter private int refinement = 0; + + // Relic + @Getter @Setter private int mainPropId; + @Getter private List appendPropIdList; + + @Getter @Setter private int equipCharacter; + @Transient @Getter @Setter private int weaponEntityId; + @Transient @Getter private boolean newItem = false; + + public GameItem() { + // Morphia only + } + + public GameItem(int itemId) { + this(GameData.getItemDataMap().get(itemId)); + } + + public GameItem(int itemId, int count) { + this(GameData.getItemDataMap().get(itemId), count); + } + + public GameItem(ItemParamData itemParamData) { + this(itemParamData.getId(), itemParamData.getCount()); + } + + public GameItem(ItemData data) { + this(data, 1); + } + + public GameItem(ItemData data, int count) { + this.itemId = data.getId(); + this.itemData = data; + + switch (data.getItemType()) { + case ITEM_VIRTUAL: + this.count = count; + break; + case ITEM_WEAPON: + this.count = 1; + this.level = Math.max(this.count, 1); // ?????????????????? + this.affixes = new ArrayList<>(2); + if (data.getSkillAffix() != null) { + for (int skillAffix : data.getSkillAffix()) { + if (skillAffix > 0) { + this.affixes.add(skillAffix); + } + } + } + break; + case ITEM_RELIQUARY: + this.count = 1; + this.level = 1; + this.appendPropIdList = new ArrayList<>(); + // Create main property + ReliquaryMainPropData mainPropData = + GameDepot.getRandomRelicMainProp(data.getMainPropDepotId()); + if (mainPropData != null) { + this.mainPropId = mainPropData.getId(); + } + // Create extra stats + this.addAppendProps(data.getAppendPropNum()); + break; + default: + this.count = Math.min(count, data.getStackLimit()); + } + } + + public int getOwnerId() { + return ownerId; + } + + public void setOwner(Player player) { + this.ownerId = player.getUid(); + this.guid = player.getNextGameGuid(); + } + + public void checkIsNew(Inventory inventory) { + // display notification when player obtain new item + if (inventory.getItemByGuid(this.itemId) == null) { + this.newItem = true; + } + } + + public ObjectId getObjectId() { + return id; + } + + public ItemType getItemType() { + return this.itemData.getItemType(); + } + + public static int getMinPromoteLevel(int level) { + if (level > 80) { + return 6; + } else if (level > 70) { + return 5; + } else if (level > 60) { + return 4; + } else if (level > 50) { + return 3; + } else if (level > 40) { + return 2; + } else if (level > 20) { + return 1; + } + return 0; + } + + public int getEquipSlot() { + return this.getItemData().getEquipType().getValue(); + } + + public boolean isEquipped() { + return this.getEquipCharacter() > 0; + } + + public boolean isDestroyable() { + return !this.isLocked() && !this.isEquipped(); + } + + public void addAppendProp() { + if (this.appendPropIdList == null) { + this.appendPropIdList = new ArrayList<>(); + } + + if (this.appendPropIdList.size() < 4) { + this.addNewAppendProp(); + } else { + this.upgradeRandomAppendProp(); + } + } + + public void addAppendProps(int quantity) { + int num = Math.max(quantity, 0); + for (int i = 0; i < num; i++) { + this.addAppendProp(); + } + } + + private Set getAppendFightProperties() { + Set props = new HashSet<>(); + // Previously this would check no more than the first four affixes, however custom artifacts may + // not respect this order. + for (int appendPropId : this.appendPropIdList) { + ReliquaryAffixData affixData = GameData.getReliquaryAffixDataMap().get(appendPropId); + if (affixData != null) { + props.add(affixData.getFightProp()); + } + } + return props; + } + + private void addNewAppendProp() { + List affixList = + GameDepot.getRelicAffixList(this.itemData.getAppendPropDepotId()); + + if (affixList == null) { + return; + } + + // Build blacklist - Dont add same stat as main/sub stat + Set blacklist = this.getAppendFightProperties(); + ReliquaryMainPropData mainPropData = + GameData.getReliquaryMainPropDataMap().get(this.mainPropId); + if (mainPropData != null) { + blacklist.add(mainPropData.getFightProp()); + } + + // Build random list + WeightedList randomList = new WeightedList<>(); + for (ReliquaryAffixData affix : affixList) { + if (!blacklist.contains(affix.getFightProp())) { + randomList.add(affix.getWeight(), affix); + } + } + + if (randomList.size() == 0) { + return; + } + + // Add random stat + ReliquaryAffixData affixData = randomList.next(); + this.appendPropIdList.add(affixData.getId()); + } + + private void upgradeRandomAppendProp() { + List affixList = + GameDepot.getRelicAffixList(this.itemData.getAppendPropDepotId()); + + if (affixList == null) { + return; + } + + // Build whitelist + Set whitelist = this.getAppendFightProperties(); + + // Build random list + WeightedList randomList = new WeightedList<>(); + for (ReliquaryAffixData affix : affixList) { + if (whitelist.contains(affix.getFightProp())) { + randomList.add(affix.getUpgradeWeight(), affix); + } + } + + // Add random stat + ReliquaryAffixData affixData = randomList.next(); + this.appendPropIdList.add(affixData.getId()); + } + + @PostLoad + public void onLoad() { + if (this.itemData == null) { + this.itemData = GameData.getItemDataMap().get(getItemId()); + } + } + + public void save() { + if (this.count > 0 && this.ownerId > 0) { + DatabaseHelper.saveItem(this); + } else if (this.getObjectId() != null) { + DatabaseHelper.deleteItem(this); + } + } + + public SceneWeaponInfo createSceneWeaponInfo() { + SceneWeaponInfo.Builder weaponInfo = + SceneWeaponInfo.newBuilder() + .setEntityId(this.getWeaponEntityId()) + .setItemId(this.getItemId()) + .setGuid(this.getGuid()) + .setLevel(this.getLevel()) + .setGadgetId(this.getItemData().getGadgetId()) + .setAbilityInfo(AbilitySyncStateInfo.newBuilder().setIsInited(getAffixes().size() > 0)); + + if (this.getAffixes() != null && this.getAffixes().size() > 0) { + for (int affix : this.getAffixes()) { + weaponInfo.putAffixMap(affix, this.getRefinement()); + } + } + + return weaponInfo.build(); + } + + public SceneReliquaryInfo createSceneReliquaryInfo() { + SceneReliquaryInfo relicInfo = + SceneReliquaryInfo.newBuilder() + .setItemId(this.getItemId()) + .setGuid(this.getGuid()) + .setLevel(this.getLevel()) + .build(); + + return relicInfo; + } + + public Weapon toWeaponProto() { + Weapon.Builder weapon = + Weapon.newBuilder() + .setLevel(this.getLevel()) + .setExp(this.getExp()) + .setPromoteLevel(this.getPromoteLevel()); + + if (this.getAffixes() != null && this.getAffixes().size() > 0) { + for (int affix : this.getAffixes()) { + weapon.putAffixMap(affix, this.getRefinement()); + } + } + + return weapon.build(); + } + + public Reliquary toReliquaryProto() { + Reliquary.Builder relic = + Reliquary.newBuilder() + .setLevel(this.getLevel()) + .setExp(this.getExp()) + .setPromoteLevel(this.getPromoteLevel()) + .setMainPropId(this.getMainPropId()) + .addAllAppendPropIdList(this.getAppendPropIdList()); + + return relic.build(); + } + + public Item toProto() { + Item.Builder proto = Item.newBuilder().setGuid(this.getGuid()).setItemId(this.getItemId()); + + switch (getItemType()) { + case ITEM_WEAPON: + Weapon weapon = this.toWeaponProto(); + proto.setEquip(Equip.newBuilder().setWeapon(weapon).setIsLocked(this.isLocked()).build()); + break; + case ITEM_RELIQUARY: + Reliquary relic = this.toReliquaryProto(); + proto.setEquip(Equip.newBuilder().setReliquary(relic).setIsLocked(this.isLocked()).build()); + break; + case ITEM_FURNITURE: + Furniture furniture = Furniture.newBuilder().setCount(getCount()).build(); + proto.setFurniture(furniture); + break; + default: + Material material = Material.newBuilder().setCount(getCount()).build(); + proto.setMaterial(material); + break; + } + + return proto.build(); + } + + public ItemHint toItemHintProto() { + return ItemHint.newBuilder() + .setItemId(getItemId()) + .setCount(getCount()) + .setIsNew(this.isNewItem()) + .build(); + } + + public ItemParam toItemParam() { + return ItemParam.newBuilder().setItemId(this.getItemId()).setCount(this.getCount()).build(); + } +} diff --git a/src/main/java/emu/grasscutter/game/inventory/Inventory.java b/src/main/java/emu/grasscutter/game/inventory/Inventory.java index 2b813b408..79618fc29 100644 --- a/src/main/java/emu/grasscutter/game/inventory/Inventory.java +++ b/src/main/java/emu/grasscutter/game/inventory/Inventory.java @@ -1,558 +1,567 @@ -package emu.grasscutter.game.inventory; - -import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.common.ItemParamData; -import emu.grasscutter.data.excels.ItemData; -import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.avatar.AvatarStorage; -import emu.grasscutter.game.player.BasePlayerManager; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.props.ItemUseAction.UseItemParams; -import emu.grasscutter.game.props.PlayerProperty; -import emu.grasscutter.game.props.WatcherTriggerType; -import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; -import emu.grasscutter.server.packet.send.PacketAvatarEquipChangeNotify; -import emu.grasscutter.server.packet.send.PacketItemAddHintNotify; -import emu.grasscutter.server.packet.send.PacketStoreItemChangeNotify; -import emu.grasscutter.server.packet.send.PacketStoreItemDelNotify; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -public class Inventory extends BasePlayerManager implements Iterable { - private final Long2ObjectMap store; - private final Int2ObjectMap inventoryTypes; - - public Inventory(Player player) { - super(player); - - this.store = new Long2ObjectOpenHashMap<>(); - this.inventoryTypes = new Int2ObjectOpenHashMap<>(); - - this.createInventoryTab(ItemType.ITEM_WEAPON, new EquipInventoryTab(INVENTORY_LIMITS.weapons)); - this.createInventoryTab( - ItemType.ITEM_RELIQUARY, new EquipInventoryTab(INVENTORY_LIMITS.relics)); - this.createInventoryTab( - ItemType.ITEM_MATERIAL, new MaterialInventoryTab(INVENTORY_LIMITS.materials)); - this.createInventoryTab( - ItemType.ITEM_FURNITURE, new MaterialInventoryTab(INVENTORY_LIMITS.furniture)); - } - - public AvatarStorage getAvatarStorage() { - return this.getPlayer().getAvatars(); - } - - public Long2ObjectMap getItems() { - return store; - } - - public Int2ObjectMap getInventoryTypes() { - return inventoryTypes; - } - - public InventoryTab getInventoryTab(ItemType type) { - return getInventoryTypes().get(type.getValue()); - } - - public void createInventoryTab(ItemType type, InventoryTab tab) { - this.getInventoryTypes().put(type.getValue(), tab); - } - - public GameItem getItemByGuid(long id) { - return this.getItems().get(id); - } - - public boolean addItem(int itemId) { - return addItem(itemId, 1); - } - - public boolean addItem(int itemId, int count) { - return addItem(itemId, count, null); - } - - public boolean addItem(int itemId, int count, ActionReason reason) { - ItemData itemData = GameData.getItemDataMap().get(itemId); - - if (itemData == null) { - return false; - } - - GameItem item = new GameItem(itemData, count); - - return addItem(item, reason); - } - - public boolean addItem(GameItem item) { - GameItem result = putItem(item); - - if (result != null) { - getPlayer() - .getBattlePassManager() - .triggerMission( - WatcherTriggerType.TRIGGER_OBTAIN_MATERIAL_NUM, - result.getItemId(), - result.getCount()); - getPlayer().sendPacket(new PacketStoreItemChangeNotify(result)); - return true; - } - - return false; - } - - public boolean addItem(GameItem item, ActionReason reason) { - boolean result = addItem(item); - - if (result && reason != null) { - getPlayer().sendPacket(new PacketItemAddHintNotify(item, reason)); - } - - return result; - } - - public boolean addItem(GameItem item, ActionReason reason, boolean forceNotify) { - boolean result = addItem(item); - - if (reason != null && (forceNotify || result)) { - getPlayer().sendPacket(new PacketItemAddHintNotify(item, reason)); - } - - return result; - } - - public boolean addItem(ItemParamData itemParam) { - return addItem(itemParam, null); - } - - public boolean addItem(ItemParamData itemParam, ActionReason reason) { - if (itemParam == null) return false; - return addItem(itemParam.getId(), itemParam.getCount(), reason); - } - - public void addItems(Collection items) { - this.addItems(items, null); - } - - public void addItems(Collection items, ActionReason reason) { - List changedItems = new ArrayList<>(); - for (var item : items) { - if (item.getItemId() == 0) continue; - GameItem result = null; - try { - // putItem might throws exception - // ignore that exception and continue - result = putItem(item); - } catch (Exception e) { - e.printStackTrace(); - } - if (result != null) { - getPlayer() - .getBattlePassManager() - .triggerMission( - WatcherTriggerType.TRIGGER_OBTAIN_MATERIAL_NUM, - result.getItemId(), - result.getCount()); - changedItems.add(result); - } - } - if (changedItems.size() == 0) { - return; - } - if (reason != null) { - getPlayer().sendPacket(new PacketItemAddHintNotify(changedItems, reason)); - } - getPlayer().sendPacket(new PacketStoreItemChangeNotify(changedItems)); - } - - public void addItemParams(Collection items) { - addItems( - items.stream().map(param -> new GameItem(param.getItemId(), param.getCount())).toList(), - null); - } - - public void addItemParamDatas(Collection items) { - addItemParamDatas(items, null); - } - - public void addItemParamDatas(Collection items, ActionReason reason) { - addItems( - items.stream().map(param -> new GameItem(param.getItemId(), param.getCount())).toList(), - reason); - } - - private synchronized GameItem putItem(GameItem item) { - // Dont add items that dont have a valid item definition. - var data = item.getItemData(); - if (data == null) return null; - - if (data.isUseOnGain()) { - var params = new UseItemParams(this.player, data.getUseTarget()); - params.usedItemId = data.getId(); - this.player.getServer().getInventorySystem().useItemDirect(data, params); - return null; - } - - // Add item to inventory store - ItemType type = item.getItemData().getItemType(); - InventoryTab tab = getInventoryTab(type); - - // Add - switch (type) { - case ITEM_WEAPON: - case ITEM_RELIQUARY: - if (tab.getSize() >= tab.getMaxCapacity()) { - return null; - } - // Duplicates cause problems - item.setCount(Math.max(item.getCount(), 1)); - // Adds to inventory - this.putItem(item, tab); - // Set ownership and save to db - item.save(); - return item; - case ITEM_VIRTUAL: - // Handle - this.addVirtualItem(item.getItemId(), item.getCount()); - return item; - default: - switch (item.getItemData().getMaterialType()) { - case MATERIAL_AVATAR: - case MATERIAL_FLYCLOAK: - case MATERIAL_COSTUME: - case MATERIAL_NAMECARD: - Grasscutter.getLogger() - .warn( - "Attempted to add a " - + item.getItemData().getMaterialType().name() - + " to inventory, but item definition lacks isUseOnGain. This indicates a Resources error."); - return null; - default: - if (tab == null) { - return null; - } - GameItem existingItem = tab.getItemById(item.getItemId()); - if (existingItem == null) { - // Item type didnt exist before, we will add it to main inventory map if there is - // enough space - if (tab.getSize() >= tab.getMaxCapacity()) { - return null; - } - this.putItem(item, tab); - // Set ownership and save to db - item.save(); - return item; - } else { - // Add count - existingItem.setCount( - Math.min( - existingItem.getCount() + item.getCount(), - item.getItemData().getStackLimit())); - existingItem.save(); - return existingItem; - } - } - } - } - - private synchronized void putItem(GameItem item, InventoryTab tab) { - this.player.getCodex().checkAddedItem(item); - // Set owner and guid FIRST! - item.setOwner(this.player); - // Put in item store - getItems().put(item.getGuid(), item); - if (tab != null) { - tab.onAddItem(item); - } - } - - private void addVirtualItem(int itemId, int count) { - switch (itemId) { - case 101 -> // Character exp - this.player.getTeamManager().getActiveTeam().stream() - .map(e -> e.getAvatar()) - .forEach( - avatar -> - this.player - .getServer() - .getInventorySystem() - .upgradeAvatar(this.player, avatar, count)); - case 102 -> // Adventure exp - this.player.addExpDirectly(count); - case 105 -> // Companionship exp - this.player.getTeamManager().getActiveTeam().stream() - .map(e -> e.getAvatar()) - .forEach( - avatar -> - this.player - .getServer() - .getInventorySystem() - .upgradeAvatarFetterLevel( - this.player, avatar, count * (this.player.isInMultiplayer() ? 2 : 1))); - case 106 -> // Resin - this.player.getResinManager().addResin(count); - case 107 -> // Legendary Key - this.player.addLegendaryKey(count); - case 121 -> // Home exp - this.player.getHome().addExp(this.player, count); - case 201 -> // Primogem - this.player.setPrimogems(this.player.getPrimogems() + count); - case 202 -> // Mora - this.player.setMora(this.player.getMora() + count); - case 203 -> // Genesis Crystals - this.player.setCrystals(this.player.getCrystals() + count); - case 204 -> // Home Coin - this.player.setHomeCoin(this.player.getHomeCoin() + count); - } - } - - private GameItem payVirtualItem(int itemId, int count) { - switch (itemId) { - case 201 -> // Primogem - player.setPrimogems(player.getPrimogems() - count); - case 202 -> // Mora - player.setMora(player.getMora() - count); - case 203 -> // Genesis Crystals - player.setCrystals(player.getCrystals() - count); - case 106 -> // Resin - player.getResinManager().useResin(count); - case 107 -> // LegendaryKey - player.useLegendaryKey(count); - case 204 -> // Home Coin - player.setHomeCoin(player.getHomeCoin() - count); - default -> { - var gameItem = getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId); - removeItem(gameItem, count); - return gameItem; - } - } - return null; - } - - private int getVirtualItemCount(int itemId) { - switch (itemId) { - case 201: // Primogem - return this.player.getPrimogems(); - case 202: // Mora - return this.player.getMora(); - case 203: // Genesis Crystals - return this.player.getCrystals(); - case 106: // Resin - return this.player.getProperty(PlayerProperty.PROP_PLAYER_RESIN); - case 107: // Legendary Key - return this.player.getProperty(PlayerProperty.PROP_PLAYER_LEGENDARY_KEY); - case 204: // Home Coin - return this.player.getHomeCoin(); - default: - GameItem item = - getInventoryTab(ItemType.ITEM_MATERIAL) - .getItemById( - itemId); // What if we ever want to operate on weapons/relics/furniture? :S - return (item == null) ? 0 : item.getCount(); - } - } - - public synchronized boolean payItem(int id, int count) { - if (this.getVirtualItemCount(id) < count) return false; - this.payVirtualItem(id, count); - return true; - } - - public boolean payItem(ItemParamData costItem) { - return this.payItem(costItem.getId(), costItem.getCount()); - } - - public boolean payItems(ItemParamData[] costItems) { - return this.payItems(costItems, 1, null); - } - - public boolean payItems(ItemParamData[] costItems, int quantity) { - return this.payItems(costItems, quantity, null); - } - - public synchronized boolean payItems( - ItemParamData[] costItems, int quantity, ActionReason reason) { - // Make sure player has requisite items - for (ItemParamData cost : costItems) - if (this.getVirtualItemCount(cost.getId()) < (cost.getCount() * quantity)) return false; - // All costs are satisfied, now remove them all - for (ItemParamData cost : costItems) { - this.payVirtualItem(cost.getId(), cost.getCount() * quantity); - } - - if (reason != null) { // Do we need these? - // getPlayer().sendPacket(new PacketItemAddHintNotify(changedItems, reason)); - } - // getPlayer().sendPacket(new PacketStoreItemChangeNotify(changedItems)); - return true; - } - - public boolean payItems(Iterable costItems) { - return this.payItems(costItems, 1, null); - } - - public boolean payItems(Iterable costItems, int quantity) { - return this.payItems(costItems, quantity, null); - } - - public synchronized boolean payItems( - Iterable costItems, int quantity, ActionReason reason) { - // Make sure player has requisite items - for (ItemParamData cost : costItems) - if (getVirtualItemCount(cost.getId()) < (cost.getCount() * quantity)) return false; - // All costs are satisfied, now remove them all - costItems.forEach(cost -> this.payVirtualItem(cost.getId(), cost.getCount() * quantity)); - // TODO:handle the reason(need to send certain package) - return true; - } - - public void removeItems(List items) { - // TODO Bulk delete - for (GameItem item : items) { - this.removeItem(item, item.getCount()); - } - } - - public boolean removeItem(long guid) { - return removeItem(guid, 1); - } - - public synchronized boolean removeItem(long guid, int count) { - GameItem item = this.getItemByGuid(guid); - - if (item == null) { - return false; - } - - return removeItem(item, count); - } - - public synchronized boolean removeItem(GameItem item) { - return removeItem(item, item.getCount()); - } - - public synchronized boolean removeItem(GameItem item, int count) { - // Sanity check - if (count <= 0 || item == null) { - return false; - } - - if (item.getItemData().isEquip()) { - item.setCount(0); - } else { - item.setCount(item.getCount() - count); - } - - if (item.getCount() <= 0) { - // Remove from inventory tab too - InventoryTab tab = null; - if (item.getItemData() != null) { - tab = getInventoryTab(item.getItemData().getItemType()); - } - // Remove if less than 0 - deleteItem(item, tab); - // - getPlayer().sendPacket(new PacketStoreItemDelNotify(item)); - } else { - getPlayer().sendPacket(new PacketStoreItemChangeNotify(item)); - } - - // Battle pass trigger - int removeCount = Math.min(count, item.getCount()); - getPlayer() - .getBattlePassManager() - .triggerMission(WatcherTriggerType.TRIGGER_COST_MATERIAL, item.getItemId(), removeCount); - - // Update in db - item.save(); - - // Returns true on success - return true; - } - - private void deleteItem(GameItem item, InventoryTab tab) { - getItems().remove(item.getGuid()); - if (tab != null) { - tab.onRemoveItem(item); - } - } - - public boolean equipItem(long avatarGuid, long equipGuid) { - Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(avatarGuid); - GameItem item = this.getItemByGuid(equipGuid); - - if (avatar != null && item != null) { - return avatar.equipItem(item, true); - } - - return false; - } - - public boolean unequipItem(long avatarGuid, int slot) { - Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(avatarGuid); - EquipType equipType = EquipType.getTypeByValue(slot); - - if (avatar != null && equipType != EquipType.EQUIP_WEAPON) { - if (avatar.unequipItem(equipType)) { - getPlayer().sendPacket(new PacketAvatarEquipChangeNotify(avatar, equipType)); - avatar.recalcStats(); - return true; - } - } - - return false; - } - - public void loadFromDatabase() { - List items = DatabaseHelper.getInventoryItems(getPlayer()); - - for (GameItem item : items) { - // Should never happen - if (item.getObjectId() == null) { - continue; - } - - ItemData itemData = GameData.getItemDataMap().get(item.getItemId()); - if (itemData == null) { - continue; - } - - item.setItemData(itemData); - - InventoryTab tab = null; - if (item.getItemData() != null) { - tab = getInventoryTab(item.getItemData().getItemType()); - } - - putItem(item, tab); - - // Equip to a character if possible - if (item.isEquipped()) { - Avatar avatar = getPlayer().getAvatars().getAvatarById(item.getEquipCharacter()); - boolean hasEquipped = false; - - if (avatar != null) { - hasEquipped = avatar.equipItem(item, false); - } - - if (!hasEquipped) { - item.setEquipCharacter(0); - item.save(); - } - } - } - } - - @Override - public Iterator iterator() { - return this.getItems().values().iterator(); - } -} +package emu.grasscutter.game.inventory; + +import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.data.excels.ItemData; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.avatar.AvatarStorage; +import emu.grasscutter.game.player.BasePlayerManager; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.props.ItemUseAction.UseItemParams; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.props.WatcherTriggerType; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; +import emu.grasscutter.server.packet.send.*; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +public class Inventory extends BasePlayerManager implements Iterable { + private final Long2ObjectMap store; + private final Int2ObjectMap inventoryTypes; + + public Inventory(Player player) { + super(player); + + this.store = new Long2ObjectOpenHashMap<>(); + this.inventoryTypes = new Int2ObjectOpenHashMap<>(); + + this.createInventoryTab(ItemType.ITEM_WEAPON, new EquipInventoryTab(INVENTORY_LIMITS.weapons)); + this.createInventoryTab( + ItemType.ITEM_RELIQUARY, new EquipInventoryTab(INVENTORY_LIMITS.relics)); + this.createInventoryTab( + ItemType.ITEM_MATERIAL, new MaterialInventoryTab(INVENTORY_LIMITS.materials)); + this.createInventoryTab( + ItemType.ITEM_FURNITURE, new MaterialInventoryTab(INVENTORY_LIMITS.furniture)); + } + + public AvatarStorage getAvatarStorage() { + return this.getPlayer().getAvatars(); + } + + public Long2ObjectMap getItems() { + return store; + } + + public Int2ObjectMap getInventoryTypes() { + return inventoryTypes; + } + + public InventoryTab getInventoryTab(ItemType type) { + return getInventoryTypes().get(type.getValue()); + } + + public void createInventoryTab(ItemType type, InventoryTab tab) { + this.getInventoryTypes().put(type.getValue(), tab); + } + + public GameItem getItemByGuid(long id) { + return this.getItems().get(id); + } + + public boolean addItem(int itemId) { + return addItem(itemId, 1); + } + + public boolean addItem(int itemId, int count) { + return addItem(itemId, count, null); + } + + public boolean addItem(int itemId, int count, ActionReason reason) { + ItemData itemData = GameData.getItemDataMap().get(itemId); + + if (itemData == null) { + return false; + } + + GameItem item = new GameItem(itemData, count); + + return addItem(item, reason); + } + + public boolean addItem(GameItem item) { + GameItem result = putItem(item); + + if (result != null) { + this.triggerAddItemEvents(result); + getPlayer().sendPacket(new PacketStoreItemChangeNotify(result)); + return true; + } + + return false; + } + + public boolean addItem(GameItem item, ActionReason reason) { + return addItem(item, reason, false); + } + + public boolean addItem(GameItem item, ActionReason reason, boolean forceNotify) { + boolean result = addItem(item); + + if (item.getItemData().getMaterialType() == MaterialType.MATERIAL_AVATAR) { + getPlayer() + .sendPacket( + new PacketAddNoGachaAvatarCardNotify( + (item.getItemId() % 1000) + 10000000, reason, item)); + } + + if (reason != null && (forceNotify || result)) { + getPlayer().sendPacket(new PacketItemAddHintNotify(item, reason)); + } + + return result; + } + + public boolean addItem(ItemParamData itemParam) { + return addItem(itemParam, null); + } + + public boolean addItem(ItemParamData itemParam, ActionReason reason) { + if (itemParam == null) return false; + return addItem(itemParam.getId(), itemParam.getCount(), reason); + } + + public void addItems(Collection items) { + this.addItems(items, null); + } + + public void addItems(Collection items, ActionReason reason) { + List changedItems = new ArrayList<>(); + for (var item : items) { + if (item.getItemId() == 0) continue; + GameItem result = null; + try { + // putItem might throws exception + // ignore that exception and continue + result = putItem(item); + } catch (Exception e) { + e.printStackTrace(); + } + if (result != null) { + this.triggerAddItemEvents(result); + changedItems.add(result); + } + } + if (changedItems.size() == 0) { + return; + } + if (reason != null) { + getPlayer().sendPacket(new PacketItemAddHintNotify(items, reason)); + } + getPlayer().sendPacket(new PacketStoreItemChangeNotify(changedItems)); + } + + private void triggerAddItemEvents(GameItem result) { + getPlayer() + .getBattlePassManager() + .triggerMission( + WatcherTriggerType.TRIGGER_OBTAIN_MATERIAL_NUM, result.getItemId(), result.getCount()); + getPlayer() + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_OBTAIN_ITEM, result.getItemId(), result.getCount()); + } + + private void triggerRemItemEvents(GameItem item, int removeCount) { + getPlayer() + .getBattlePassManager() + .triggerMission(WatcherTriggerType.TRIGGER_COST_MATERIAL, item.getItemId(), removeCount); + getPlayer() + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_ITEM_LESS_THAN, item.getItemId(), item.getCount()); + } + + public void addItemParams(Collection items) { + addItems( + items.stream().map(param -> new GameItem(param.getItemId(), param.getCount())).toList(), + null); + } + + public void addItemParamDatas(Collection items) { + addItemParamDatas(items, null); + } + + public void addItemParamDatas(Collection items, ActionReason reason) { + addItems( + items.stream().map(param -> new GameItem(param.getItemId(), param.getCount())).toList(), + reason); + } + + private synchronized GameItem putItem(GameItem item) { + // Dont add items that dont have a valid item definition. + var data = item.getItemData(); + if (data == null) return null; + + this.player.getProgressManager().addItemObtainedHistory(item.getItemId(), item.getCount()); + + if (data.isUseOnGain()) { + var params = new UseItemParams(this.player, data.getUseTarget()); + params.usedItemId = data.getId(); + this.player.getServer().getInventorySystem().useItemDirect(data, params); + return null; + } + + // Add item to inventory store + ItemType type = item.getItemData().getItemType(); + InventoryTab tab = getInventoryTab(type); + + // Add + switch (type) { + case ITEM_WEAPON: + case ITEM_RELIQUARY: + if (tab.getSize() >= tab.getMaxCapacity()) { + return null; + } + // Duplicates cause problems + item.setCount(Math.max(item.getCount(), 1)); + // Adds to inventory + this.putItem(item, tab); + // Set ownership and save to db + item.save(); + return item; + case ITEM_VIRTUAL: + // Handle + this.addVirtualItem(item.getItemId(), item.getCount()); + return item; + default: + switch (item.getItemData().getMaterialType()) { + case MATERIAL_AVATAR: + case MATERIAL_FLYCLOAK: + case MATERIAL_COSTUME: + case MATERIAL_NAMECARD: + Grasscutter.getLogger() + .warn( + "Attempted to add a " + + item.getItemData().getMaterialType().name() + + " to inventory, but item definition lacks isUseOnGain. This indicates a Resources error."); + return null; + default: + if (tab == null) { + return null; + } + GameItem existingItem = tab.getItemById(item.getItemId()); + if (existingItem == null) { + // Item type didnt exist before, we will add it to main inventory map if there is + // enough space + if (tab.getSize() >= tab.getMaxCapacity()) { + return null; + } + this.putItem(item, tab); + // Set ownership and save to db + item.save(); + return item; + } else { + // Add count + existingItem.setCount( + Math.min( + existingItem.getCount() + item.getCount(), + item.getItemData().getStackLimit())); + existingItem.save(); + return existingItem; + } + } + } + } + + private synchronized void putItem(GameItem item, InventoryTab tab) { + this.player.getCodex().checkAddedItem(item); + // Set owner and guid FIRST! + item.setOwner(this.player); + item.checkIsNew(this); + // Put in item store + getItems().put(item.getGuid(), item); + if (tab != null) { + tab.onAddItem(item); + } + } + + private void addVirtualItem(int itemId, int count) { + switch (itemId) { + case 101 -> // Character exp + this.player.getTeamManager().getActiveTeam().stream() + .map(e -> e.getAvatar()) + .forEach( + avatar -> + this.player + .getServer() + .getInventorySystem() + .upgradeAvatar(this.player, avatar, count)); + case 102 -> // Adventure exp + this.player.addExpDirectly(count); + case 105 -> // Companionship exp + this.player.getTeamManager().getActiveTeam().stream() + .map(e -> e.getAvatar()) + .forEach( + avatar -> + this.player + .getServer() + .getInventorySystem() + .upgradeAvatarFetterLevel( + this.player, avatar, count * (this.player.isInMultiplayer() ? 2 : 1))); + case 106 -> // Resin + this.player.getResinManager().addResin(count); + case 107 -> // Legendary Key + this.player.addLegendaryKey(count); + case 121 -> // Home exp + this.player.getHome().addExp(this.player, count); + case 201 -> // Primogem + this.player.setPrimogems(this.player.getPrimogems() + count); + case 202 -> // Mora + this.player.setMora(this.player.getMora() + count); + case 203 -> // Genesis Crystals + this.player.setCrystals(this.player.getCrystals() + count); + case 204 -> // Home Coin + this.player.setHomeCoin(this.player.getHomeCoin() + count); + } + } + + private GameItem payVirtualItem(int itemId, int count) { + switch (itemId) { + case 201 -> // Primogem + player.setPrimogems(player.getPrimogems() - count); + case 202 -> // Mora + player.setMora(player.getMora() - count); + case 203 -> // Genesis Crystals + player.setCrystals(player.getCrystals() - count); + case 106 -> // Resin + player.getResinManager().useResin(count); + case 107 -> // LegendaryKey + player.useLegendaryKey(count); + case 204 -> // Home Coin + player.setHomeCoin(player.getHomeCoin() - count); + default -> { + var gameItem = getInventoryTab(ItemType.ITEM_MATERIAL).getItemById(itemId); + removeItem(gameItem, count); + return gameItem; + } + } + return null; + } + + private int getVirtualItemCount(int itemId) { + switch (itemId) { + case 201: // Primogem + return this.player.getPrimogems(); + case 202: // Mora + return this.player.getMora(); + case 203: // Genesis Crystals + return this.player.getCrystals(); + case 106: // Resin + return this.player.getProperty(PlayerProperty.PROP_PLAYER_RESIN); + case 107: // Legendary Key + return this.player.getProperty(PlayerProperty.PROP_PLAYER_LEGENDARY_KEY); + case 204: // Home Coin + return this.player.getHomeCoin(); + default: + GameItem item = + getInventoryTab(ItemType.ITEM_MATERIAL) + .getItemById( + itemId); // What if we ever want to operate on weapons/relics/furniture? :S + return (item == null) ? 0 : item.getCount(); + } + } + + public synchronized boolean payItem(int id, int count) { + if (this.getVirtualItemCount(id) < count) return false; + this.payVirtualItem(id, count); + return true; + } + + public boolean payItem(ItemParamData costItem) { + return this.payItem(costItem.getId(), costItem.getCount()); + } + + public boolean payItems(ItemParamData[] costItems) { + return this.payItems(costItems, 1, null); + } + + public boolean payItems(ItemParamData[] costItems, int quantity) { + return this.payItems(costItems, quantity, null); + } + + public synchronized boolean payItems( + ItemParamData[] costItems, int quantity, ActionReason reason) { + // Make sure player has requisite items + for (ItemParamData cost : costItems) + if (this.getVirtualItemCount(cost.getId()) < (cost.getCount() * quantity)) return false; + // All costs are satisfied, now remove them all + for (ItemParamData cost : costItems) { + this.payVirtualItem(cost.getId(), cost.getCount() * quantity); + } + + if (reason != null) { // Do we need these? + // getPlayer().sendPacket(new PacketItemAddHintNotify(changedItems, reason)); + } + // getPlayer().sendPacket(new PacketStoreItemChangeNotify(changedItems)); + return true; + } + + public boolean payItems(Iterable costItems) { + return this.payItems(costItems, 1, null); + } + + public boolean payItems(Iterable costItems, int quantity) { + return this.payItems(costItems, quantity, null); + } + + public synchronized boolean payItems( + Iterable costItems, int quantity, ActionReason reason) { + // Make sure player has requisite items + for (ItemParamData cost : costItems) + if (getVirtualItemCount(cost.getId()) < (cost.getCount() * quantity)) return false; + // All costs are satisfied, now remove them all + costItems.forEach(cost -> this.payVirtualItem(cost.getId(), cost.getCount() * quantity)); + // TODO:handle the reason(need to send certain package) + return true; + } + + public void removeItems(List items) { + // TODO Bulk delete + for (GameItem item : items) { + this.removeItem(item, item.getCount()); + } + } + + public boolean removeItem(long guid) { + return removeItem(guid, 1); + } + + public synchronized boolean removeItem(long guid, int count) { + GameItem item = this.getItemByGuid(guid); + + if (item == null) { + return false; + } + + return removeItem(item, count); + } + + public synchronized boolean removeItem(GameItem item) { + return removeItem(item, item.getCount()); + } + + public synchronized boolean removeItem(GameItem item, int count) { + // Sanity check + if (count <= 0 || item == null) { + return false; + } + + if (item.getItemData().isEquip()) { + item.setCount(0); + } else { + item.setCount(item.getCount() - count); + } + + if (item.getCount() <= 0) { + // Remove from inventory tab too + InventoryTab tab = null; + if (item.getItemData() != null) { + tab = getInventoryTab(item.getItemData().getItemType()); + } + // Remove if less than 0 + deleteItem(item, tab); + // + getPlayer().sendPacket(new PacketStoreItemDelNotify(item)); + } else { + getPlayer().sendPacket(new PacketStoreItemChangeNotify(item)); + } + + // Battle pass trigger + int removeCount = Math.min(count, item.getCount()); + this.triggerRemItemEvents(item, removeCount); + + // Update in db + item.save(); + + // Returns true on success + return true; + } + + private void deleteItem(GameItem item, InventoryTab tab) { + getItems().remove(item.getGuid()); + if (tab != null) { + tab.onRemoveItem(item); + } + } + + public boolean equipItem(long avatarGuid, long equipGuid) { + Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(avatarGuid); + GameItem item = this.getItemByGuid(equipGuid); + + if (avatar != null && item != null) { + return avatar.equipItem(item, true); + } + + return false; + } + + public boolean unequipItem(long avatarGuid, int slot) { + Avatar avatar = getPlayer().getAvatars().getAvatarByGuid(avatarGuid); + EquipType equipType = EquipType.getTypeByValue(slot); + + if (avatar != null && equipType != EquipType.EQUIP_WEAPON) { + if (avatar.unequipItem(equipType)) { + getPlayer().sendPacket(new PacketAvatarEquipChangeNotify(avatar, equipType)); + avatar.recalcStats(); + return true; + } + } + + return false; + } + + public void loadFromDatabase() { + List items = DatabaseHelper.getInventoryItems(getPlayer()); + + for (GameItem item : items) { + // Should never happen + if (item.getObjectId() == null) { + continue; + } + + ItemData itemData = GameData.getItemDataMap().get(item.getItemId()); + if (itemData == null) { + continue; + } + + item.setItemData(itemData); + + InventoryTab tab = null; + if (item.getItemData() != null) { + tab = getInventoryTab(item.getItemData().getItemType()); + } + + putItem(item, tab); + + // Equip to a character if possible + if (item.isEquipped()) { + Avatar avatar = getPlayer().getAvatars().getAvatarById(item.getEquipCharacter()); + boolean hasEquipped = false; + + if (avatar != null) { + hasEquipped = avatar.equipItem(item, false); + } + + if (!hasEquipped) { + item.setEquipCharacter(0); + item.save(); + } + } + } + } + + @Override + public Iterator iterator() { + return this.getItems().values().iterator(); + } +} diff --git a/src/main/java/emu/grasscutter/game/inventory/ItemQuality.java b/src/main/java/emu/grasscutter/game/inventory/ItemQuality.java index d3c5ec061..e3f1d6d83 100644 --- a/src/main/java/emu/grasscutter/game/inventory/ItemQuality.java +++ b/src/main/java/emu/grasscutter/game/inventory/ItemQuality.java @@ -1,47 +1,45 @@ -package emu.grasscutter.game.inventory; - -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; - -public enum ItemQuality { - QUALITY_NONE(0), - QUALITY_WHITE(1), - QUALITY_GREEN(2), - QUALITY_BLUE(3), - QUALITY_PURPLE(4), - QUALITY_ORANGE(5), - QUALITY_ORANGE_SP(105); - - private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); - private static final Map stringMap = new HashMap<>(); - - static { - Stream.of(values()) - .forEach( - e -> { - map.put(e.getValue(), e); - stringMap.put(e.name(), e); - }); - } - - private final int value; - - ItemQuality(int value) { - this.value = value; - } - - public static ItemQuality getTypeByValue(int value) { - return map.getOrDefault(value, QUALITY_NONE); - } - - public static ItemQuality getTypeByName(String name) { - return stringMap.getOrDefault(name, QUALITY_NONE); - } - - public int getValue() { - return value; - } -} +package emu.grasscutter.game.inventory; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public enum ItemQuality { + QUALITY_NONE(0), + QUALITY_WHITE(1), + QUALITY_GREEN(2), + QUALITY_BLUE(3), + QUALITY_PURPLE(4), + QUALITY_ORANGE(5), + QUALITY_ORANGE_SP(105); + + private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); + private static final Map stringMap = new HashMap<>(); + + static { + Stream.of(values()) + .forEach( + e -> { + map.put(e.getValue(), e); + stringMap.put(e.name(), e); + }); + } + + @Getter private final int value; + + ItemQuality(int value) { + this.value = value; + } + + public static ItemQuality getTypeByValue(int value) { + return map.getOrDefault(value, QUALITY_NONE); + } + + public static ItemQuality getTypeByName(String name) { + return stringMap.getOrDefault(name, QUALITY_NONE); + } +} diff --git a/src/main/java/emu/grasscutter/game/inventory/ItemType.java b/src/main/java/emu/grasscutter/game/inventory/ItemType.java index 7cf351fd8..19d7135fd 100644 --- a/src/main/java/emu/grasscutter/game/inventory/ItemType.java +++ b/src/main/java/emu/grasscutter/game/inventory/ItemType.java @@ -1,47 +1,45 @@ -package emu.grasscutter.game.inventory; - -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; - -public enum ItemType { - ITEM_NONE(0), - ITEM_VIRTUAL(1), - ITEM_MATERIAL(2), - ITEM_RELIQUARY(3), - ITEM_WEAPON(4), - ITEM_DISPLAY(5), - ITEM_FURNITURE(6); - - private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); - private static final Map stringMap = new HashMap<>(); - - static { - Stream.of(values()) - .forEach( - e -> { - map.put(e.getValue(), e); - stringMap.put(e.name(), e); - }); - } - - private final int value; - - ItemType(int value) { - this.value = value; - } - - public static ItemType getTypeByValue(int value) { - return map.getOrDefault(value, ITEM_NONE); - } - - public static ItemType getTypeByName(String name) { - return stringMap.getOrDefault(name, ITEM_NONE); - } - - public int getValue() { - return value; - } -} +package emu.grasscutter.game.inventory; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public enum ItemType { + ITEM_NONE(0), + ITEM_VIRTUAL(1), + ITEM_MATERIAL(2), + ITEM_RELIQUARY(3), + ITEM_WEAPON(4), + ITEM_DISPLAY(5), + ITEM_FURNITURE(6); + + private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); + private static final Map stringMap = new HashMap<>(); + + static { + Stream.of(values()) + .forEach( + e -> { + map.put(e.getValue(), e); + stringMap.put(e.name(), e); + }); + } + + @Getter private final int value; + + ItemType(int value) { + this.value = value; + } + + public static ItemType getTypeByValue(int value) { + return map.getOrDefault(value, ITEM_NONE); + } + + public static ItemType getTypeByName(String name) { + return stringMap.getOrDefault(name, ITEM_NONE); + } +} diff --git a/src/main/java/emu/grasscutter/game/inventory/MaterialType.java b/src/main/java/emu/grasscutter/game/inventory/MaterialType.java index 58fbe98ac..716e8270c 100644 --- a/src/main/java/emu/grasscutter/game/inventory/MaterialType.java +++ b/src/main/java/emu/grasscutter/game/inventory/MaterialType.java @@ -1,81 +1,79 @@ -package emu.grasscutter.game.inventory; - -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Stream; - -public enum MaterialType { - MATERIAL_NONE(0), - MATERIAL_FOOD(1), - MATERIAL_QUEST(2), - MATERIAL_EXCHANGE(4), - MATERIAL_CONSUME(5), - MATERIAL_EXP_FRUIT(6), - MATERIAL_AVATAR(7), - MATERIAL_ADSORBATE(8), - MATERIAL_CRICKET(9), - MATERIAL_ELEM_CRYSTAL(10), - MATERIAL_WEAPON_EXP_STONE(11), - MATERIAL_CHEST(12), - MATERIAL_RELIQUARY_MATERIAL(13), - MATERIAL_AVATAR_MATERIAL(14), - MATERIAL_NOTICE_ADD_HP(15), - MATERIAL_SEA_LAMP(16), - MATERIAL_SELECTABLE_CHEST(17), - MATERIAL_FLYCLOAK(18), - MATERIAL_NAMECARD(19), - MATERIAL_TALENT(20), - MATERIAL_WIDGET(21), - MATERIAL_CHEST_BATCH_USE(22), - MATERIAL_FAKE_ABSORBATE(23), - MATERIAL_CONSUME_BATCH_USE(24), - MATERIAL_WOOD(25), - MATERIAL_FURNITURE_FORMULA(27), - MATERIAL_CHANNELLER_SLAB_BUFF(28), - MATERIAL_FURNITURE_SUITE_FORMULA(29), - MATERIAL_COSTUME(30), - MATERIAL_HOME_SEED(31), - MATERIAL_FISH_BAIT(32), - MATERIAL_FISH_ROD(33), - MATERIAL_SUMO_BUFF(34), - MATERIAL_FIREWORKS(35), - MATERIAL_BGM(36), - MATERIAL_SPICE_FOOD(37), - MATERIAL_ACTIVITY_ROBOT(38), - MATERIAL_ACTIVITY_GEAR(39), - MATERIAL_ACTIVITY_JIGSAW(40), - MATERIAL_ARANARA(41), - MATERIAL_DESHRET_MANUAL(46); - - private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); - private static final Map stringMap = new HashMap<>(); - - static { - Stream.of(values()) - .forEach( - e -> { - map.put(e.getValue(), e); - stringMap.put(e.name(), e); - }); - } - - private final int value; - - MaterialType(int value) { - this.value = value; - } - - public static MaterialType getTypeByValue(int value) { - return map.getOrDefault(value, MATERIAL_NONE); - } - - public static MaterialType getTypeByName(String name) { - return stringMap.getOrDefault(name, MATERIAL_NONE); - } - - public int getValue() { - return value; - } -} +package emu.grasscutter.game.inventory; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import lombok.Getter; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +public enum MaterialType { + MATERIAL_NONE(0), + MATERIAL_FOOD(1), + MATERIAL_QUEST(2), + MATERIAL_EXCHANGE(4), + MATERIAL_CONSUME(5), + MATERIAL_EXP_FRUIT(6), + MATERIAL_AVATAR(7), + MATERIAL_ADSORBATE(8), + MATERIAL_CRICKET(9), + MATERIAL_ELEM_CRYSTAL(10), + MATERIAL_WEAPON_EXP_STONE(11), + MATERIAL_CHEST(12), + MATERIAL_RELIQUARY_MATERIAL(13), + MATERIAL_AVATAR_MATERIAL(14), + MATERIAL_NOTICE_ADD_HP(15), + MATERIAL_SEA_LAMP(16), + MATERIAL_SELECTABLE_CHEST(17), + MATERIAL_FLYCLOAK(18), + MATERIAL_NAMECARD(19), + MATERIAL_TALENT(20), + MATERIAL_WIDGET(21), + MATERIAL_CHEST_BATCH_USE(22), + MATERIAL_FAKE_ABSORBATE(23), + MATERIAL_CONSUME_BATCH_USE(24), + MATERIAL_WOOD(25), + MATERIAL_FURNITURE_FORMULA(27), + MATERIAL_CHANNELLER_SLAB_BUFF(28), + MATERIAL_FURNITURE_SUITE_FORMULA(29), + MATERIAL_COSTUME(30), + MATERIAL_HOME_SEED(31), + MATERIAL_FISH_BAIT(32), + MATERIAL_FISH_ROD(33), + MATERIAL_SUMO_BUFF(34), + MATERIAL_FIREWORKS(35), + MATERIAL_BGM(36), + MATERIAL_SPICE_FOOD(37), + MATERIAL_ACTIVITY_ROBOT(38), + MATERIAL_ACTIVITY_GEAR(39), + MATERIAL_ACTIVITY_JIGSAW(40), + MATERIAL_ARANARA(41), + MATERIAL_DESHRET_MANUAL(46); + + private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); + private static final Map stringMap = new HashMap<>(); + + static { + Stream.of(values()) + .forEach( + e -> { + map.put(e.getValue(), e); + stringMap.put(e.name(), e); + }); + } + + @Getter private final int value; + + MaterialType(int value) { + this.value = value; + } + + public static MaterialType getTypeByValue(int value) { + return map.getOrDefault(value, MATERIAL_NONE); + } + + public static MaterialType getTypeByName(String name) { + return stringMap.getOrDefault(name, MATERIAL_NONE); + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/blossom/BlossomManager.java b/src/main/java/emu/grasscutter/game/managers/blossom/BlossomManager.java index a170ab012..5b28f45df 100644 --- a/src/main/java/emu/grasscutter/game/managers/blossom/BlossomManager.java +++ b/src/main/java/emu/grasscutter/game/managers/blossom/BlossomManager.java @@ -1,243 +1,244 @@ -package emu.grasscutter.game.managers.blossom; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.GameDepot; -import emu.grasscutter.data.common.ItemParamData; -import emu.grasscutter.data.excels.RewardPreviewData; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.GameEntity; -import emu.grasscutter.game.entity.gadget.GadgetWorktop; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.game.world.SpawnDataEntry; -import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry; -import emu.grasscutter.net.proto.BlossomBriefInfoOuterClass; -import emu.grasscutter.net.proto.VisionTypeOuterClass; -import emu.grasscutter.server.packet.send.PacketBlossomBriefInfoNotify; -import emu.grasscutter.utils.Utils; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntList; -import java.util.ArrayList; -import java.util.List; - -public class BlossomManager { - private final Scene scene; - private final List blossomActivities = new ArrayList<>(); - private final List activeChests = new ArrayList<>(); - private final List createdEntity = new ArrayList<>(); - private final List blossomConsumed = new ArrayList<>(); - - public BlossomManager(Scene scene) { - this.scene = scene; - } - - private static Integer getPreviewReward(BlossomType type, int worldLevel) { - // TODO: blossoms should be based on their city - if (type == null) { - Grasscutter.getLogger().error("Illegal blossom type {}", type); - return null; - } - - int blossomChestId = type.getBlossomChestId(); - var dataMap = GameData.getBlossomRefreshExcelConfigDataMap(); - for (var data : dataMap.values()) { - if (blossomChestId == data.getBlossomChestId()) { - var dropVecList = data.getDropVec(); - if (worldLevel > dropVecList.length) { - Grasscutter.getLogger().error("Illegal world level {}", worldLevel); - return null; - } - return dropVecList[worldLevel].getPreviewReward(); - } - } - Grasscutter.getLogger().error("Cannot find blossom type {}", type); - return null; - } - - private static RewardPreviewData getRewardList(BlossomType type, int worldLevel) { - Integer previewReward = getPreviewReward(type, worldLevel); - if (previewReward == null) return null; - return GameData.getRewardPreviewDataMap().get((int) previewReward); - } - - public static IntList getRandomMonstersID(int difficulty, int count) { - IntList result = new IntArrayList(); - List monsters = - GameDepot.getBlossomConfig().getMonsterIdsPerDifficulty().get(difficulty); - for (int i = 0; i < count; i++) { - result.add((int) monsters.get(Utils.randomRange(0, monsters.size() - 1))); - } - return result; - } - - public void onTick() { - synchronized (blossomActivities) { - var it = blossomActivities.iterator(); - while (it.hasNext()) { - var active = it.next(); - active.onTick(); - if (active.getPass()) { - EntityGadget chest = active.getChest(); - scene.addEntity(chest); - scene.setChallenge(null); - activeChests.add(active); - it.remove(); - } - } - } - } - - public void recycleGadgetEntity(List entities) { - for (var entity : entities) { - if (entity instanceof EntityGadget gadget) { - createdEntity.remove(gadget); - } - } - notifyIcon(); - } - - public void initBlossom(EntityGadget gadget) { - if (createdEntity.contains(gadget)) { - return; - } - if (blossomConsumed.contains(gadget.getSpawnEntry())) { - return; - } - var id = gadget.getGadgetId(); - if (BlossomType.valueOf(id) == null) { - return; - } - gadget.buildContent(); - gadget.setState(204); - int worldLevel = getWorldLevel(); - GadgetWorktop gadgetWorktop = ((GadgetWorktop) gadget.getContent()); - gadgetWorktop.addWorktopOptions(new int[] {187}); - gadgetWorktop.setOnSelectWorktopOptionEvent( - (GadgetWorktop context, int option) -> { - BlossomActivity activity; - EntityGadget entityGadget = context.getGadget(); - synchronized (blossomActivities) { - for (BlossomActivity i : this.blossomActivities) { - if (i.getGadget() == entityGadget) { - return false; - } - } - - int volume = 0; - IntList monsters = new IntArrayList(); - while (true) { - var remain = GameDepot.getBlossomConfig().getMonsterFightingVolume() - volume; - if (remain <= 0) { - break; - } - var rand = Utils.randomRange(1, 100); - if (rand > 85 && remain >= 50) { // 15% ,generate strong monster - monsters.addAll(getRandomMonstersID(2, 1)); - volume += 50; - } else if (rand > 50 && remain >= 20) { // 35% ,generate normal monster - monsters.addAll(getRandomMonstersID(1, 1)); - volume += 20; - } else { // 50% ,generate weak monster - monsters.addAll(getRandomMonstersID(0, 1)); - volume += 10; - } - } - - Grasscutter.getLogger().info("Blossom Monsters:" + monsters); - - activity = new BlossomActivity(entityGadget, monsters, -1, worldLevel); - blossomActivities.add(activity); - } - entityGadget.updateState(201); - scene.setChallenge(activity.getChallenge()); - scene.removeEntity(entityGadget, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE); - activity.start(); - return true; - }); - createdEntity.add(gadget); - notifyIcon(); - } - - public void notifyIcon() { - final int wl = getWorldLevel(); - final int worldLevel = (wl < 0) ? 0 : ((wl > 8) ? 8 : wl); - final var worldLevelData = GameData.getWorldLevelDataMap().get(worldLevel); - final int monsterLevel = (worldLevelData != null) ? worldLevelData.getMonsterLevel() : 1; - List blossoms = new ArrayList<>(); - GameDepot.getSpawnLists() - .forEach( - (gridBlockId, spawnDataEntryList) -> { - int sceneId = gridBlockId.getSceneId(); - spawnDataEntryList.stream() - .map(SpawnDataEntry::getGroup) - .map(SpawnGroupEntry::getSpawns) - .flatMap(List::stream) - .filter(spawn -> !blossomConsumed.contains(spawn)) - .filter(spawn -> BlossomType.valueOf(spawn.getGadgetId()) != null) - .forEach( - spawn -> { - var type = BlossomType.valueOf(spawn.getGadgetId()); - int previewReward = getPreviewReward(type, worldLevel); - blossoms.add( - BlossomBriefInfoOuterClass.BlossomBriefInfo.newBuilder() - .setSceneId(sceneId) - .setPos(spawn.getPos().toProto()) - .setResin(20) - .setMonsterLevel(monsterLevel) - .setRewardId(previewReward) - .setCircleCampId(type.getCircleCampId()) - .setRefreshId( - type.getBlossomChestId()) // TODO: replace when using actual - // leylines - .build()); - }); - }); - scene.broadcastPacket(new PacketBlossomBriefInfoNotify(blossoms)); - } - - public int getWorldLevel() { - return scene.getWorld().getWorldLevel(); - } - - public List onReward(Player player, EntityGadget chest, boolean useCondensedResin) { - var resinManager = player.getResinManager(); - synchronized (activeChests) { - var it = activeChests.iterator(); - while (it.hasNext()) { - var activeChest = it.next(); - if (activeChest.getChest() == chest) { - boolean pay = - useCondensedResin ? resinManager.useCondensedResin(1) : resinManager.useResin(20); - if (pay) { - int worldLevel = getWorldLevel(); - List items = new ArrayList<>(); - var gadget = activeChest.getGadget(); - var type = BlossomType.valueOf(gadget.getGadgetId()); - RewardPreviewData blossomRewards = getRewardList(type, worldLevel); - if (blossomRewards == null) { - Grasscutter.getLogger() - .error("Blossom could not support world level : " + worldLevel); - return null; - } - var rewards = blossomRewards.getPreviewItems(); - for (ItemParamData blossomReward : rewards) { - int rewardCount = blossomReward.getCount(); - if (useCondensedResin) { - rewardCount += blossomReward.getCount(); // Double! - } - items.add(new GameItem(blossomReward.getItemId(), rewardCount)); - } - it.remove(); - recycleGadgetEntity(List.of(gadget)); - blossomConsumed.add(gadget.getSpawnEntry()); - return items; - } - return null; - } - } - } - return null; - } -} +package emu.grasscutter.game.managers.blossom; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.GameDepot; +import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.data.excels.RewardPreviewData; +import emu.grasscutter.game.entity.EntityGadget; +import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.entity.gadget.GadgetWorktop; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.game.world.SpawnDataEntry; +import emu.grasscutter.game.world.SpawnDataEntry.SpawnGroupEntry; +import emu.grasscutter.net.proto.BlossomBriefInfoOuterClass; +import emu.grasscutter.net.proto.VisionTypeOuterClass; +import emu.grasscutter.server.packet.send.PacketBlossomBriefInfoNotify; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import java.util.ArrayList; +import java.util.List; + +public class BlossomManager { + private final Scene scene; + private final List blossomActivities = new ArrayList<>(); + private final List activeChests = new ArrayList<>(); + private final List createdEntity = new ArrayList<>(); + + private final List blossomConsumed = new ArrayList<>(); + + public BlossomManager(Scene scene) { + this.scene = scene; + } + + public void onTick() { + synchronized (blossomActivities) { + var it = blossomActivities.iterator(); + while (it.hasNext()) { + var active = it.next(); + active.onTick(); + if (active.getPass()) { + EntityGadget chest = active.getChest(); + scene.addEntity(chest); + scene.setChallenge(null); + activeChests.add(active); + it.remove(); + } + } + } + } + + public void recycleGadgetEntity(List entities) { + for (var entity : entities) { + if (entity instanceof EntityGadget gadget) { + createdEntity.remove(gadget); + } + } + notifyIcon(); + } + + public void initBlossom(EntityGadget gadget) { + if (createdEntity.contains(gadget)) { + return; + } + if (blossomConsumed.contains(gadget.getSpawnEntry())) { + return; + } + var id = gadget.getGadgetId(); + if (BlossomType.valueOf(id) == null) { + return; + } + gadget.buildContent(); + gadget.setState(204); + int worldLevel = getWorldLevel(); + GadgetWorktop gadgetWorktop = ((GadgetWorktop) gadget.getContent()); + gadgetWorktop.addWorktopOptions(new int[] {187}); + gadgetWorktop.setOnSelectWorktopOptionEvent( + (GadgetWorktop context, int option) -> { + BlossomActivity activity; + EntityGadget entityGadget = context.getGadget(); + synchronized (blossomActivities) { + for (BlossomActivity i : this.blossomActivities) { + if (i.getGadget() == entityGadget) { + return false; + } + } + + int volume = 0; + IntList monsters = new IntArrayList(); + while (true) { + var remain = GameDepot.getBlossomConfig().getMonsterFightingVolume() - volume; + if (remain <= 0) { + break; + } + var rand = Utils.randomRange(1, 100); + if (rand > 85 && remain >= 50) { // 15% ,generate strong monster + monsters.addAll(getRandomMonstersID(2, 1)); + volume += 50; + } else if (rand > 50 && remain >= 20) { // 35% ,generate normal monster + monsters.addAll(getRandomMonstersID(1, 1)); + volume += 20; + } else { // 50% ,generate weak monster + monsters.addAll(getRandomMonstersID(0, 1)); + volume += 10; + } + } + + Grasscutter.getLogger().info("Blossom Monsters:" + monsters); + + activity = new BlossomActivity(entityGadget, monsters, -1, worldLevel); + blossomActivities.add(activity); + } + entityGadget.updateState(201); + scene.setChallenge(activity.getChallenge()); + scene.removeEntity(entityGadget, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE); + activity.start(); + return true; + }); + createdEntity.add(gadget); + notifyIcon(); + } + + public void notifyIcon() { + final int wl = getWorldLevel(); + final int worldLevel = (wl < 0) ? 0 : ((wl > 8) ? 8 : wl); + final var worldLevelData = GameData.getWorldLevelDataMap().get(worldLevel); + final int monsterLevel = (worldLevelData != null) ? worldLevelData.getMonsterLevel() : 1; + List blossoms = new ArrayList<>(); + GameDepot.getSpawnLists() + .forEach( + (gridBlockId, spawnDataEntryList) -> { + int sceneId = gridBlockId.getSceneId(); + spawnDataEntryList.stream() + .map(SpawnDataEntry::getGroup) + .map(SpawnGroupEntry::getSpawns) + .flatMap(List::stream) + .filter(spawn -> !blossomConsumed.contains(spawn)) + .filter(spawn -> BlossomType.valueOf(spawn.getGadgetId()) != null) + .forEach( + spawn -> { + var type = BlossomType.valueOf(spawn.getGadgetId()); + int previewReward = getPreviewReward(type, worldLevel); + blossoms.add( + BlossomBriefInfoOuterClass.BlossomBriefInfo.newBuilder() + .setSceneId(sceneId) + .setPos(spawn.getPos().toProto()) + .setResin(20) + .setMonsterLevel(monsterLevel) + .setRewardId(previewReward) + .setCircleCampId(type.getCircleCampId()) + .setRefreshId( + type.getBlossomChestId()) // TODO: replace when using actual + // leylines + .build()); + }); + }); + scene.broadcastPacket(new PacketBlossomBriefInfoNotify(blossoms)); + } + + public int getWorldLevel() { + return scene.getWorld().getWorldLevel(); + } + + private static Integer getPreviewReward(BlossomType type, int worldLevel) { + // TODO: blossoms should be based on their city + if (type == null) { + Grasscutter.getLogger().error("Illegal blossom type {}", type); + return null; + } + + int blossomChestId = type.getBlossomChestId(); + var dataMap = GameData.getBlossomRefreshExcelConfigDataMap(); + for (var data : dataMap.values()) { + if (blossomChestId == data.getBlossomChestId()) { + var dropVecList = data.getDropVec(); + if (worldLevel > dropVecList.length) { + Grasscutter.getLogger().error("Illegal world level {}", worldLevel); + return null; + } + return dropVecList[worldLevel].getPreviewReward(); + } + } + Grasscutter.getLogger().error("Cannot find blossom type {}", type); + return null; + } + + private static RewardPreviewData getRewardList(BlossomType type, int worldLevel) { + Integer previewReward = getPreviewReward(type, worldLevel); + if (previewReward == null) return null; + return GameData.getRewardPreviewDataMap().get((int) previewReward); + } + + public List onReward(Player player, EntityGadget chest, boolean useCondensedResin) { + var resinManager = player.getResinManager(); + synchronized (activeChests) { + var it = activeChests.iterator(); + while (it.hasNext()) { + var activeChest = it.next(); + if (activeChest.getChest() == chest) { + boolean pay = + useCondensedResin ? resinManager.useCondensedResin(1) : resinManager.useResin(20); + if (pay) { + int worldLevel = getWorldLevel(); + List items = new ArrayList<>(); + var gadget = activeChest.getGadget(); + var type = BlossomType.valueOf(gadget.getGadgetId()); + RewardPreviewData blossomRewards = getRewardList(type, worldLevel); + if (blossomRewards == null) { + Grasscutter.getLogger() + .error("Blossom could not support world level : " + worldLevel); + return null; + } + var rewards = blossomRewards.getPreviewItems(); + for (ItemParamData blossomReward : rewards) { + int rewardCount = blossomReward.getCount(); + if (useCondensedResin) { + rewardCount += blossomReward.getCount(); // Double! + } + items.add(new GameItem(blossomReward.getItemId(), rewardCount)); + } + it.remove(); + recycleGadgetEntity(List.of(gadget)); + blossomConsumed.add(gadget.getSpawnEntry()); + return items; + } + return null; + } + } + } + return null; + } + + public static IntList getRandomMonstersID(int difficulty, int count) { + IntList result = new IntArrayList(); + List monsters = + GameDepot.getBlossomConfig().getMonsterIdsPerDifficulty().get(difficulty); + for (int i = 0; i < count; i++) { + result.add((int) monsters.get(Utils.randomRange(0, monsters.size() - 1))); + } + return result; + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/cooking/CookingCompoundManager.java b/src/main/java/emu/grasscutter/game/managers/cooking/CookingCompoundManager.java index 604c31443..b24eadbfc 100644 --- a/src/main/java/emu/grasscutter/game/managers/cooking/CookingCompoundManager.java +++ b/src/main/java/emu/grasscutter/game/managers/cooking/CookingCompoundManager.java @@ -1,163 +1,163 @@ -package emu.grasscutter.game.managers.cooking; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.common.ItemParamData; -import emu.grasscutter.data.excels.CompoundData; -import emu.grasscutter.game.inventory.GameItem; -import emu.grasscutter.game.player.BasePlayerManager; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.net.proto.CompoundQueueDataOuterClass.CompoundQueueData; -import emu.grasscutter.net.proto.GetCompoundDataReqOuterClass.GetCompoundDataReq; -import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; -import emu.grasscutter.net.proto.PlayerCompoundMaterialReqOuterClass.PlayerCompoundMaterialReq; -import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; -import emu.grasscutter.net.proto.TakeCompoundOutputReqOuterClass.TakeCompoundOutputReq; -import emu.grasscutter.server.packet.send.PackageTakeCompoundOutputRsp; -import emu.grasscutter.server.packet.send.PacketCompoundDataNotify; -import emu.grasscutter.server.packet.send.PacketGetCompoundDataRsp; -import emu.grasscutter.server.packet.send.PacketPlayerCompoundMaterialRsp; -import emu.grasscutter.utils.Utils; -import java.util.*; - -public class CookingCompoundManager extends BasePlayerManager { - private static Set defaultUnlockedCompounds; - private static Map> compoundGroups; - // TODO:bind it to player - private static Set unlocked; - - public CookingCompoundManager(Player player) { - super(player); - } - - public static void initialize() { - defaultUnlockedCompounds = new HashSet<>(); - compoundGroups = new HashMap<>(); - GameData.getCompoundDataMap() - .forEach( - (id, compound) -> { - if (compound.isDefaultUnlocked()) { - defaultUnlockedCompounds.add(id); - } - compoundGroups.computeIfAbsent(compound.getGroupID(), gid -> new HashSet<>()).add(id); - }); - // TODO:Because we haven't implemented fishing feature,unlock all compounds related to - // fish.Besides,it should be bound to player rather than manager. - unlocked = new HashSet<>(defaultUnlockedCompounds); - if (compoundGroups.containsKey(3)) // Avoid NPE from Resources error - unlocked.addAll(compoundGroups.get(3)); - } - - private synchronized List getCompoundQueueData() { - List compoundQueueData = - new ArrayList<>(player.getActiveCookCompounds().size()); - int currentTime = Utils.getCurrentSeconds(); - for (var item : player.getActiveCookCompounds().values()) { - var data = - CompoundQueueData.newBuilder() - .setCompoundId(item.getCompoundId()) - .setOutputCount(item.getOutputCount(currentTime)) - .setOutputTime(item.getOutputTime(currentTime)) - .setWaitCount(item.getWaitCount(currentTime)) - .build(); - compoundQueueData.add(data); - } - return compoundQueueData; - } - - public synchronized void handleGetCompoundDataReq(GetCompoundDataReq req) { - player.sendPacket(new PacketGetCompoundDataRsp(unlocked, getCompoundQueueData())); - } - - public synchronized void handlePlayerCompoundMaterialReq(PlayerCompoundMaterialReq req) { - int id = req.getCompoundId(), count = req.getCount(); - CompoundData compound = GameData.getCompoundDataMap().get(id); - var activeCompounds = player.getActiveCookCompounds(); - - // check whether the compound is available - // TODO:add other compounds,see my pr for detail - if (!unlocked.contains(id)) { - player.sendPacket(new PacketPlayerCompoundMaterialRsp(Retcode.RET_FAIL_VALUE)); - return; - } - // check whether the queue is full - if (activeCompounds.containsKey(id) - && activeCompounds.get(id).getTotalCount() + count > compound.getQueueSize()) { - player.sendPacket(new PacketPlayerCompoundMaterialRsp(Retcode.RET_COMPOUND_QUEUE_FULL_VALUE)); - return; - } - // try to consume raw materials - if (!player.getInventory().payItems(compound.getInputVec(), count)) { - // TODO:I'm not sure whether retcode is correct. - player.sendPacket( - new PacketPlayerCompoundMaterialRsp(Retcode.RET_ITEM_COUNT_NOT_ENOUGH_VALUE)); - return; - } - ActiveCookCompoundData c; - int currentTime = Utils.getCurrentSeconds(); - if (activeCompounds.containsKey(id)) { - c = activeCompounds.get(id); - c.addCompound(count, currentTime); - } else { - c = new ActiveCookCompoundData(id, compound.getCostTime(), count, currentTime); - activeCompounds.put(id, c); - } - var data = - CompoundQueueData.newBuilder() - .setCompoundId(id) - .setOutputCount(c.getOutputCount(currentTime)) - .setOutputTime(c.getOutputTime(currentTime)) - .setWaitCount(c.getWaitCount(currentTime)) - .build(); - player.sendPacket(new PacketPlayerCompoundMaterialRsp(data)); - } - - public synchronized void handleTakeCompoundOutputReq(TakeCompoundOutputReq req) { - // Client won't set compound_id and will set group_id instead. - int groupId = req.getCompoundGroupId(); - var activeCompounds = player.getActiveCookCompounds(); - int now = Utils.getCurrentSeconds(); - // check available queues - boolean success = false; - Map allRewards = new HashMap<>(); - for (int id : compoundGroups.get(groupId)) { - if (!activeCompounds.containsKey(id)) continue; - int quantity = activeCompounds.get(id).takeCompound(now); - if (activeCompounds.get(id).getTotalCount() == 0) activeCompounds.remove(id); - if (quantity == 0) continue; - List rewards = GameData.getCompoundDataMap().get(id).getOutputVec(); - for (var i : rewards) { - if (i.getId() == 0) continue; - if (allRewards.containsKey(i.getId())) { - GameItem item = allRewards.get(i.getId()); - item.setCount(item.getCount() + i.getCount() * quantity); - } else { - allRewards.put(i.getId(), new GameItem(i.getId(), i.getCount() * quantity)); - } - } - success = true; - } - // give player the rewards - if (success) { - player.getInventory().addItems(allRewards.values(), ActionReason.Compound); - player.sendPacket( - new PackageTakeCompoundOutputRsp( - allRewards.values().stream() - .map( - i -> - ItemParam.newBuilder() - .setItemId(i.getItemId()) - .setCount(i.getCount()) - .build()) - .toList(), - Retcode.RET_SUCC_VALUE)); - } else { - player.sendPacket( - new PackageTakeCompoundOutputRsp(null, Retcode.RET_COMPOUND_NOT_FINISH_VALUE)); - } - } - - public void onPlayerLogin() { - player.sendPacket(new PacketCompoundDataNotify(unlocked, getCompoundQueueData())); - } -} +package emu.grasscutter.game.managers.cooking; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.data.excels.CompoundData; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.player.BasePlayerManager; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.net.proto.CompoundQueueDataOuterClass.CompoundQueueData; +import emu.grasscutter.net.proto.GetCompoundDataReqOuterClass.GetCompoundDataReq; +import emu.grasscutter.net.proto.ItemParamOuterClass.ItemParam; +import emu.grasscutter.net.proto.PlayerCompoundMaterialReqOuterClass.PlayerCompoundMaterialReq; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; +import emu.grasscutter.net.proto.TakeCompoundOutputReqOuterClass.TakeCompoundOutputReq; +import emu.grasscutter.server.packet.send.PackageTakeCompoundOutputRsp; +import emu.grasscutter.server.packet.send.PacketCompoundDataNotify; +import emu.grasscutter.server.packet.send.PacketGetCompoundDataRsp; +import emu.grasscutter.server.packet.send.PacketPlayerCompoundMaterialRsp; +import emu.grasscutter.utils.Utils; +import java.util.*; + +public class CookingCompoundManager extends BasePlayerManager { + private static Set defaultUnlockedCompounds; + private static Map> compoundGroups; + // TODO:bind it to player + private static Set unlocked; + + public CookingCompoundManager(Player player) { + super(player); + } + + public static void initialize() { + defaultUnlockedCompounds = new HashSet<>(); + compoundGroups = new HashMap<>(); + GameData.getCompoundDataMap() + .forEach( + (id, compound) -> { + if (compound.isDefaultUnlocked()) { + defaultUnlockedCompounds.add(id); + } + compoundGroups.computeIfAbsent(compound.getGroupId(), gid -> new HashSet<>()).add(id); + }); + // TODO:Because we haven't implemented fishing feature,unlock all compounds related to + // fish.Besides,it should be bound to player rather than manager. + unlocked = new HashSet<>(defaultUnlockedCompounds); + if (compoundGroups.containsKey(3)) // Avoid NPE from Resources error + unlocked.addAll(compoundGroups.get(3)); + } + + private synchronized List getCompoundQueueData() { + List compoundQueueData = + new ArrayList<>(player.getActiveCookCompounds().size()); + int currentTime = Utils.getCurrentSeconds(); + for (var item : player.getActiveCookCompounds().values()) { + var data = + CompoundQueueData.newBuilder() + .setCompoundId(item.getCompoundId()) + .setOutputCount(item.getOutputCount(currentTime)) + .setOutputTime(item.getOutputTime(currentTime)) + .setWaitCount(item.getWaitCount(currentTime)) + .build(); + compoundQueueData.add(data); + } + return compoundQueueData; + } + + public synchronized void handleGetCompoundDataReq(GetCompoundDataReq req) { + player.sendPacket(new PacketGetCompoundDataRsp(unlocked, getCompoundQueueData())); + } + + public synchronized void handlePlayerCompoundMaterialReq(PlayerCompoundMaterialReq req) { + int id = req.getCompoundId(), count = req.getCount(); + CompoundData compound = GameData.getCompoundDataMap().get(id); + var activeCompounds = player.getActiveCookCompounds(); + + // check whether the compound is available + // TODO:add other compounds,see my pr for detail + if (!unlocked.contains(id)) { + player.sendPacket(new PacketPlayerCompoundMaterialRsp(Retcode.RET_FAIL_VALUE)); + return; + } + // check whether the queue is full + if (activeCompounds.containsKey(id) + && activeCompounds.get(id).getTotalCount() + count > compound.getQueueSize()) { + player.sendPacket(new PacketPlayerCompoundMaterialRsp(Retcode.RET_COMPOUND_QUEUE_FULL_VALUE)); + return; + } + // try to consume raw materials + if (!player.getInventory().payItems(compound.getInputVec(), count)) { + // TODO:I'm not sure whether retcode is correct. + player.sendPacket( + new PacketPlayerCompoundMaterialRsp(Retcode.RET_ITEM_COUNT_NOT_ENOUGH_VALUE)); + return; + } + ActiveCookCompoundData c; + int currentTime = Utils.getCurrentSeconds(); + if (activeCompounds.containsKey(id)) { + c = activeCompounds.get(id); + c.addCompound(count, currentTime); + } else { + c = new ActiveCookCompoundData(id, compound.getCostTime(), count, currentTime); + activeCompounds.put(id, c); + } + var data = + CompoundQueueData.newBuilder() + .setCompoundId(id) + .setOutputCount(c.getOutputCount(currentTime)) + .setOutputTime(c.getOutputTime(currentTime)) + .setWaitCount(c.getWaitCount(currentTime)) + .build(); + player.sendPacket(new PacketPlayerCompoundMaterialRsp(data)); + } + + public synchronized void handleTakeCompoundOutputReq(TakeCompoundOutputReq req) { + // Client won't set compound_id and will set group_id instead. + int groupId = req.getCompoundGroupId(); + var activeCompounds = player.getActiveCookCompounds(); + int now = Utils.getCurrentSeconds(); + // check available queues + boolean success = false; + Map allRewards = new HashMap<>(); + for (int id : compoundGroups.get(groupId)) { + if (!activeCompounds.containsKey(id)) continue; + int quantity = activeCompounds.get(id).takeCompound(now); + if (activeCompounds.get(id).getTotalCount() == 0) activeCompounds.remove(id); + if (quantity == 0) continue; + List rewards = GameData.getCompoundDataMap().get(id).getOutputVec(); + for (var i : rewards) { + if (i.getId() == 0) continue; + if (allRewards.containsKey(i.getId())) { + GameItem item = allRewards.get(i.getId()); + item.setCount(item.getCount() + i.getCount() * quantity); + } else { + allRewards.put(i.getId(), new GameItem(i.getId(), i.getCount() * quantity)); + } + } + success = true; + } + // give player the rewards + if (success) { + player.getInventory().addItems(allRewards.values(), ActionReason.Compound); + player.sendPacket( + new PackageTakeCompoundOutputRsp( + allRewards.values().stream() + .map( + i -> + ItemParam.newBuilder() + .setItemId(i.getItemId()) + .setCount(i.getCount()) + .build()) + .toList(), + Retcode.RET_SUCC_VALUE)); + } else { + player.sendPacket( + new PackageTakeCompoundOutputRsp(null, Retcode.RET_COMPOUND_NOT_FINISH_VALUE)); + } + } + + public void onPlayerLogin() { + player.sendPacket(new PacketCompoundDataNotify(unlocked, getCompoundQueueData())); + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java b/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java index cd3dfeaf0..a624855ac 100644 --- a/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java +++ b/src/main/java/emu/grasscutter/game/managers/energy/EnergyManager.java @@ -1,420 +1,418 @@ -package emu.grasscutter.game.managers.energy; - -import static emu.grasscutter.config.Configuration.GAME_OPTIONS; - -import com.google.protobuf.InvalidProtocolBufferException; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.DataLoader; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.ItemData; -import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; -import emu.grasscutter.data.excels.monster.MonsterData.HpDrops; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.entity.*; -import emu.grasscutter.game.player.BasePlayerManager; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.props.ElementType; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.props.MonsterType; -import emu.grasscutter.game.props.WeaponType; -import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall; -import emu.grasscutter.net.proto.AbilityIdentifierOuterClass.AbilityIdentifier; -import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; -import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; -import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason; -import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; -import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; -import emu.grasscutter.server.game.GameSession; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.ThreadLocalRandom; -import lombok.Getter; - -public class EnergyManager extends BasePlayerManager { - private static final Int2ObjectMap> energyDropData = - new Int2ObjectOpenHashMap<>(); - private static final Int2ObjectMap> - skillParticleGenerationData = new Int2ObjectOpenHashMap<>(); - private final Object2IntMap avatarNormalProbabilities; - @Getter private boolean energyUsage; // Should energy usage be enabled for this player? - - public EnergyManager(Player player) { - super(player); - this.avatarNormalProbabilities = new Object2IntOpenHashMap<>(); - this.energyUsage = GAME_OPTIONS.energyUsage; - } - - public static void initialize() { - // Read the data we need for monster energy drops. - try { - DataLoader.loadList("EnergyDrop.json", EnergyDropEntry.class) - .forEach( - entry -> { - energyDropData.put(entry.getDropId(), entry.getDropList()); - }); - - Grasscutter.getLogger().debug("Energy drop data successfully loaded."); - } catch (Exception ex) { - Grasscutter.getLogger().error("Unable to load energy drop data.", ex); - } - - // Read the data for particle generation from skills - try { - DataLoader.loadList("SkillParticleGeneration.json", SkillParticleGenerationEntry.class) - .forEach( - entry -> { - skillParticleGenerationData.put(entry.getAvatarId(), entry.getAmountList()); - }); - - Grasscutter.getLogger().debug("Skill particle generation data successfully loaded."); - } catch (Exception ex) { - Grasscutter.getLogger().error("Unable to load skill particle generation data data.", ex); - } - } - - /** Particle creation for elemental skills. */ - private int getBallCountForAvatar(int avatarId) { - // We default to two particles. - int count = 2; - - // If we don't have any data for this avatar, stop. - if (!skillParticleGenerationData.containsKey(avatarId)) { - Grasscutter.getLogger().warn("No particle generation data for avatarId {} found.", avatarId); - } - // If we do have data, roll for how many particles we should generate. - else { - int roll = ThreadLocalRandom.current().nextInt(0, 100); - int percentageStack = 0; - for (SkillParticleGenerationInfo info : skillParticleGenerationData.get(avatarId)) { - int chance = info.getChance(); - percentageStack += chance; - if (roll < percentageStack) { - count = info.getValue(); - break; - } - } - } - - // Done. - return count; - } - - private int getBallIdForElement(ElementType element) { - // If we have no element, we default to an element-less particle. - if (element == null) { - return 2024; - } - - // Otherwise, we determine the particle's ID based on the element. - return switch (element) { - case Fire -> 2017; - case Water -> 2018; - case Grass -> 2019; - case Electric -> 2020; - case Wind -> 2021; - case Ice -> 2022; - case Rock -> 2023; - default -> 2024; - }; - } - - public void handleGenerateElemBall(AbilityInvokeEntry invoke) - throws InvalidProtocolBufferException { - // ToDo: - // This is also called when a weapon like Favonius Warbow etc. creates energy through its - // passive. - // We are not handling this correctly at the moment. - - // Get action info. - AbilityActionGenerateElemBall action = - AbilityActionGenerateElemBall.parseFrom(invoke.getAbilityData()); - if (action == null) { - return; - } - - // Default to an elementless particle. - int itemId = 2024; - - // Generate 2 particles by default. - int amount = 2; - - // Try to get the casting avatar from the player's party. - Optional avatarEntity = - this.getCastingAvatarEntityForEnergy(invoke.getEntityId()); - - // Bug: invokes twice sometimes, Ayato, Keqing - // ToDo: deal with press, hold difference. deal with charge(Beidou, Yunjin) - if (avatarEntity.isPresent()) { - Avatar avatar = avatarEntity.get().getAvatar(); - - if (avatar != null) { - int avatarId = avatar.getAvatarId(); - AvatarSkillDepotData skillDepotData = avatar.getSkillDepot(); - - // Determine how many particles we need to create for this avatar. - amount = this.getBallCountForAvatar(avatarId); - - // Determine the avatar's element, and based on that the ID of the - // particles we have to generate. - if (skillDepotData != null) { - ElementType element = skillDepotData.getElementType(); - itemId = this.getBallIdForElement(element); - } - } - } - - // Generate the particles. - var pos = new Position(action.getPos()); - for (int i = 0; i < amount; i++) { - this.generateElemBall(itemId, pos, 1); - } - } - - /** - * Energy generation for NAs/CAs. - * - * @param avatar The avatar. - */ - private void generateEnergyForNormalAndCharged(EntityAvatar avatar) { - // This logic is based on the descriptions given in - // https://genshin-impact.fandom.com/wiki/Energy#Energy_Generated_by_Normal_Attacks - // https://library.keqingmains.com/combat-mechanics/energy#auto-attacking - // Those descriptions are lacking in some information, so this implementation most likely - // does not fully replicate the behavior of the official server. Open questions: - // - Does the probability for a character reset after some time? - // - Does the probability for a character reset when switching them out? - // - Does this really count every individual hit separately? - - // Get the avatar's weapon type. - WeaponType weaponType = avatar.getAvatar().getAvatarData().getWeaponType(); - - // Check if we already have probability data for this avatar. If not, insert it. - if (!this.avatarNormalProbabilities.containsKey(avatar)) { - this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability()); - } - - // Roll for energy. - int currentProbability = this.avatarNormalProbabilities.getInt(avatar); - int roll = ThreadLocalRandom.current().nextInt(0, 100); - - // If the player wins the roll, we increase the avatar's energy and reset the probability. - if (roll < currentProbability) { - avatar.addEnergy(1.0f, PropChangeReason.PROP_CHANGE_REASON_ABILITY, true); - this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability()); - } - // Otherwise, we increase the probability for the next hit. - else { - this.avatarNormalProbabilities.put( - avatar, currentProbability + weaponType.getEnergyGainIncreaseProbability()); - } - } - - public void handleAttackHit(EvtBeingHitInfo hitInfo) { - // Get the attack result. - AttackResult attackRes = hitInfo.getAttackResult(); - - // Make sure the attack was performed by the currently active avatar. If not, we ignore the hit. - Optional attackerEntity = - this.getCastingAvatarEntityForEnergy(attackRes.getAttackerId()); - if (attackerEntity.isEmpty() - || this.player.getTeamManager().getCurrentAvatarEntity().getId() - != attackerEntity.get().getId()) { - return; - } - - // Make sure the target is an actual enemy. - GameEntity targetEntity = this.player.getScene().getEntityById(attackRes.getDefenseId()); - if (!(targetEntity instanceof EntityMonster targetMonster)) { - return; - } - - MonsterType targetType = targetMonster.getMonsterData().getType(); - if (targetType != MonsterType.MONSTER_ORDINARY && targetType != MonsterType.MONSTER_BOSS) { - return; - } - - // Get the ability that caused this hit. - AbilityIdentifier ability = attackRes.getAbilityIdentifier(); - - // Make sure there is no actual "ability" associated with the hit. For now, this is how we - // identify normal and charged attacks. Note that this is not completely accurate: - // - Many character's charged attacks have an ability associated with them. This means that, - // for now, we don't identify charged attacks reliably. - // - There might also be some cases where we incorrectly identify something as a normal or - // charged attack that is not (Diluc's E?). - // - Catalyst normal attacks have an ability, so we don't handle those for now. - // ToDo: Fix all of that. - if (ability != AbilityIdentifier.getDefaultInstance()) { - return; - } - - // Handle the energy generation. - this.generateEnergyForNormalAndCharged(attackerEntity.get()); - } - - /* - * Energy logic related to using skills. - */ - - private void handleBurstCast(Avatar avatar, int skillId) { - // Don't do anything if energy usage is disabled. - if (!GAME_OPTIONS.energyUsage || !this.energyUsage) { - return; - } - - // If the cast skill was a burst, consume energy. - if (avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) { - avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START); - } - } - - public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) { - // Determine the entity that has cast the skill. Cancel if we can't find that avatar. - Optional caster = - this.player.getTeamManager().getActiveTeam().stream() - .filter(character -> character.getId() == casterId) - .findFirst(); - - if (caster.isEmpty()) { - return; - } - - Avatar avatar = caster.get().getAvatar(); - - // Handle elemental burst. - this.handleBurstCast(avatar, skillId); - } - - /* - * Monster energy drops. - */ - - private void generateElemBallDrops(EntityMonster monster, int dropId) { - // Generate all drops specified for the given drop id. - if (!energyDropData.containsKey(dropId)) { - Grasscutter.getLogger().warn("No drop data for dropId {} found.", dropId); - return; - } - - for (EnergyDropInfo info : energyDropData.get(dropId)) { - this.generateElemBall(info.getBallId(), monster.getPosition(), info.getCount()); - } - } - - public void handleMonsterEnergyDrop( - EntityMonster monster, float hpBeforeDamage, float hpAfterDamage) { - // Make sure this is actually a monster. - // Note that some wildlife also has that type, like boars or birds. - MonsterType type = monster.getMonsterData().getType(); - if (type != MonsterType.MONSTER_ORDINARY && type != MonsterType.MONSTER_BOSS) { - return; - } - - // Calculate the HP thresholds for before and after the damage was taken. - float maxHp = monster.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); - float thresholdBefore = hpBeforeDamage / maxHp; - float thresholdAfter = hpAfterDamage / maxHp; - - // Determine the thresholds the monster has passed, and generate drops based on that. - for (HpDrops drop : monster.getMonsterData().getHpDrops()) { - if (drop.getDropId() == 0) { - continue; - } - - float threshold = drop.getHpPercent() / 100.0f; - if (threshold < thresholdBefore && threshold >= thresholdAfter) { - this.generateElemBallDrops(monster, drop.getDropId()); - } - } - - // Handle kill drops. - if (hpAfterDamage <= 0 && monster.getMonsterData().getKillDropId() != 0) { - this.generateElemBallDrops(monster, monster.getMonsterData().getKillDropId()); - } - } - - /* - * Utilities. - */ - - private void generateElemBall(int ballId, Position position, int count) { - // Generate a particle/orb with the specified parameters. - ItemData itemData = GameData.getItemDataMap().get(ballId); - if (itemData == null) { - return; - } - - EntityItem energyBall = - new EntityItem(this.getPlayer().getScene(), this.getPlayer(), itemData, position, count); - this.getPlayer().getScene().addEntity(energyBall); - } - - private Optional getCastingAvatarEntityForEnergy(int invokeEntityId) { - // To determine the avatar that has cast the skill that caused the energy particle to be - // generated, - // we have to look at the entity that has invoked the ability. This can either be that avatar - // directly, - // or it can be an `EntityClientGadget`, owned (some way up the owner hierarchy) by the avatar - // that cast the skill. - - // Try to get the invoking entity from the scene. - GameEntity entity = this.player.getScene().getEntityById(invokeEntityId); - - // Determine the ID of the entity that originally cast this skill. If the scene entity is null, - // or not an `EntityClientGadget`, we assume that we are directly looking at the casting avatar - // (the null case will happen if the avatar was switched out between casting the skill and the - // particle being generated). If the scene entity is an `EntityClientGadget`, we need to find - // the - // ID of the original owner of that gadget. - int avatarEntityId = - (!(entity instanceof EntityClientGadget)) - ? invokeEntityId - : ((EntityClientGadget) entity).getOriginalOwnerEntityId(); - - // Finally, find the avatar entity in the player's team. - return this.player.getTeamManager().getActiveTeam().stream() - .filter(character -> character.getId() == avatarEntityId) - .findFirst(); - } - - /** - * Refills the energy of the active avatar. - * - * @return True if the energy was refilled, false otherwise. - */ - public boolean refillActiveEnergy() { - var activeEntity = this.player.getTeamManager().getCurrentAvatarEntity(); - return activeEntity.addEnergy( - activeEntity.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal()); - } - - /** - * Refills the energy of the entire team. - * - * @param changeReason The reason for the energy change. - * @param isFlat Whether the energy should be added as a flat value. - */ - public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) { - for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) { - // giving the exact amount read off the AvatarSkillData.json - entityAvatar.addEnergy( - entityAvatar.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal(), - changeReason, - isFlat); - } - } - - public void setEnergyUsage(boolean energyUsage) { - this.energyUsage = energyUsage; - if (!energyUsage) { // Refill team energy if usage is disabled - for (EntityAvatar entityAvatar : this.player.getTeamManager().getActiveTeam()) { - entityAvatar.addEnergy(1000, PropChangeReason.PROP_CHANGE_REASON_GM, true); - } - } - } -} +package emu.grasscutter.game.managers.energy; + +import static emu.grasscutter.config.Configuration.GAME_OPTIONS; + +import com.google.protobuf.InvalidProtocolBufferException; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.DataLoader; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.ItemData; +import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; +import emu.grasscutter.data.excels.monster.MonsterData.HpDrops; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.entity.*; +import emu.grasscutter.game.player.BasePlayerManager; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.ElementType; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.props.MonsterType; +import emu.grasscutter.game.props.WeaponType; +import emu.grasscutter.net.proto.AbilityActionGenerateElemBallOuterClass.AbilityActionGenerateElemBall; +import emu.grasscutter.net.proto.AbilityIdentifierOuterClass.AbilityIdentifier; +import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; +import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; +import emu.grasscutter.net.proto.ChangeEnergyReasonOuterClass.ChangeEnergyReason; +import emu.grasscutter.net.proto.EvtBeingHitInfoOuterClass.EvtBeingHitInfo; +import emu.grasscutter.net.proto.PropChangeReasonOuterClass.PropChangeReason; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; +import lombok.Getter; + +public class EnergyManager extends BasePlayerManager { + private static final Int2ObjectMap> energyDropData = + new Int2ObjectOpenHashMap<>(); + private static final Int2ObjectMap> + skillParticleGenerationData = new Int2ObjectOpenHashMap<>(); + private final Object2IntMap avatarNormalProbabilities; + @Getter private boolean energyUsage; // Should energy usage be enabled for this player? + + public EnergyManager(Player player) { + super(player); + this.avatarNormalProbabilities = new Object2IntOpenHashMap<>(); + this.energyUsage = GAME_OPTIONS.energyUsage; + } + + public static void initialize() { + // Read the data we need for monster energy drops. + try { + DataLoader.loadList("EnergyDrop.json", EnergyDropEntry.class) + .forEach( + entry -> { + energyDropData.put(entry.getDropId(), entry.getDropList()); + }); + + Grasscutter.getLogger().debug("Energy drop data successfully loaded."); + } catch (Exception ex) { + Grasscutter.getLogger().error("Unable to load energy drop data.", ex); + } + + // Read the data for particle generation from skills + try { + DataLoader.loadList("SkillParticleGeneration.json", SkillParticleGenerationEntry.class) + .forEach( + entry -> { + skillParticleGenerationData.put(entry.getAvatarId(), entry.getAmountList()); + }); + + Grasscutter.getLogger().debug("Skill particle generation data successfully loaded."); + } catch (Exception ex) { + Grasscutter.getLogger().error("Unable to load skill particle generation data data.", ex); + } + } + + /** Particle creation for elemental skills. */ + private int getBallCountForAvatar(int avatarId) { + // We default to two particles. + int count = 2; + + // If we don't have any data for this avatar, stop. + if (!skillParticleGenerationData.containsKey(avatarId)) { + Grasscutter.getLogger().warn("No particle generation data for avatarId {} found.", avatarId); + } + // If we do have data, roll for how many particles we should generate. + else { + int roll = ThreadLocalRandom.current().nextInt(0, 100); + int percentageStack = 0; + for (SkillParticleGenerationInfo info : skillParticleGenerationData.get(avatarId)) { + int chance = info.getChance(); + percentageStack += chance; + if (roll < percentageStack) { + count = info.getValue(); + break; + } + } + } + + // Done. + return count; + } + + private int getBallIdForElement(ElementType element) { + // If we have no element, we default to an element-less particle. + if (element == null) { + return 2024; + } + + // Otherwise, we determine the particle's ID based on the element. + return switch (element) { + case Fire -> 2017; + case Water -> 2018; + case Grass -> 2019; + case Electric -> 2020; + case Wind -> 2021; + case Ice -> 2022; + case Rock -> 2023; + default -> 2024; + }; + } + + public void handleGenerateElemBall(AbilityInvokeEntry invoke) + throws InvalidProtocolBufferException { + // ToDo: + // This is also called when a weapon like Favonius Warbow etc. creates energy through its + // passive. + // We are not handling this correctly at the moment. + + // Get action info. + AbilityActionGenerateElemBall action = + AbilityActionGenerateElemBall.parseFrom(invoke.getAbilityData()); + if (action == null) { + return; + } + + // Default to an elementless particle. + int itemId = 2024; + + // Generate 2 particles by default. + int amount = 2; + + // Try to get the casting avatar from the player's party. + Optional avatarEntity = + this.getCastingAvatarEntityForEnergy(invoke.getEntityId()); + + // Bug: invokes twice sometimes, Ayato, Keqing + // ToDo: deal with press, hold difference. deal with charge(Beidou, Yunjin) + if (avatarEntity.isPresent()) { + Avatar avatar = avatarEntity.get().getAvatar(); + + if (avatar != null) { + int avatarId = avatar.getAvatarId(); + AvatarSkillDepotData skillDepotData = avatar.getSkillDepot(); + + // Determine how many particles we need to create for this avatar. + amount = this.getBallCountForAvatar(avatarId); + + // Determine the avatar's element, and based on that the ID of the + // particles we have to generate. + if (skillDepotData != null) { + ElementType element = skillDepotData.getElementType(); + itemId = this.getBallIdForElement(element); + } + } + } + + // Generate the particles. + var pos = new Position(action.getPos()); + for (int i = 0; i < amount; i++) { + this.generateElemBall(itemId, pos, 1); + } + } + + /** + * Energy generation for NAs/CAs. + * + * @param avatar The avatar. + */ + private void generateEnergyForNormalAndCharged(EntityAvatar avatar) { + // This logic is based on the descriptions given in + // https://genshin-impact.fandom.com/wiki/Energy#Energy_Generated_by_Normal_Attacks + // https://library.keqingmains.com/combat-mechanics/energy#auto-attacking + // Those descriptions are lacking in some information, so this implementation most likely + // does not fully replicate the behavior of the official server. Open questions: + // - Does the probability for a character reset after some time? + // - Does the probability for a character reset when switching them out? + // - Does this really count every individual hit separately? + + // Get the avatar's weapon type. + WeaponType weaponType = avatar.getAvatar().getAvatarData().getWeaponType(); + + // Check if we already have probability data for this avatar. If not, insert it. + if (!this.avatarNormalProbabilities.containsKey(avatar)) { + this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability()); + } + + // Roll for energy. + int currentProbability = this.avatarNormalProbabilities.getInt(avatar); + int roll = ThreadLocalRandom.current().nextInt(0, 100); + + // If the player wins the roll, we increase the avatar's energy and reset the probability. + if (roll < currentProbability) { + avatar.addEnergy(1.0f, PropChangeReason.PROP_CHANGE_REASON_ABILITY, true); + this.avatarNormalProbabilities.put(avatar, weaponType.getEnergyGainInitialProbability()); + } + // Otherwise, we increase the probability for the next hit. + else { + this.avatarNormalProbabilities.put( + avatar, currentProbability + weaponType.getEnergyGainIncreaseProbability()); + } + } + + public void handleAttackHit(EvtBeingHitInfo hitInfo) { + // Get the attack result. + AttackResult attackRes = hitInfo.getAttackResult(); + + // Make sure the attack was performed by the currently active avatar. If not, we ignore the hit. + Optional attackerEntity = + this.getCastingAvatarEntityForEnergy(attackRes.getAttackerId()); + if (attackerEntity.isEmpty() + || this.player.getTeamManager().getCurrentAvatarEntity().getId() + != attackerEntity.get().getId()) { + return; + } + + // Make sure the target is an actual enemy. + GameEntity targetEntity = this.player.getScene().getEntityById(attackRes.getDefenseId()); + if (!(targetEntity instanceof EntityMonster targetMonster)) { + return; + } + + MonsterType targetType = targetMonster.getMonsterData().getType(); + if (targetType != MonsterType.MONSTER_ORDINARY && targetType != MonsterType.MONSTER_BOSS) { + return; + } + + // Get the ability that caused this hit. + AbilityIdentifier ability = attackRes.getAbilityIdentifier(); + + // Make sure there is no actual "ability" associated with the hit. For now, this is how we + // identify normal and charged attacks. Note that this is not completely accurate: + // - Many character's charged attacks have an ability associated with them. This means that, + // for now, we don't identify charged attacks reliably. + // - There might also be some cases where we incorrectly identify something as a normal or + // charged attack that is not (Diluc's E?). + // - Catalyst normal attacks have an ability, so we don't handle those for now. + // ToDo: Fix all of that. + if (ability != AbilityIdentifier.getDefaultInstance()) { + return; + } + + // Handle the energy generation. + this.generateEnergyForNormalAndCharged(attackerEntity.get()); + } + + /* + * Energy logic related to using skills. + */ + + private void handleBurstCast(Avatar avatar, int skillId) { + // Don't do anything if energy usage is disabled. + if (!GAME_OPTIONS.energyUsage || !this.energyUsage) { + return; + } + + // If the cast skill was a burst, consume energy. + if (avatar.getSkillDepot() != null && skillId == avatar.getSkillDepot().getEnergySkill()) { + avatar.getAsEntity().clearEnergy(ChangeEnergyReason.CHANGE_ENERGY_REASON_SKILL_START); + } + } + + public void handleEvtDoSkillSuccNotify(GameSession session, int skillId, int casterId) { + // Determine the entity that has cast the skill. Cancel if we can't find that avatar. + Optional caster = + this.player.getTeamManager().getActiveTeam().stream() + .filter(character -> character.getId() == casterId) + .findFirst(); + + if (caster.isEmpty()) { + return; + } + + Avatar avatar = caster.get().getAvatar(); + + // Handle elemental burst. + this.handleBurstCast(avatar, skillId); + } + + /* + * Monster energy drops. + */ + + private void generateElemBallDrops(EntityMonster monster, int dropId) { + // Generate all drops specified for the given drop id. + if (!energyDropData.containsKey(dropId)) { + Grasscutter.getLogger().warn("No drop data for dropId {} found.", dropId); + return; + } + + for (EnergyDropInfo info : energyDropData.get(dropId)) { + this.generateElemBall(info.getBallId(), monster.getPosition(), info.getCount()); + } + } + + public void handleMonsterEnergyDrop( + EntityMonster monster, float hpBeforeDamage, float hpAfterDamage) { + // Make sure this is actually a monster. + // Note that some wildlife also has that type, like boars or birds. + MonsterType type = monster.getMonsterData().getType(); + if (type != MonsterType.MONSTER_ORDINARY && type != MonsterType.MONSTER_BOSS) { + return; + } + + // Calculate the HP thresholds for before and after the damage was taken. + float maxHp = monster.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP); + float thresholdBefore = hpBeforeDamage / maxHp; + float thresholdAfter = hpAfterDamage / maxHp; + + // Determine the thresholds the monster has passed, and generate drops based on that. + for (HpDrops drop : monster.getMonsterData().getHpDrops()) { + if (drop.getDropId() == 0) { + continue; + } + + float threshold = drop.getHpPercent() / 100.0f; + if (threshold < thresholdBefore && threshold >= thresholdAfter) { + this.generateElemBallDrops(monster, drop.getDropId()); + } + } + + // Handle kill drops. + if (hpAfterDamage <= 0 && monster.getMonsterData().getKillDropId() != 0) { + this.generateElemBallDrops(monster, monster.getMonsterData().getKillDropId()); + } + } + + /* + * Utilities. + */ + + private void generateElemBall(int ballId, Position position, int count) { + // Generate a particle/orb with the specified parameters. + ItemData itemData = GameData.getItemDataMap().get(ballId); + if (itemData == null) { + return; + } + + EntityItem energyBall = + new EntityItem(this.getPlayer().getScene(), this.getPlayer(), itemData, position, count); + this.getPlayer().getScene().addEntity(energyBall); + } + + private Optional getCastingAvatarEntityForEnergy(int invokeEntityId) { + // To determine the avatar that has cast the skill that caused the energy particle to be + // generated, + // we have to look at the entity that has invoked the ability. This can either be that avatar + // directly, + // or it can be an `EntityClientGadget`, owned (some way up the owner hierarchy) by the avatar + // that cast the skill. + + // Try to get the invoking entity from the scene. + GameEntity entity = this.player.getScene().getEntityById(invokeEntityId); + + // Determine the ID of the entity that originally cast this skill. If the scene entity is null, + // or not an `EntityClientGadget`, we assume that we are directly looking at the casting avatar + // (the null case will happen if the avatar was switched out between casting the skill and the + // particle being generated). If the scene entity is an `EntityClientGadget`, we need to find + // the + // ID of the original owner of that gadget. + int avatarEntityId = + (!(entity instanceof EntityClientGadget)) + ? invokeEntityId + : ((EntityClientGadget) entity).getOriginalOwnerEntityId(); + + // Finally, find the avatar entity in the player's team. + return this.player.getTeamManager().getActiveTeam().stream() + .filter(character -> character.getId() == avatarEntityId) + .findFirst(); + } + + /** + * Refills the energy of the active avatar. + * + * @return True if the energy was refilled, false otherwise. + */ + public boolean refillActiveEnergy() { + var activeEntity = this.player.getTeamManager().getCurrentAvatarEntity(); + return activeEntity.addEnergy( + activeEntity.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal()); + } + + /** + * Refills the energy of the entire team. + * + * @param changeReason The reason for the energy change. + * @param isFlat Whether the energy should be added as a flat value. + */ + public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) { + for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) { + // giving the exact amount read off the AvatarSkillData.json + entityAvatar.addEnergy( + entityAvatar.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal(), + changeReason, + isFlat); + } + } + + public void setEnergyUsage(boolean energyUsage) { + this.energyUsage = energyUsage; + if (!energyUsage) { // Refill team energy if usage is disabled + this.refillTeamEnergy(PropChangeReason.PROP_CHANGE_REASON_GM, true); + } + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/stamina/StaminaManager.java b/src/main/java/emu/grasscutter/game/managers/stamina/StaminaManager.java index 4e39cd582..d1b367b52 100644 --- a/src/main/java/emu/grasscutter/game/managers/stamina/StaminaManager.java +++ b/src/main/java/emu/grasscutter/game/managers/stamina/StaminaManager.java @@ -141,6 +141,7 @@ public class StaminaManager extends BasePlayerManager { put(242301, 0.8f); put(542301, 0.8f); }}; + private final Logger logger = Grasscutter.getLogger(); private final HashMap beforeUpdateStaminaListeners = new HashMap<>(); private final HashMap afterUpdateStaminaListeners = new HashMap<>(); @@ -414,13 +415,7 @@ public class StaminaManager extends BasePlayerManager { // Internal handler private void handleImmediateStamina(GameSession session, @NotNull MotionState motionState) { - if (currentState == motionState) { - if (motionState.equals(MotionState.MOTION_STATE_CLIMB_JUMP)) { - updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_JUMP), true); - } - return; - } - + if (currentState == motionState) return; switch (motionState) { case MOTION_STATE_CLIMB -> updateStaminaRelative(session, new Consumption(ConsumptionType.CLIMB_START), true); @@ -440,6 +435,73 @@ public class StaminaManager extends BasePlayerManager { updateStaminaRelative(session, consumption, true); } + private class SustainedStaminaHandler extends TimerTask { + public void run() { + boolean moving = isPlayerMoving(); + int currentCharacterStamina = getCurrentCharacterStamina(); + int maxCharacterStamina = getMaxCharacterStamina(); + int currentVehicleStamina = getCurrentVehicleStamina(); + int maxVehicleStamina = getMaxVehicleStamina(); + if (moving || (currentCharacterStamina < maxCharacterStamina) || (currentVehicleStamina < maxVehicleStamina)) { + logger.trace("Player moving: " + moving + ", stamina full: " + + (currentCharacterStamina >= maxCharacterStamina) + ", recalculate stamina"); + boolean isCharacterStamina = true; + Consumption consumption; + if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { + consumption = getClimbConsumption(); + } else if (MotionStatesCategorized.get("DASH").contains(currentState)) { + consumption = getDashConsumption(); + } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { + consumption = getFlyConsumption(); + } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { + consumption = new Consumption(ConsumptionType.RUN); + } else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) { + consumption = getSkiffConsumption(); + isCharacterStamina = false; + } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { + consumption = new Consumption(ConsumptionType.STANDBY); + } else if (MotionStatesCategorized.get("SWIM").contains(currentState)) { + consumption = getSwimConsumptions(); + } else if (MotionStatesCategorized.get("WALK").contains(currentState)) { + consumption = new Consumption(ConsumptionType.WALK); + } else if (MotionStatesCategorized.get("NOCOST_NORECOVER").contains(currentState)) { + consumption = new Consumption(); + } else if (MotionStatesCategorized.get("OTHER").contains(currentState)) { + consumption = getOtherConsumptions(); + } else { // ignore + return; + } + + if (consumption.amount < 0 && isCharacterStamina) { + // Do not apply reduction factor when recovering stamina + if (player.getTeamManager().getTeamResonances().contains(10301)) { + consumption.amount *= 0.85f; + } + } + // Delay 1 seconds before starts recovering stamina + if (consumption.amount != 0 && cachedSession != null) { + if (consumption.amount < 0) { + staminaRecoverDelay = 0; + } + if (consumption.amount > 0 + && consumption.type != ConsumptionType.POWERED_FLY + && consumption.type != ConsumptionType.POWERED_SKIFF) { + // For POWERED_* recover immediately - things like Amber's gliding exam and skiff challenges may require this. + if (staminaRecoverDelay < 5) { + // For others recover after 1 seconds (5 ticks) - as official server does. + staminaRecoverDelay++; + consumption.amount = 0; + logger.trace("Delaying recovery: " + staminaRecoverDelay); + } + } + updateStaminaRelative(cachedSession, consumption, isCharacterStamina); + } + } + previousState = currentState; + previousCoordinates = currentCoordinates.clone(); + } + } + private void handleDrowning() { // TODO: fix drowning waverider entity int stamina = getCurrentCharacterStamina(); @@ -452,6 +514,10 @@ public class StaminaManager extends BasePlayerManager { } } + // Consumption Calculators + + // Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina + private Consumption getFightConsumption(int skillCasting) { // Talent moving if (TalentMovements.contains(skillCasting)) { @@ -471,10 +537,6 @@ public class StaminaManager extends BasePlayerManager { }; } - // Consumption Calculators - - // Stamina Consumption Reduction: https://genshin-impact.fandom.com/wiki/Stamina - private Consumption getClimbConsumption() { Consumption consumption = new Consumption(); if (currentState == MotionState.MOTION_STATE_CLIMB && isPlayerMoving()) { @@ -552,6 +614,8 @@ public class StaminaManager extends BasePlayerManager { return new Consumption(); } + // Reduction getter + private float getTalentCostReductionFactor(HashMap talentReductionMap) { // All known talents reductions are not stackable float reduction = 1; @@ -568,8 +632,6 @@ public class StaminaManager extends BasePlayerManager { return reduction; } - // Reduction getter - private float getFoodCostReductionFactor(HashMap foodReductionMap) { // All known food reductions are not stackable // TODO: Check consumed food (buff?) and return proper factor @@ -633,76 +695,11 @@ public class StaminaManager extends BasePlayerManager { private Consumption getSwordCost(int skillId) { Consumption consumption = new Consumption(ConsumptionType.FIGHT, -2000); // Character specific handling - if (skillId == 10421) { - consumption.amount = -2500; + switch (skillId) { + case 10421: + consumption.amount = -2500; + break; } return consumption; } - - private class SustainedStaminaHandler extends TimerTask { - public void run() { - boolean moving = isPlayerMoving(); - int currentCharacterStamina = getCurrentCharacterStamina(); - int maxCharacterStamina = getMaxCharacterStamina(); - int currentVehicleStamina = getCurrentVehicleStamina(); - int maxVehicleStamina = getMaxVehicleStamina(); - if (moving || (currentCharacterStamina < maxCharacterStamina) || (currentVehicleStamina < maxVehicleStamina)) { - logger.trace("Player moving: " + moving + ", stamina full: " + - (currentCharacterStamina >= maxCharacterStamina) + ", recalculate stamina"); - boolean isCharacterStamina = true; - Consumption consumption; - if (MotionStatesCategorized.get("CLIMB").contains(currentState)) { - consumption = getClimbConsumption(); - } else if (MotionStatesCategorized.get("DASH").contains(currentState)) { - consumption = getDashConsumption(); - } else if (MotionStatesCategorized.get("FLY").contains(currentState)) { - consumption = getFlyConsumption(); - } else if (MotionStatesCategorized.get("RUN").contains(currentState)) { - consumption = new Consumption(ConsumptionType.RUN); - } else if (MotionStatesCategorized.get("SKIFF").contains(currentState)) { - consumption = getSkiffConsumption(); - isCharacterStamina = false; - } else if (MotionStatesCategorized.get("STANDBY").contains(currentState)) { - consumption = new Consumption(ConsumptionType.STANDBY); - } else if (MotionStatesCategorized.get("SWIM").contains(currentState)) { - consumption = getSwimConsumptions(); - } else if (MotionStatesCategorized.get("WALK").contains(currentState)) { - consumption = new Consumption(ConsumptionType.WALK); - } else if (MotionStatesCategorized.get("NOCOST_NORECOVER").contains(currentState)) { - consumption = new Consumption(); - } else if (MotionStatesCategorized.get("OTHER").contains(currentState)) { - consumption = getOtherConsumptions(); - } else { // ignore - return; - } - - if (consumption.amount < 0 && isCharacterStamina) { - // Do not apply reduction factor when recovering stamina - if (player.getTeamManager().getTeamResonances().contains(10301)) { - consumption.amount *= 0.85f; - } - } - // Delay 1 seconds before starts recovering stamina - if (consumption.amount != 0 && cachedSession != null) { - if (consumption.amount < 0) { - staminaRecoverDelay = 0; - } - if (consumption.amount > 0 - && consumption.type != ConsumptionType.POWERED_FLY - && consumption.type != ConsumptionType.POWERED_SKIFF) { - // For POWERED_* recover immediately - things like Amber's gliding exam and skiff challenges may require this. - if (staminaRecoverDelay < 5) { - // For others recover after 1 seconds (5 ticks) - as official server does. - staminaRecoverDelay++; - consumption.amount = 0; - logger.trace("Delaying recovery: " + staminaRecoverDelay); - } - } - updateStaminaRelative(cachedSession, consumption, isCharacterStamina); - } - } - previousState = currentState; - previousCoordinates = currentCoordinates.clone(); - } - } } diff --git a/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java b/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java index 13a8630ca..40e550f43 100644 --- a/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java +++ b/src/main/java/emu/grasscutter/game/player/PlayerProgressManager.java @@ -1,276 +1,303 @@ -package emu.grasscutter.game.player; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.ScenePointEntry; -import emu.grasscutter.data.excels.OpenStateData; -import emu.grasscutter.data.excels.OpenStateData.OpenStateCondType; -import emu.grasscutter.game.props.ActionReason; -import emu.grasscutter.game.quest.enums.QuestCond; -import emu.grasscutter.game.quest.enums.QuestContent; -import emu.grasscutter.game.quest.enums.QuestState; -import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; -import emu.grasscutter.server.packet.send.*; -import java.util.Set; -import java.util.stream.Collectors; - -// @Entity -public final class PlayerProgressManager extends BasePlayerDataManager { - /****************************************************************************************************************** - ****************************************************************************************************************** - * OPEN STATES - ****************************************************************************************************************** - *****************************************************************************************************************/ - - // Set of open states that are never unlocked, whether they fulfill the conditions or not. - public static final Set BLACKLIST_OPEN_STATES = - Set.of( - 48 // blacklist OPEN_STATE_LIMIT_REGION_GLOBAL to make Meledy happy. =D Remove this as - // soon as quest unlocks are fully implemented. - ); - // Set of open states that are set per default for all accounts. Can be overwritten by an entry in - // `map`. - public static final Set DEFAULT_OPEN_STATES = - GameData.getOpenStateList().stream() - .filter( - s -> - s.isDefaultState() // Actual default-opened states. - // All states whose unlock we don't handle correctly yet. - || (s.getCond().stream() - .filter( - c -> - c.getCondType() - == OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL) - .count() - == 0) - // Always unlock OPEN_STATE_PAIMON, otherwise the player will not have a - // working chat. - || s.getId() == 1) - .filter( - s -> - !BLACKLIST_OPEN_STATES.contains(s.getId())) // Filter out states in the blacklist. - .map(s -> s.getId()) - .collect(Collectors.toSet()); - - public PlayerProgressManager(Player player) { - super(player); - } - - /********** - * Handler for player login. - **********/ - public void onPlayerLogin() { - // Try unlocking open states on player login. This handles accounts where unlock conditions were - // already met before certain open state unlocks were implemented. - this.tryUnlockOpenStates(false); - - // Send notify to the client. - player.getSession().send(new PacketOpenStateUpdateNotify(this.player)); - - // Add statue quests if necessary. - this.addStatueQuestsOnLogin(); - - // Auto-unlock the first statue and map area, until we figure out how to make - // that particular statue interactable. - this.player.getUnlockedScenePoints(3).add(7); - this.player.getUnlockedSceneAreas(3).add(1); - } - - /********** - * Direct getters and setters for open states. - **********/ - public int getOpenState(int openState) { - return this.player.getOpenStates().getOrDefault(openState, 0); - } - - private void setOpenState(int openState, int value, boolean sendNotify) { - int previousValue = this.player.getOpenStates().getOrDefault(openState, 0); - - if (value != previousValue) { - this.player.getOpenStates().put(openState, value); - - if (sendNotify) { - player.getSession().send(new PacketOpenStateChangeNotify(openState, value)); - } - } - } - - private void setOpenState(int openState, int value) { - this.setOpenState(openState, value, true); - } - - /********** - * Condition checking for setting open states. - **********/ - private boolean areConditionsMet(OpenStateData openState) { - // Check all conditions and test if at least one of them is violated. - for (var condition : openState.getCond()) { - // For level conditions, check if the player has reached the necessary level. - if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL) { - if (this.player.getLevel() < condition.getParam()) { - return false; - } - } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_QUEST) { - // ToDo: Implement. - } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_COND_PARENT_QUEST) { - // ToDo: Implement. - } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_OFFERING_LEVEL) { - // ToDo: Implement. - } else if (condition.getCondType() == OpenStateCondType.OPEN_STATE_CITY_REPUTATION_LEVEL) { - // ToDo: Implement. - } - } - - // Done. If we didn't find any violations, all conditions are met. - return true; - } - - /********** - * Setting open states from the client (via `SetOpenStateReq`). - **********/ - public void setOpenStateFromClient(int openState, int value) { - // Get the data for this open state. - OpenStateData data = GameData.getOpenStateDataMap().get(openState); - if (data == null) { - this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL)); - return; - } - - // Make sure that this is an open state that the client is allowed to set, - // and that it doesn't have any further conditions attached. - if (!data.isAllowClientOpen() || !this.areConditionsMet(data)) { - this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL)); - return; - } - - // Set. - this.setOpenState(openState, value); - this.player.sendPacket(new PacketSetOpenStateRsp(openState, value)); - } - - /** This force sets an open state, ignoring all conditions and permissions */ - public void forceSetOpenState(int openState, int value) { - this.setOpenState(openState, value); - } - - /********** - * Triggered unlocking of open states (unlock states whose conditions have been met.) - **********/ - public void tryUnlockOpenStates(boolean sendNotify) { - // Get list of open states that are not yet unlocked. - var lockedStates = - GameData.getOpenStateList().stream() - .filter(s -> this.player.getOpenStates().getOrDefault(s, 0) == 0) - .toList(); - - // Try unlocking all of them. - for (var state : lockedStates) { - // To auto-unlock a state, it has to meet three conditions: - // * it can not be a state that is unlocked by the client, - // * it has to meet all its unlock conditions, and - // * it can not be in the blacklist. - if (!state.isAllowClientOpen() - && this.areConditionsMet(state) - && !BLACKLIST_OPEN_STATES.contains(state.getId())) { - this.setOpenState(state.getId(), 1, sendNotify); - } - } - } - - public void tryUnlockOpenStates() { - this.tryUnlockOpenStates(true); - } - - /****************************************************************************************************************** - ****************************************************************************************************************** - * MAP AREAS AND POINTS - ****************************************************************************************************************** - *****************************************************************************************************************/ - private void addStatueQuestsOnLogin() { - // Get all currently existing subquests for the "unlock all statues" main quest. - var statueMainQuest = GameData.getMainQuestDataMap().get(303); - var statueSubQuests = statueMainQuest.getSubQuests(); - - // Add the main statue quest if it isn't active yet. - var statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303); - if (statueGameMainQuest == null) { - this.player.getQuestManager().addQuest(30302); - statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303); - } - - // Set all subquests to active if they aren't already finished. - for (var subData : statueSubQuests) { - var subGameQuest = statueGameMainQuest.getChildQuestById(subData.getSubId()); - if (subGameQuest != null && subGameQuest.getState() == QuestState.QUEST_STATE_UNSTARTED) { - this.player.getQuestManager().addQuest(subData.getSubId()); - } - } - } - - public boolean unlockTransPoint(int sceneId, int pointId, boolean isStatue) { - // Check whether the unlocked point exists and whether it is still locked. - ScenePointEntry scenePointEntry = GameData.getScenePointEntryById(sceneId, pointId); - - if (scenePointEntry == null || this.player.getUnlockedScenePoints(sceneId).contains(pointId)) { - return false; - } - - // Add the point to the list of unlocked points for its scene. - this.player.getUnlockedScenePoints(sceneId).add(pointId); - - // Give primogems and Adventure EXP for unlocking. - this.player.getInventory().addItem(201, 5, ActionReason.UnlockPointReward); - this.player.getInventory().addItem(102, isStatue ? 50 : 10, ActionReason.UnlockPointReward); - - // this.player.sendPacket(new - // PacketPlayerPropChangeReasonNotify(this.player.getProperty(PlayerProperty.PROP_PLAYER_EXP), - // PlayerProperty.PROP_PLAYER_EXP, PropChangeReason.PROP_CHANGE_REASON_PLAYER_ADD_EXP)); - - // Fire quest trigger for trans point unlock. - this.player - .getQuestManager() - .queueEvent(QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT, sceneId, pointId); - - // Send packet. - this.player.sendPacket(new PacketScenePointUnlockNotify(sceneId, pointId)); - return true; - } - - public void unlockSceneArea(int sceneId, int areaId) { - // Add the area to the list of unlocked areas in its scene. - this.player.getUnlockedSceneAreas(sceneId).add(areaId); - - // Send packet. - this.player.sendPacket(new PacketSceneAreaUnlockNotify(sceneId, areaId)); - } - - /** Give replace costume to player (Amber, Jean, Mona, Rosaria) */ - public void addReplaceCostumes() { - var currentPlayerCostumes = player.getCostumeList(); - GameData.getAvatarReplaceCostumeDataMap() - .keySet() - .forEach( - costumeId -> { - if (GameData.getAvatarCostumeDataMap().get(costumeId) == null - || currentPlayerCostumes.contains(costumeId)) { - return; - } - this.player.addCostume(costumeId); - }); - } - - /** Quest progress */ - public void addQuestProgress(int id, int count) { - var newCount = player.getPlayerProgress().addToCurrentProgress(id, count); - player.save(); - player - .getQuestManager() - .queueEvent(QuestContent.QUEST_CONTENT_ADD_QUEST_PROGRESS, id, newCount); - } - - /** Item history */ - public void addItemObtainedHistory(int id, int count) { - var newCount = player.getPlayerProgress().addToItemHistory(id, count); - player.save(); - player.getQuestManager().queueEvent(QuestCond.QUEST_COND_HISTORY_GOT_ANY_ITEM, id, newCount); - } -} +package emu.grasscutter.game.player; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.ScenePointEntry; +import emu.grasscutter.data.excels.OpenStateData; +import emu.grasscutter.data.excels.OpenStateData.OpenStateCondType; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.quest.enums.ParentQuestState; +import emu.grasscutter.game.quest.enums.QuestCond; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.game.quest.enums.QuestState; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.packet.send.*; +import java.util.Set; +import java.util.stream.Collectors; + +import static emu.grasscutter.scripts.constants.EventType.EVENT_UNLOCK_TRANS_POINT; + +// @Entity +public final class PlayerProgressManager extends BasePlayerDataManager { + /****************************************************************************************************************** + ****************************************************************************************************************** + * OPEN STATES + ****************************************************************************************************************** + *****************************************************************************************************************/ + + // Set of open states that are never unlocked, whether they fulfill the conditions or not. + public static final Set BLACKLIST_OPEN_STATES = + Set.of( + 48 // blacklist OPEN_STATE_LIMIT_REGION_GLOBAL to make Meledy happy. =D Remove this as + // soon as quest unlocks are fully implemented. + ); + // Set of open states that are set per default for all accounts. Can be overwritten by an entry in + // `map`. + public static final Set DEFAULT_OPEN_STATES = + GameData.getOpenStateList().stream() + .filter( + s -> + s.isDefaultState() && !s.isAllowClientOpen() // Actual default-opened states. + || ((s.getCond().size() == 1) + && (s.getCond().get(0).getCondType() + == OpenStateCondType.OPEN_STATE_COND_PLAYER_LEVEL) + && (s.getCond().get(0).getParam() == 1)) + // All states whose unlock we don't handle correctly yet. + || (s.getCond().stream() + .anyMatch( + c -> + c.getCondType() == OpenStateCondType.OPEN_STATE_OFFERING_LEVEL + || c.getCondType() + == OpenStateCondType.OPEN_STATE_CITY_REPUTATION_LEVEL)) + // Always unlock OPEN_STATE_PAIMON, otherwise the player will not have a + // working chat. + || s.getId() == 1) + .filter( + s -> + !BLACKLIST_OPEN_STATES.contains(s.getId())) // Filter out states in the blacklist. + .map(OpenStateData::getId) + .collect(Collectors.toSet()); + + public PlayerProgressManager(Player player) { + super(player); + } + + /********** + * Handler for player login. + **********/ + public void onPlayerLogin() { + // Try unlocking open states on player login. This handles accounts where unlock conditions were + // already met before certain open state unlocks were implemented. + this.tryUnlockOpenStates(false); + + // Send notify to the client. + player.getSession().send(new PacketOpenStateUpdateNotify(this.player)); + + // Add statue quests if necessary. + this.addStatueQuestsOnLogin(); + + // Auto-unlock the first statue and map area, until we figure out how to make + // that particular statue interactable. + this.player.getUnlockedScenePoints(3).add(7); + this.player.getUnlockedSceneAreas(3).add(1); + } + + /********** + * Direct getters and setters for open states. + **********/ + public int getOpenState(int openState) { + return this.player.getOpenStates().getOrDefault(openState, 0); + } + + private void setOpenState(int openState, int value, boolean sendNotify) { + int previousValue = this.player.getOpenStates().getOrDefault(openState, 0); + + if (value != previousValue) { + this.player.getOpenStates().put(openState, value); + + this.player + .getQuestManager() + .queueEvent(QuestCond.QUEST_COND_OPEN_STATE_EQUAL, openState, value); + + if (sendNotify) { + player.getSession().send(new PacketOpenStateChangeNotify(openState, value)); + } + } + } + + private void setOpenState(int openState, int value) { + this.setOpenState(openState, value, true); + } + + /********** + * Condition checking for setting open states. + **********/ + private boolean areConditionsMet(OpenStateData openState) { + // Check all conditions and test if at least one of them is violated. + for (var condition : openState.getCond()) { + switch (condition.getCondType()) { + // For level conditions, check if the player has reached the necessary level. + case OPEN_STATE_COND_PLAYER_LEVEL -> { + if (this.player.getLevel() < condition.getParam()) { + return false; + } + } + case OPEN_STATE_COND_QUEST -> { + // check sub quest id for quest finished met requirements + var quest = this.player.getQuestManager().getQuestById(condition.getParam()); + if (quest == null || quest.getState() != QuestState.QUEST_STATE_FINISHED) { + return false; + } + } + case OPEN_STATE_COND_PARENT_QUEST -> { + // check main quest id for quest finished met requirements + // TODO not sure if its having or finished quest + var mainQuest = this.player.getQuestManager().getMainQuestById(condition.getParam()); + if (mainQuest == null + || mainQuest.getState() != ParentQuestState.PARENT_QUEST_STATE_FINISHED) { + return false; + } + } + // ToDo: Implement. + case OPEN_STATE_OFFERING_LEVEL, OPEN_STATE_CITY_REPUTATION_LEVEL -> {} + } + } + + // Done. If we didn't find any violations, all conditions are met. + return true; + } + + /********** + * Setting open states from the client (via `SetOpenStateReq`). + **********/ + public void setOpenStateFromClient(int openState, int value) { + // Get the data for this open state. + OpenStateData data = GameData.getOpenStateDataMap().get(openState); + if (data == null) { + this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL)); + return; + } + + // Make sure that this is an open state that the client is allowed to set, + // and that it doesn't have any further conditions attached. + if (!data.isAllowClientOpen() || !this.areConditionsMet(data)) { + this.player.sendPacket(new PacketSetOpenStateRsp(Retcode.RET_FAIL)); + return; + } + + // Set. + this.setOpenState(openState, value); + this.player.sendPacket(new PacketSetOpenStateRsp(openState, value)); + } + + /** This force sets an open state, ignoring all conditions and permissions */ + public void forceSetOpenState(int openState, int value) { + this.setOpenState(openState, value); + } + + /********** + * Triggered unlocking of open states (unlock states whose conditions have been met.) + **********/ + public void tryUnlockOpenStates(boolean sendNotify) { + // Get list of open states that are not yet unlocked. + var lockedStates = + GameData.getOpenStateList().stream() + .filter(s -> this.player.getOpenStates().getOrDefault(s, 0) == 0) + .toList(); + + // Try unlocking all of them. + for (var state : lockedStates) { + // To auto-unlock a state, it has to meet three conditions: + // * it can not be a state that is unlocked by the client, + // * it has to meet all its unlock conditions, and + // * it can not be in the blacklist. + if (!state.isAllowClientOpen() + && this.areConditionsMet(state) + && !BLACKLIST_OPEN_STATES.contains(state.getId())) { + this.setOpenState(state.getId(), 1, sendNotify); + } + } + } + + public void tryUnlockOpenStates() { + this.tryUnlockOpenStates(true); + } + + /****************************************************************************************************************** + ****************************************************************************************************************** + * MAP AREAS AND POINTS + ****************************************************************************************************************** + *****************************************************************************************************************/ + private void addStatueQuestsOnLogin() { + // Get all currently existing subquests for the "unlock all statues" main quest. + var statueMainQuest = GameData.getMainQuestDataMap().get(303); + var statueSubQuests = statueMainQuest.getSubQuests(); + + // Add the main statue quest if it isn't active yet. + var statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303); + if (statueGameMainQuest == null) { + this.player.getQuestManager().addQuest(30302); + statueGameMainQuest = this.player.getQuestManager().getMainQuestById(303); + } + + // Set all subquests to active if they aren't already finished. + for (var subData : statueSubQuests) { + var subGameQuest = statueGameMainQuest.getChildQuestById(subData.getSubId()); + if (subGameQuest != null && subGameQuest.getState() == QuestState.QUEST_STATE_UNSTARTED) { + this.player.getQuestManager().addQuest(subData.getSubId()); + } + } + } + + public boolean unlockTransPoint(int sceneId, int pointId, boolean isStatue) { + // Check whether the unlocked point exists and whether it is still locked. + ScenePointEntry scenePointEntry = GameData.getScenePointEntryById(sceneId, pointId); + + if (scenePointEntry == null || this.player.getUnlockedScenePoints(sceneId).contains(pointId)) { + return false; + } + + // Add the point to the list of unlocked points for its scene. + this.player.getUnlockedScenePoints(sceneId).add(pointId); + + // Give primogems and Adventure EXP for unlocking. + this.player.getInventory().addItem(201, 5, ActionReason.UnlockPointReward); + this.player.getInventory().addItem(102, isStatue ? 50 : 10, ActionReason.UnlockPointReward); + + // this.player.sendPacket(new + // PacketPlayerPropChangeReasonNotify(this.player.getProperty(PlayerProperty.PROP_PLAYER_EXP), + // PlayerProperty.PROP_PLAYER_EXP, PropChangeReason.PROP_CHANGE_REASON_PLAYER_ADD_EXP)); + + // Fire quest trigger for trans point unlock. + this.player + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT, sceneId, pointId); + this.player + .getScene() + .getScriptManager() + .callEvent(new ScriptArgs(0, EVENT_UNLOCK_TRANS_POINT, sceneId, pointId)); + + // Send packet. + this.player.sendPacket(new PacketScenePointUnlockNotify(sceneId, pointId)); + return true; + } + + public void unlockSceneArea(int sceneId, int areaId) { + // Add the area to the list of unlocked areas in its scene. + this.player.getUnlockedSceneAreas(sceneId).add(areaId); + + // Send packet. + this.player.sendPacket(new PacketSceneAreaUnlockNotify(sceneId, areaId)); + } + + /** Give replace costume to player (Amber, Jean, Mona, Rosaria) */ + public void addReplaceCostumes() { + var currentPlayerCostumes = player.getCostumeList(); + GameData.getAvatarReplaceCostumeDataMap() + .keySet() + .forEach( + costumeId -> { + if (GameData.getAvatarCostumeDataMap().get(costumeId) == null + || currentPlayerCostumes.contains(costumeId)) { + return; + } + this.player.addCostume(costumeId); + }); + } + + /** Quest progress */ + public void addQuestProgress(int id, int count) { + var newCount = player.getPlayerProgress().addToCurrentProgress(id, count); + player.save(); + player + .getQuestManager() + .queueEvent(QuestContent.QUEST_CONTENT_ADD_QUEST_PROGRESS, id, newCount); + } + + /** Item history */ + public void addItemObtainedHistory(int id, int count) { + var newCount = player.getPlayerProgress().addToItemHistory(id, count); + player.save(); + player.getQuestManager().queueEvent(QuestCond.QUEST_COND_HISTORY_GOT_ANY_ITEM, id, newCount); + } +} diff --git a/src/main/java/emu/grasscutter/game/player/TeamManager.java b/src/main/java/emu/grasscutter/game/player/TeamManager.java index 728503fcd..7a05c815b 100644 --- a/src/main/java/emu/grasscutter/game/player/TeamManager.java +++ b/src/main/java/emu/grasscutter/game/player/TeamManager.java @@ -1,1082 +1,1084 @@ -package emu.grasscutter.game.player; - -import static emu.grasscutter.config.Configuration.GAME_OPTIONS; - -import dev.morphia.annotations.Entity; -import dev.morphia.annotations.Transient; -import emu.grasscutter.GameConstants; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.binout.config.fields.ConfigAbilityData; -import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.game.entity.EntityBaseGadget; -import emu.grasscutter.game.props.ElementType; -import emu.grasscutter.game.props.EnterReason; -import emu.grasscutter.game.props.FightProperty; -import emu.grasscutter.game.world.Scene; -import emu.grasscutter.game.world.World; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType; -import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; -import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; -import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; -import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGrantRecord.GrantReason; -import emu.grasscutter.net.proto.VisionTypeOuterClass; -import emu.grasscutter.server.event.player.PlayerTeamDeathEvent; -import emu.grasscutter.server.packet.send.*; -import emu.grasscutter.utils.Position; -import emu.grasscutter.utils.Utils; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.ints.IntSet; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import java.util.*; -import java.util.stream.Stream; -import lombok.Getter; -import lombok.Setter; -import lombok.val; - -@Entity -public final class TeamManager extends BasePlayerDataManager { - @Transient private final List avatars; - @Transient @Getter private final Set gadgets; - @Transient @Getter private final IntSet teamResonances; - @Transient @Getter private final IntSet teamResonancesConfig; - // This needs to be a LinkedHashMap to guarantee insertion order. - @Getter private LinkedHashMap teams; - private int currentTeamIndex; - @Getter @Setter private int currentCharacterIndex; - @Transient @Getter @Setter private TeamInfo mpTeam; - @Transient @Getter @Setter private int entityId; - - @Transient private int useTemporarilyTeamIndex = -1; - @Transient private List temporaryTeam; // Temporary Team for tower - @Transient @Getter @Setter private boolean usingTrialTeam; - @Transient @Getter @Setter private TeamInfo trialAvatarTeam; - // hold trial avatars for later use in rebuilding active team - @Transient @Getter @Setter private Map trialAvatars; - - @Transient @Getter @Setter - private int previousIndex = -1; // index of character selection in team before adding trial avatar - - public TeamManager() { - this.mpTeam = new TeamInfo(); - this.avatars = new ArrayList<>(); - this.gadgets = new HashSet<>(); - this.teamResonances = new IntOpenHashSet(); - this.teamResonancesConfig = new IntOpenHashSet(); - } - - public TeamManager(Player player) { - this(); - this.setPlayer(player); - - this.teams = new LinkedHashMap<>(); - this.currentTeamIndex = 1; - for (int i = 1; i <= GameConstants.DEFAULT_TEAMS; i++) { - this.teams.put(i, new TeamInfo()); - } - } - - public World getWorld() { - return this.getPlayer().getWorld(); - } - - /** - * Search through all teams and if the team matches, return that index. Otherwise, return -1. No - * match could mean that the team does not currently belong to the player. - */ - public int getTeamId(TeamInfo team) { - for (int i = 1; i <= this.teams.size(); i++) { - if (this.teams.get(i).equals(team)) { - return i; - } - } - return -1; - } - - public int getCurrentTeamId() { - // Starts from 1 - return currentTeamIndex; - } - - private void setCurrentTeamId(int currentTeamIndex) { - this.currentTeamIndex = currentTeamIndex; - } - - public long getCurrentCharacterGuid() { - return this.getCurrentAvatarEntity().getAvatar().getGuid(); - } - - public TeamInfo getCurrentTeamInfo() { - if (useTemporarilyTeamIndex >= 0 && useTemporarilyTeamIndex < temporaryTeam.size()) { - return temporaryTeam.get(useTemporarilyTeamIndex); - } - if (this.getPlayer().isInMultiplayer()) { - return this.getMpTeam(); - } - return this.getTeams().get(this.currentTeamIndex); - } - - public TeamInfo getCurrentSinglePlayerTeamInfo() { - return this.getTeams().get(this.currentTeamIndex); - } - - public List getActiveTeam() { - return avatars; - } - - public EntityAvatar getCurrentAvatarEntity() { - return this.getActiveTeam().get(currentCharacterIndex); - } - - public boolean isSpawned() { - return this.getPlayer().getScene() != null - && this.getPlayer() - .getScene() - .getEntities() - .containsKey(this.getCurrentAvatarEntity().getId()); - } - - public int getMaxTeamSize() { - if (this.getPlayer().isInMultiplayer()) { - int max = GAME_OPTIONS.avatarLimits.multiplayerTeam; - if (this.getPlayer().getWorld().getHost() == this.getPlayer()) { - return Math.max(1, (int) Math.ceil(max / (double) this.getWorld().getPlayerCount())); - } - return Math.max(1, (int) Math.floor(max / (double) this.getWorld().getPlayerCount())); - } - - return GAME_OPTIONS.avatarLimits.singlePlayerTeam; - } - - // Methods - - /** Returns true if there is space to add the number of avatars to the team. */ - public boolean canAddAvatarsToTeam(TeamInfo team, int avatars) { - return team.size() + avatars <= this.getMaxTeamSize(); - } - - /** Returns true if there is space to add to the team. */ - public boolean canAddAvatarToTeam(TeamInfo team) { - return this.canAddAvatarsToTeam(team, 1); - } - - /** - * Returns true if there is space to add the number of avatars to the current team. If the current - * team is temporary, returns false. - */ - public boolean canAddAvatarsToCurrentTeam(int avatars) { - if (this.useTemporarilyTeamIndex != -1) { - return false; - } - return this.canAddAvatarsToTeam(this.getCurrentTeamInfo(), avatars); - } - - /** - * Returns true if there is space to add to the current team. If the current team is temporary, - * returns false. - */ - public boolean canAddAvatarToCurrentTeam() { - return this.canAddAvatarsToCurrentTeam(1); - } - - /** - * Try to add the collection of avatars to the team. Returns true if all were successfully added. - * If some can not be added, returns false and does not add any. - */ - public boolean addAvatarsToTeam(TeamInfo team, Collection avatars) { - if (!this.canAddAvatarsToTeam(team, avatars.size())) { - return false; - } - - // Convert avatars into a collection of avatar IDs, then add - team.getAvatars().addAll(avatars.stream().map(a -> a.getAvatarId()).toList()); - - // Update team - if (this.getPlayer().isInMultiplayer()) { - if (team.equals(this.getMpTeam())) { - // MP team Packet - this.updateTeamEntities(new PacketChangeMpTeamAvatarRsp(this.getPlayer(), team)); - } - } else { - // SP team update packet - this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(this.getPlayer())); - - int teamId = this.getTeamId(team); - if (teamId != -1) { - // This is one of the player's teams - // Update entites - if (teamId == this.getCurrentTeamId()) { - this.updateTeamEntities(new PacketSetUpAvatarTeamRsp(this.getPlayer(), teamId, team)); - } else { - this.getPlayer().sendPacket(new PacketSetUpAvatarTeamRsp(this.getPlayer(), teamId, team)); - } - } - } - - return true; - } - - /** Try to add an avatar to a team. Returns true if successful. */ - public boolean addAvatarToTeam(TeamInfo team, Avatar avatar) { - return this.addAvatarsToTeam(team, Collections.singleton(avatar)); - } - - /** - * Try to add the collection of avatars to the current team. Will not modify a temporary team. - * Returns true if all were successfully added. If some can not be added, returns false and does - * not add any. - */ - public boolean addAvatarsToCurrentTeam(Collection avatars) { - if (this.useTemporarilyTeamIndex != -1) { - return false; - } - return this.addAvatarsToTeam(this.getCurrentTeamInfo(), avatars); - } - - /** - * Try to add an avatar to the current team. Will not modify a temporary team. Returns true if - * successful. - */ - public boolean addAvatarToCurrentTeam(Avatar avatar) { - return this.addAvatarsToCurrentTeam(Collections.singleton(avatar)); - } - - private void updateTeamResonances() { - this.getTeamResonances().clear(); - this.getTeamResonancesConfig().clear(); - // Official resonances require a full party - if (this.avatars.size() < 4) return; - - // TODO: make this actually read from TeamResonanceExcelConfigData.json for the real resonances - // and conditions - // Currently we just hardcode these conditions, but this won't work for modded resources or - // future changes - var elementCounts = new Object2IntOpenHashMap(); - this.getActiveTeam().stream() - .map(EntityAvatar::getAvatar) - .filter(Objects::nonNull) - .map(Avatar::getSkillDepot) - .filter(Objects::nonNull) - .map(AvatarSkillDepotData::getElementType) - .filter(Objects::nonNull) - .forEach(elementType -> elementCounts.addTo(elementType, 1)); - - // Dual element resonances - elementCounts.object2IntEntrySet().stream() - .filter(e -> e.getIntValue() >= 2) - .map(e -> e.getKey()) - .filter(elementType -> elementType.getTeamResonanceId() != 0) - .forEach( - elementType -> { - this.teamResonances.add(elementType.getTeamResonanceId()); - this.teamResonancesConfig.add(elementType.getConfigHash()); - }); - - // Four element resonance - if (elementCounts.size() >= 4) { - this.teamResonances.add(ElementType.Default.getTeamResonanceId()); - this.teamResonancesConfig.add(ElementType.Default.getConfigHash()); - } - } - - /** Updates all properties of the active team. */ - public void updateTeamProperties() { - this.updateTeamResonances(); // Update team resonances. - this.getPlayer() - .sendPacket(new PacketSceneTeamUpdateNotify(this.getPlayer())); // Notify the player. - - // Skill charges packet - Yes, this is official server behavior as of 2.6.0 - this.getActiveTeam().stream() - .map(EntityAvatar::getAvatar) - .forEach(Avatar::sendSkillExtraChargeMap); - } - - public void updateTeamEntities(BasePacket responsePacket) { - // Sanity check - Should never happen - if (this.getCurrentTeamInfo().getAvatars().size() <= 0) { - return; - } - - // If current team has changed - var currentEntity = this.getCurrentAvatarEntity(); - var existingAvatars = new Int2ObjectOpenHashMap(); - var prevSelectedAvatarIndex = -1; - - for (EntityAvatar entity : this.getActiveTeam()) { - existingAvatars.put(entity.getAvatar().getAvatarId(), entity); - } - - // Clear active team entity list - this.getActiveTeam().clear(); - - // Add back entities into team - for (int i = 0; i < this.getCurrentTeamInfo().getAvatars().size(); i++) { - var avatarId = (int) this.getCurrentTeamInfo().getAvatars().get(i); - EntityAvatar entity; - if (existingAvatars.containsKey(avatarId)) { - entity = existingAvatars.get(avatarId); - existingAvatars.remove(avatarId); - if (entity == currentEntity) { - prevSelectedAvatarIndex = i; - } - } else { - entity = - new EntityAvatar( - this.getPlayer().getScene(), this.getPlayer().getAvatars().getAvatarById(avatarId)); - } - - this.getActiveTeam().add(entity); - } - - // Unload removed entities - for (var entity : existingAvatars.values()) { - this.getPlayer().getScene().removeEntity(entity); - entity.getAvatar().save(); - } - - // Set new selected character index - if (prevSelectedAvatarIndex == -1) { - // Previous selected avatar is not in the same spot, we will select the current one in the - // prev slot - prevSelectedAvatarIndex = - Math.min(this.currentCharacterIndex, this.getActiveTeam().size() - 1); - } - this.currentCharacterIndex = prevSelectedAvatarIndex; - - // Update properties. - // Notify player. - this.updateTeamProperties(); - - // Send response packet. - if (responsePacket != null) { - this.getPlayer().sendPacket(responsePacket); - } - - // Check if character changed - if (currentEntity != this.getCurrentAvatarEntity()) { - // Remove and Add - this.getPlayer().getScene().replaceEntity(currentEntity, this.getCurrentAvatarEntity()); - } - } - - public synchronized void setupAvatarTeam(int teamId, List list) { - // Sanity checks - if (list.size() == 0 - || list.size() > this.getMaxTeamSize() - || this.getPlayer().isInMultiplayer()) { - return; - } - - // Get team - TeamInfo teamInfo = this.getTeams().get(teamId); - if (teamInfo == null) { - return; - } - - // Set team data - LinkedHashSet newTeam = new LinkedHashSet<>(); - for (Long aLong : list) { - Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); - if (avatar == null || newTeam.contains(avatar)) { - // Should never happen - return; - } - newTeam.add(avatar); - } - - // Clear current team info and add avatars from our new team - teamInfo.getAvatars().clear(); - this.addAvatarsToTeam(teamInfo, newTeam); - } - - public void setupMpTeam(List list) { - // Sanity checks - if (list.size() == 0 - || list.size() > this.getMaxTeamSize() - || !this.getPlayer().isInMultiplayer()) { - return; - } - - TeamInfo teamInfo = this.getMpTeam(); - - // Set team data - LinkedHashSet newTeam = new LinkedHashSet<>(); - for (Long aLong : list) { - Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); - if (avatar == null || newTeam.contains(avatar)) { - // Should never happen - return; - } - newTeam.add(avatar); - } - - // Clear current team info and add avatars from our new team - teamInfo.getAvatars().clear(); - this.addAvatarsToTeam(teamInfo, newTeam); - } - - /** - * Setup avatars for a trial avatar team. - * - * @param save Should the original team be saved? - */ - public void setupTrialAvatars(boolean save) { - this.setPreviousIndex(this.getCurrentCharacterIndex()); - - if (save) { - var originalTeam = getCurrentTeamInfo(); - this.getTrialAvatarTeam().copyFrom(originalTeam); - } else this.getActiveTeam().clear(); - - this.usingTrialTeam = true; - } - - /** Displays the trial avatars. Picks the last avatar in the team. */ - public void trialAvatarTeamPostUpdate() { - this.trialAvatarTeamPostUpdate(this.getActiveTeam().size() - 1); - } - - /** - * Displays the trial avatars. - * - * @param newCharacterIndex The avatar to equip. - */ - public void trialAvatarTeamPostUpdate(int newCharacterIndex) { - this.setCurrentCharacterIndex(Math.min(newCharacterIndex, this.getActiveTeam().size() - 1)); - - this.updateTeamProperties(); - this.getPlayer().getScene().addEntity(this.getCurrentAvatarEntity()); - } - - /** - * Adds an avatar to the trial team. - * - * @param trialAvatar The avatar to add. - */ - public void addAvatarToTrialTeam(Avatar trialAvatar) { - // Remove the existing team's avatars. - this.getActiveTeam() - .forEach( - x -> - this.getPlayer() - .getScene() - .removeEntity(x, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE)); - // Remove the existing avatar from the teams if it exists. - this.getActiveTeam().removeIf(x -> x.getAvatar().getAvatarId() == trialAvatar.getAvatarId()); - this.getCurrentTeamInfo().getAvatars().removeIf(x -> x == trialAvatar.getAvatarId()); - // Add the avatar to the teams. - this.getActiveTeam().add(new EntityAvatar(this.getPlayer().getScene(), trialAvatar)); - this.getCurrentTeamInfo().addAvatar(trialAvatar); - this.getTrialAvatars().put(trialAvatar.getAvatarId(), trialAvatar); - } - - /** - * Get the GUID of a trial avatar. - * - * @param avatarId The avatar ID. - * @return The GUID of the avatar. - */ - public long getTrialAvatarGuid(int avatarId) { - return getTrialAvatars().values().stream() - .filter(avatar -> avatar.getTrialAvatarId() == avatarId) - .map(avatar -> avatar.getGuid()) - .findFirst() - .orElse(0L); - } - - /** Rollback changes from using a trial avatar team. */ - public void unsetTrialAvatarTeam() { - this.trialAvatarTeamPostUpdate(this.getPreviousIndex()); - this.setPreviousIndex(-1); - } - - /** Removes all avatars from the trial avatar team. */ - public void removeTrialAvatarTeam() { - this.removeTrialAvatarTeam( - this.getActiveTeam().stream().map(avatar -> avatar.getAvatar().getAvatarId()).toList()); - } - - /** - * Removes one avatar from the trial avatar team. - * - * @param avatarId The avatar ID to remove. - */ - public void removeTrialAvatarTeam(int avatarId) { - this.removeTrialAvatarTeam(List.of(avatarId)); - } - - /** - * Removes a collection of avatars from the trial avatar team. - * - * @param avatarIds The avatar IDs to remove. - */ - public void removeTrialAvatarTeam(List avatarIds) { - var player = this.getPlayer(); - - // Disable the trial team. - this.usingTrialTeam = false; - this.trialAvatarTeam = new TeamInfo(); - - // Remove the avatars from the team. - avatarIds.forEach( - avatarId -> { - this.getActiveTeam() - .forEach( - x -> - player - .getScene() - .removeEntity(x, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE)); - this.getActiveTeam().removeIf(x -> x.getAvatar().getTrialAvatarId() == avatarId); - this.getTrialAvatars().values().removeIf(x -> x.getTrialAvatarId() == avatarId); - }); - - // Re-add the avatars to the team. - var index = 0; - for (var avatar : this.getCurrentTeamInfo().getAvatars()) { - if (this.getActiveTeam().stream() - .map(entity -> entity.getAvatar().getAvatarId()) - .toList() - .contains(avatar)) return; - - this.getActiveTeam() - .add( - index++, - new EntityAvatar(player.getScene(), player.getAvatars().getAvatarById(avatar))); - } - - this.unsetTrialAvatarTeam(); - } - - public void setupTemporaryTeam(List> guidList) { - this.temporaryTeam = - guidList.stream() - .map( - list -> { - // Sanity checks - if (list.size() == 0 || list.size() > this.getMaxTeamSize()) { - return null; - } - - // Set team data - LinkedHashSet newTeam = new LinkedHashSet<>(); - for (Long aLong : list) { - Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); - if (avatar == null || newTeam.contains(avatar)) { - // Should never happen - return null; - } - newTeam.add(avatar); - } - - // convert to avatar ids - return newTeam.stream().map(Avatar::getAvatarId).toList(); - }) - .filter(Objects::nonNull) - .map(TeamInfo::new) - .toList(); - } - - public void useTemporaryTeam(int index) { - this.useTemporarilyTeamIndex = index; - this.updateTeamEntities(null); - } - - public void cleanTemporaryTeam() { - // check if using temporary team - if (useTemporarilyTeamIndex < 0) { - return; - } - - this.useTemporarilyTeamIndex = -1; - this.temporaryTeam = null; - this.updateTeamEntities(null); - } - - public synchronized void setCurrentTeam(int teamId) { - // - if (this.getPlayer().isInMultiplayer()) { - return; - } - - // Get team - TeamInfo teamInfo = this.getTeams().get(teamId); - if (teamInfo == null || teamInfo.getAvatars().size() == 0) { - return; - } - - // Set - this.setCurrentTeamId(teamId); - this.updateTeamEntities(new PacketChooseCurAvatarTeamRsp(teamId)); - } - - public synchronized void setTeamName(int teamId, String teamName) { - // Get team - TeamInfo teamInfo = this.getTeams().get(teamId); - if (teamInfo == null) { - return; - } - - teamInfo.setName(teamName); - - // Packet - this.getPlayer().sendPacket(new PacketChangeTeamNameRsp(teamId, teamName)); - } - - public synchronized void changeAvatar(long guid) { - EntityAvatar oldEntity = this.getCurrentAvatarEntity(); - - if (guid == oldEntity.getAvatar().getGuid()) { - return; - } - - EntityAvatar newEntity = null; - int index = -1; - for (int i = 0; i < this.getActiveTeam().size(); i++) { - if (guid == this.getActiveTeam().get(i).getAvatar().getGuid()) { - index = i; - newEntity = this.getActiveTeam().get(i); - } - } - - if (index < 0 || newEntity == oldEntity) { - return; - } - - // Set index - this.setCurrentCharacterIndex(index); - - // Old entity motion state - oldEntity.setMotionState(MotionState.MOTION_STATE_STANDBY); - - // Remove and Add - this.getPlayer().getScene().replaceEntity(oldEntity, newEntity); - this.getPlayer().sendPacket(new PacketChangeAvatarRsp(guid)); - } - - /** - * Applies 10% of the avatar's max HP as damage. This occurs when the avatar is killed by the - * void. - */ - public void applyVoidDamage() { - this.getActiveTeam() - .forEach( - entity -> { - entity.damage(entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * .1f); - player.sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); - }); - } - - public void onAvatarDie(long dieGuid) { - EntityAvatar deadAvatar = this.getCurrentAvatarEntity(); - - if (deadAvatar.isAlive() || deadAvatar.getId() != dieGuid) { - return; - } - - PlayerDieType dieType = deadAvatar.getKilledType(); - int killedBy = deadAvatar.getKilledBy(); - - if (dieType == PlayerDieType.PLAYER_DIE_TYPE_DRAWN) { - // Died in water. Do not replace - // The official server has skipped this notify and will just respawn the team immediately - // after the animation. - // TODO: Perhaps find a way to get vanilla experience? - this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); - } else { - // Replacement avatar - EntityAvatar replacement = null; - int replaceIndex = -1; - - for (int i = 0; i < this.getActiveTeam().size(); i++) { - EntityAvatar entity = this.getActiveTeam().get(i); - if (entity.isAlive()) { - replaceIndex = i; - replacement = entity; - break; - } - } - - if (replacement == null) { - // No more living team members... - this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); - // Invoke player team death event. - PlayerTeamDeathEvent event = - new PlayerTeamDeathEvent( - this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex())); - event.call(); - } else { - // Set index and spawn replacement member - this.setCurrentCharacterIndex(replaceIndex); - this.getPlayer().getScene().addEntity(replacement); - } - } - - // Response packet - this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0)); - } - - public boolean reviveAvatar(Avatar avatar) { - for (EntityAvatar entity : this.getActiveTeam()) { - if (entity.getAvatar() == avatar) { - if (entity.isAlive()) { - return false; - } - - entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f); - // Satiation is reset when reviving an avatar - player.getSatiationManager().removeSatiationDirectly(entity.getAvatar(), 15000); - this.getPlayer() - .sendPacket( - new PacketAvatarFightPropUpdateNotify( - entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); - this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); - return true; - } - } - - return false; - } - - public boolean healAvatar(Avatar avatar, int healRate, int healAmount) { - for (EntityAvatar entity : this.getActiveTeam()) { - if (entity.getAvatar() == avatar) { - if (!entity.isAlive()) { - return false; - } - - entity.setFightProperty( - FightProperty.FIGHT_PROP_CUR_HP, - (float) - Math.min( - (entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) - + entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) - * (float) healRate - / 100.0 - + (float) healAmount / 100.0), - entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP))); - this.getPlayer() - .sendPacket( - new PacketAvatarFightPropUpdateNotify( - entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); - this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); - return true; - } - } - return false; - } - - public void respawnTeam() { - // Make sure all team members are dead - // Drowning needs revive when there may be other team members still alive. - // for (EntityAvatar entity : getActiveTeam()) { - // if (entity.isAlive()) { - // return; - // } - // } - player - .getStaminaManager() - .stopSustainedStaminaHandler(); // prevent drowning immediately after respawn - - // Revive all team members - for (EntityAvatar entity : this.getActiveTeam()) { - entity.setFightProperty( - FightProperty.FIGHT_PROP_CUR_HP, - entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * .4f); - player.getSatiationManager().removeSatiationDirectly(entity.getAvatar(), 15000); - this.getPlayer() - .sendPacket( - new PacketAvatarFightPropUpdateNotify( - entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); - this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); - } - - // Teleport player and set player position - try { - this.getPlayer() - .sendPacket( - new PacketPlayerEnterSceneNotify( - this.getPlayer(), - EnterType.ENTER_TYPE_SELF, - EnterReason.Revival, - player.getSceneId(), - getRespawnPosition())); - player.getPosition().set(getRespawnPosition()); - } catch (Exception e) { - this.getPlayer() - .sendPacket( - new PacketPlayerEnterSceneNotify( - this.getPlayer(), - EnterType.ENTER_TYPE_SELF, - EnterReason.Revival, - 3, - GameConstants.START_POSITION)); - player - .getPosition() - .set(GameConstants.START_POSITION); // If something goes wrong, the resurrection is here - } - - // Packets - this.getPlayer().sendPacket(new BasePacket(PacketOpcodes.WorldPlayerReviveRsp)); - } - - public Position getRespawnPosition() { - var deathPos = this.getPlayer().getPosition(); - int sceneId = this.getPlayer().getSceneId(); - - // Get the closest trans point to where the player died. - var respawnPoint = - this.getPlayer().getUnlockedScenePoints(sceneId).stream() - .map(pointId -> GameData.getScenePointEntryById(sceneId, pointId)) - .filter(point -> point.getPointData().getType().equals("SceneTransPoint")) - .min( - (Comparator.comparingDouble( - pos -> Utils.getDist(pos.getPointData().getTranPos(), deathPos)))); - - return respawnPoint.get().getPointData().getTranPos(); - } - - public void saveAvatars() { - // Save all avatars from active team - for (EntityAvatar entity : this.getActiveTeam()) { - entity.getAvatar().save(); - } - } - - public void onPlayerLogin() { // Hack for now to fix resonances on login - this.updateTeamResonances(); - } - - public synchronized void addNewCustomTeam() { - // Sanity check - max number of teams. - if (this.teams.size() == GameConstants.MAX_TEAMS) { - player.sendPacket(new PacketAddBackupAvatarTeamRsp(Retcode.RET_FAIL)); - return; - } - - // The id of the new custom team is the lowest id in [5,MAX_TEAMS] that is not yet taken. - int id = -1; - for (int i = 5; i <= GameConstants.MAX_TEAMS; i++) { - if (!this.teams.containsKey(i)) { - id = i; - break; - } - } - - // Create the new team. - this.teams.put(id, new TeamInfo()); - - // Send packets. - player.sendPacket(new PacketAvatarTeamAllDataNotify(player)); - player.sendPacket(new PacketAddBackupAvatarTeamRsp()); - } - - public synchronized void removeCustomTeam(int id) { - // Check if the target id exists. - if (!this.teams.containsKey(id)) { - player.sendPacket(new PacketDelBackupAvatarTeamRsp(Retcode.RET_FAIL, id)); - } - - // Remove team. - this.teams.remove(id); - - // Send packets. - player.sendPacket(new PacketAvatarTeamAllDataNotify(player)); - player.sendPacket(new PacketDelBackupAvatarTeamRsp(id)); - } - - /** - * Applies abilities for the currently selected team. These abilities are sourced from the scene. - * - * @param scene The scene with the abilities to apply. - */ - public void applyAbilities(Scene scene) { - try { - var levelEntityConfig = scene.getSceneData().getLevelEntityConfig(); - var config = GameData.getConfigLevelEntityDataMap().get(levelEntityConfig); - if (config == null) return; - - var avatars = this.getPlayer().getAvatars(); - var avatarIds = scene.getSceneData().getSpecifiedAvatarList(); - var specifiedAvatarList = this.getActiveTeam(); - - if (avatarIds != null && avatarIds.size() > 0) { - // certain scene could limit specific avatars' entry - specifiedAvatarList.clear(); - for (int id : avatarIds) { - var avatar = avatars.getAvatarById(id); - if (avatar == null) continue; - - specifiedAvatarList.add(new EntityAvatar(scene, avatar)); - } - } - - for (var entityAvatar : specifiedAvatarList) { - var avatarData = entityAvatar.getAvatar().getAvatarData(); - if (avatarData == null) { - continue; - } - - avatarData.buildEmbryo(); // Create avatar abilities. - if (config.getAvatarAbilities() == null) { - continue; // continue and not break because has to rebuild ability for the next avatar if - // any - } - - for (ConfigAbilityData abilities : config.getAvatarAbilities()) { - avatarData.getAbilities().add(Utils.abilityHash(abilities.getAbilityName())); - } - } - } catch (Exception e) { - Grasscutter.getLogger() - .error( - "Error applying level entity config for scene {}", scene.getSceneData().getId(), e); - } - } - - public List getTrialAvatarParam(int trialAvatarId) { - if (GameData.getTrialAvatarCustomData() - .isEmpty()) { // use default data if custom data not available - if (GameData.getTrialAvatarDataMap().get(trialAvatarId) == null) return List.of(); - - return GameData.getTrialAvatarDataMap().get(trialAvatarId).getTrialAvatarParamList(); - } - // use custom data - if (GameData.getTrialAvatarCustomData().get(trialAvatarId) == null) return List.of(); - - val trialCustomParams = - GameData.getTrialAvatarCustomData().get(trialAvatarId).getTrialAvatarParamList(); - return trialCustomParams.isEmpty() - ? List.of() - : Stream.of(trialCustomParams.get(0).split(";")).map(Integer::parseInt).toList(); - } - - /** - * Adds a trial avatar to the player's team. - * - * @param avatarId The ID of the avatar. - * @param questMainId The quest ID associated with the quest. - * @param reason The reason for granting the avatar. - * @return True if the avatar was added, false otherwise. - */ - public boolean addTrialAvatar(int avatarId, int questMainId, GrantReason reason) { - List trialAvatarBasicParam = getTrialAvatarParam(avatarId); - if (trialAvatarBasicParam.isEmpty()) return false; - - var avatar = new Avatar(trialAvatarBasicParam.get(0)); - if (avatar.getAvatarData() == null || !this.getPlayer().hasSentLoginPackets()) return false; - - avatar.setOwner(this.getPlayer()); - // Add trial weapons and relics. - avatar.setTrialAvatarInfo(trialAvatarBasicParam.get(1), avatarId, reason, questMainId); - avatar.equipTrialItems(); - // Re-calculate stats - avatar.recalcStats(); - - // Packet, mimic official server behaviour, add to player's bag but not saving to database. - this.getPlayer().sendPacket(new PacketAvatarAddNotify(avatar, false)); - // Add to avatar to the temporary trial team. - this.addAvatarToTrialTeam(avatar); - return true; - } - - /** - * Adds a trial avatar to the player's team. - * - * @param avatarId The ID of the avatar. - * @param questMainId The quest ID associated with the quest. - */ - public void addTrialAvatar(int avatarId, int questMainId) { - this.addTrialAvatars(List.of(avatarId), questMainId, true); - - // Packet, mimic official server behaviour, necessary to stop player from modifying team. - this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(this.getPlayer())); - } - - /** - * Adds a collection of trial avatars to the player's team. - * - * @param avatarIds List of trial avatar IDs. - */ - public void addTrialAvatars(List avatarIds) { - this.addTrialAvatars(avatarIds, 0, false); - } - - /** - * Adds a collection of trial avatars to the player's team. - * - * @param avatarIds List of trial avatar IDs. - * @param save Whether to retain the currently equipped avatars. - */ - public void addTrialAvatars(List avatarIds, boolean save) { - this.addTrialAvatars(avatarIds, 0, save); - } - - /** - * Adds a list of trial avatars to the player's team. - * - * @param avatarIds List of trial avatar IDs. - * @param questId The ID of the quest this trial team is associated with. - * @param save Whether to retain the currently equipped avatars. - */ - public void addTrialAvatars(List avatarIds, int questId, boolean save) { - this.setupTrialAvatars(save); // Perform initial setup. - - // Add the avatars to the team. - avatarIds.forEach( - avatarId -> { - var result = - this.addTrialAvatar( - avatarId, - questId, - questId == 0 - ? GrantReason.GRANT_REASON_BY_QUEST - : GrantReason.GRANT_REASON_BY_TRIAL_AVATAR_ACTIVITY); - - if (!result) throw new RuntimeException("Unable to add trial avatar to team."); - }); - - // Update the team. - this.trialAvatarTeamPostUpdate(questId == 0 ? getActiveTeam().size() - 1 : 0); - } - - /** Removes all trial avatars from the player's team. */ - public void removeTrialAvatar() { - this.removeTrialAvatar( - this.getActiveTeam().stream() - .map(EntityAvatar::getAvatar) - .map(Avatar::getAvatarId) - .toList()); - } - - /** - * Removes a trial avatar from the player's team. Additionally, unlocks the ability to change the - * team configuration. - * - * @param avatarId The ID of the avatar. - */ - public void removeTrialAvatar(int avatarId) { - this.removeTrialAvatar(List.of(avatarId)); - } - - /** - * Removes a collection of trial avatars from the player's team. - * - * @param avatarIds List of trial avatar IDs. - */ - public void removeTrialAvatar(List avatarIds) { - if (!this.isUsingTrialTeam()) throw new RuntimeException("Player is not using a trial team."); - - this.getPlayer() - .sendPacket( - new PacketAvatarDelNotify(avatarIds.stream().map(this::getTrialAvatarGuid).toList())); - this.removeTrialAvatarTeam(avatarIds); - - // Update the team. - if (avatarIds.size() == 1) this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify()); - } -} +package emu.grasscutter.game.player; + +import static emu.grasscutter.config.Configuration.GAME_OPTIONS; + +import dev.morphia.annotations.Entity; +import dev.morphia.annotations.Transient; +import emu.grasscutter.GameConstants; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.binout.config.fields.ConfigAbilityData; +import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.entity.EntityAvatar; +import emu.grasscutter.game.entity.EntityBaseGadget; +import emu.grasscutter.game.props.ElementType; +import emu.grasscutter.game.props.EnterReason; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.game.world.Scene; +import emu.grasscutter.game.world.World; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType; +import emu.grasscutter.net.proto.MotionStateOuterClass.MotionState; +import emu.grasscutter.net.proto.PlayerDieTypeOuterClass.PlayerDieType; +import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode; +import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGrantRecord.GrantReason; +import emu.grasscutter.net.proto.VisionTypeOuterClass; +import emu.grasscutter.server.event.player.PlayerTeamDeathEvent; +import emu.grasscutter.server.packet.send.*; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import java.util.*; +import java.util.stream.Stream; +import lombok.Getter; +import lombok.Setter; +import lombok.val; + +@Entity +public final class TeamManager extends BasePlayerDataManager { + @Transient private final List avatars; + @Transient @Getter private final Set gadgets; + @Transient @Getter private final IntSet teamResonances; + @Transient @Getter private final IntSet teamResonancesConfig; + // This needs to be a LinkedHashMap to guarantee insertion order. + @Getter private LinkedHashMap teams; + private int currentTeamIndex; + @Getter @Setter private int currentCharacterIndex; + @Transient @Getter @Setter private TeamInfo mpTeam; + @Transient @Getter @Setter private int entityId; + + @Transient private int useTemporarilyTeamIndex = -1; + @Transient private List temporaryTeam; // Temporary Team for tower + @Transient @Getter @Setter private boolean usingTrialTeam; + @Transient @Getter @Setter private TeamInfo trialAvatarTeam; + // hold trial avatars for later use in rebuilding active team + @Transient @Getter @Setter private Map trialAvatars; + + @Transient @Getter @Setter + private int previousIndex = -1; // index of character selection in team before adding trial avatar + + public TeamManager() { + this.mpTeam = new TeamInfo(); + this.avatars = new ArrayList<>(); + this.gadgets = new HashSet<>(); + this.teamResonances = new IntOpenHashSet(); + this.teamResonancesConfig = new IntOpenHashSet(); + this.trialAvatars = new HashMap<>(); + this.trialAvatarTeam = new TeamInfo(); + } + + public TeamManager(Player player) { + this(); + this.setPlayer(player); + + this.teams = new LinkedHashMap<>(); + this.currentTeamIndex = 1; + for (int i = 1; i <= GameConstants.DEFAULT_TEAMS; i++) { + this.teams.put(i, new TeamInfo()); + } + } + + public World getWorld() { + return this.getPlayer().getWorld(); + } + + /** + * Search through all teams and if the team matches, return that index. Otherwise, return -1. No + * match could mean that the team does not currently belong to the player. + */ + public int getTeamId(TeamInfo team) { + for (int i = 1; i <= this.teams.size(); i++) { + if (this.teams.get(i).equals(team)) { + return i; + } + } + return -1; + } + + public int getCurrentTeamId() { + // Starts from 1 + return currentTeamIndex; + } + + private void setCurrentTeamId(int currentTeamIndex) { + this.currentTeamIndex = currentTeamIndex; + } + + public long getCurrentCharacterGuid() { + return this.getCurrentAvatarEntity().getAvatar().getGuid(); + } + + public TeamInfo getCurrentTeamInfo() { + if (useTemporarilyTeamIndex >= 0 && useTemporarilyTeamIndex < temporaryTeam.size()) { + return temporaryTeam.get(useTemporarilyTeamIndex); + } + if (this.getPlayer().isInMultiplayer()) { + return this.getMpTeam(); + } + return this.getTeams().get(this.currentTeamIndex); + } + + public TeamInfo getCurrentSinglePlayerTeamInfo() { + return this.getTeams().get(this.currentTeamIndex); + } + + public List getActiveTeam() { + return avatars; + } + + public EntityAvatar getCurrentAvatarEntity() { + return this.getActiveTeam().get(currentCharacterIndex); + } + + public boolean isSpawned() { + return this.getPlayer().getScene() != null + && this.getPlayer() + .getScene() + .getEntities() + .containsKey(this.getCurrentAvatarEntity().getId()); + } + + public int getMaxTeamSize() { + if (this.getPlayer().isInMultiplayer()) { + int max = GAME_OPTIONS.avatarLimits.multiplayerTeam; + if (this.getPlayer().getWorld().getHost() == this.getPlayer()) { + return Math.max(1, (int) Math.ceil(max / (double) this.getWorld().getPlayerCount())); + } + return Math.max(1, (int) Math.floor(max / (double) this.getWorld().getPlayerCount())); + } + + return GAME_OPTIONS.avatarLimits.singlePlayerTeam; + } + + // Methods + + /** Returns true if there is space to add the number of avatars to the team. */ + public boolean canAddAvatarsToTeam(TeamInfo team, int avatars) { + return team.size() + avatars <= this.getMaxTeamSize(); + } + + /** Returns true if there is space to add to the team. */ + public boolean canAddAvatarToTeam(TeamInfo team) { + return this.canAddAvatarsToTeam(team, 1); + } + + /** + * Returns true if there is space to add the number of avatars to the current team. If the current + * team is temporary, returns false. + */ + public boolean canAddAvatarsToCurrentTeam(int avatars) { + if (this.useTemporarilyTeamIndex != -1) { + return false; + } + return this.canAddAvatarsToTeam(this.getCurrentTeamInfo(), avatars); + } + + /** + * Returns true if there is space to add to the current team. If the current team is temporary, + * returns false. + */ + public boolean canAddAvatarToCurrentTeam() { + return this.canAddAvatarsToCurrentTeam(1); + } + + /** + * Try to add the collection of avatars to the team. Returns true if all were successfully added. + * If some can not be added, returns false and does not add any. + */ + public boolean addAvatarsToTeam(TeamInfo team, Collection avatars) { + if (!this.canAddAvatarsToTeam(team, avatars.size())) { + return false; + } + + // Convert avatars into a collection of avatar IDs, then add + team.getAvatars().addAll(avatars.stream().map(a -> a.getAvatarId()).toList()); + + // Update team + if (this.getPlayer().isInMultiplayer()) { + if (team.equals(this.getMpTeam())) { + // MP team Packet + this.updateTeamEntities(new PacketChangeMpTeamAvatarRsp(this.getPlayer(), team)); + } + } else { + // SP team update packet + this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(this.getPlayer())); + + int teamId = this.getTeamId(team); + if (teamId != -1) { + // This is one of the player's teams + // Update entites + if (teamId == this.getCurrentTeamId()) { + this.updateTeamEntities(new PacketSetUpAvatarTeamRsp(this.getPlayer(), teamId, team)); + } else { + this.getPlayer().sendPacket(new PacketSetUpAvatarTeamRsp(this.getPlayer(), teamId, team)); + } + } + } + + return true; + } + + /** Try to add an avatar to a team. Returns true if successful. */ + public boolean addAvatarToTeam(TeamInfo team, Avatar avatar) { + return this.addAvatarsToTeam(team, Collections.singleton(avatar)); + } + + /** + * Try to add the collection of avatars to the current team. Will not modify a temporary team. + * Returns true if all were successfully added. If some can not be added, returns false and does + * not add any. + */ + public boolean addAvatarsToCurrentTeam(Collection avatars) { + if (this.useTemporarilyTeamIndex != -1) { + return false; + } + return this.addAvatarsToTeam(this.getCurrentTeamInfo(), avatars); + } + + /** + * Try to add an avatar to the current team. Will not modify a temporary team. Returns true if + * successful. + */ + public boolean addAvatarToCurrentTeam(Avatar avatar) { + return this.addAvatarsToCurrentTeam(Collections.singleton(avatar)); + } + + private void updateTeamResonances() { + this.getTeamResonances().clear(); + this.getTeamResonancesConfig().clear(); + // Official resonances require a full party + if (this.avatars.size() < 4) return; + + // TODO: make this actually read from TeamResonanceExcelConfigData.json for the real resonances + // and conditions + // Currently we just hardcode these conditions, but this won't work for modded resources or + // future changes + var elementCounts = new Object2IntOpenHashMap(); + this.getActiveTeam().stream() + .map(EntityAvatar::getAvatar) + .filter(Objects::nonNull) + .map(Avatar::getSkillDepot) + .filter(Objects::nonNull) + .map(AvatarSkillDepotData::getElementType) + .filter(Objects::nonNull) + .forEach(elementType -> elementCounts.addTo(elementType, 1)); + + // Dual element resonances + elementCounts.object2IntEntrySet().stream() + .filter(e -> e.getIntValue() >= 2) + .map(e -> e.getKey()) + .filter(elementType -> elementType.getTeamResonanceId() != 0) + .forEach( + elementType -> { + this.teamResonances.add(elementType.getTeamResonanceId()); + this.teamResonancesConfig.add(elementType.getConfigHash()); + }); + + // Four element resonance + if (elementCounts.size() >= 4) { + this.teamResonances.add(ElementType.Default.getTeamResonanceId()); + this.teamResonancesConfig.add(ElementType.Default.getConfigHash()); + } + } + + /** Updates all properties of the active team. */ + public void updateTeamProperties() { + this.updateTeamResonances(); // Update team resonances. + this.getPlayer() + .sendPacket(new PacketSceneTeamUpdateNotify(this.getPlayer())); // Notify the player. + + // Skill charges packet - Yes, this is official server behavior as of 2.6.0 + this.getActiveTeam().stream() + .map(EntityAvatar::getAvatar) + .forEach(Avatar::sendSkillExtraChargeMap); + } + + public void updateTeamEntities(BasePacket responsePacket) { + // Sanity check - Should never happen + if (this.getCurrentTeamInfo().getAvatars().size() <= 0) { + return; + } + + // If current team has changed + var currentEntity = this.getCurrentAvatarEntity(); + var existingAvatars = new Int2ObjectOpenHashMap(); + var prevSelectedAvatarIndex = -1; + + for (EntityAvatar entity : this.getActiveTeam()) { + existingAvatars.put(entity.getAvatar().getAvatarId(), entity); + } + + // Clear active team entity list + this.getActiveTeam().clear(); + + // Add back entities into team + for (int i = 0; i < this.getCurrentTeamInfo().getAvatars().size(); i++) { + var avatarId = (int) this.getCurrentTeamInfo().getAvatars().get(i); + EntityAvatar entity; + if (existingAvatars.containsKey(avatarId)) { + entity = existingAvatars.get(avatarId); + existingAvatars.remove(avatarId); + if (entity == currentEntity) { + prevSelectedAvatarIndex = i; + } + } else { + entity = + new EntityAvatar( + this.getPlayer().getScene(), this.getPlayer().getAvatars().getAvatarById(avatarId)); + } + + this.getActiveTeam().add(entity); + } + + // Unload removed entities + for (var entity : existingAvatars.values()) { + this.getPlayer().getScene().removeEntity(entity); + entity.getAvatar().save(); + } + + // Set new selected character index + if (prevSelectedAvatarIndex == -1) { + // Previous selected avatar is not in the same spot, we will select the current one in the + // prev slot + prevSelectedAvatarIndex = + Math.min(this.currentCharacterIndex, this.getActiveTeam().size() - 1); + } + this.currentCharacterIndex = prevSelectedAvatarIndex; + + // Update properties. + // Notify player. + this.updateTeamProperties(); + + // Send response packet. + if (responsePacket != null) { + this.getPlayer().sendPacket(responsePacket); + } + + // Check if character changed + if (currentEntity != this.getCurrentAvatarEntity()) { + // Remove and Add + this.getPlayer().getScene().replaceEntity(currentEntity, this.getCurrentAvatarEntity()); + } + } + + public synchronized void setupAvatarTeam(int teamId, List list) { + // Sanity checks + if (list.size() == 0 + || list.size() > this.getMaxTeamSize() + || this.getPlayer().isInMultiplayer()) { + return; + } + + // Get team + TeamInfo teamInfo = this.getTeams().get(teamId); + if (teamInfo == null) { + return; + } + + // Set team data + LinkedHashSet newTeam = new LinkedHashSet<>(); + for (Long aLong : list) { + Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); + if (avatar == null || newTeam.contains(avatar)) { + // Should never happen + return; + } + newTeam.add(avatar); + } + + // Clear current team info and add avatars from our new team + teamInfo.getAvatars().clear(); + this.addAvatarsToTeam(teamInfo, newTeam); + } + + public void setupMpTeam(List list) { + // Sanity checks + if (list.size() == 0 + || list.size() > this.getMaxTeamSize() + || !this.getPlayer().isInMultiplayer()) { + return; + } + + TeamInfo teamInfo = this.getMpTeam(); + + // Set team data + LinkedHashSet newTeam = new LinkedHashSet<>(); + for (Long aLong : list) { + Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); + if (avatar == null || newTeam.contains(avatar)) { + // Should never happen + return; + } + newTeam.add(avatar); + } + + // Clear current team info and add avatars from our new team + teamInfo.getAvatars().clear(); + this.addAvatarsToTeam(teamInfo, newTeam); + } + + /** + * Setup avatars for a trial avatar team. + * + * @param save Should the original team be saved? + */ + public void setupTrialAvatars(boolean save) { + this.setPreviousIndex(this.getCurrentCharacterIndex()); + + if (save) { + var originalTeam = getCurrentTeamInfo(); + this.getTrialAvatarTeam().copyFrom(originalTeam); + } else this.getActiveTeam().clear(); + + this.usingTrialTeam = true; + } + + /** Displays the trial avatars. Picks the last avatar in the team. */ + public void trialAvatarTeamPostUpdate() { + this.trialAvatarTeamPostUpdate(this.getActiveTeam().size() - 1); + } + + /** + * Displays the trial avatars. + * + * @param newCharacterIndex The avatar to equip. + */ + public void trialAvatarTeamPostUpdate(int newCharacterIndex) { + this.setCurrentCharacterIndex(Math.min(newCharacterIndex, this.getActiveTeam().size() - 1)); + + this.updateTeamProperties(); + this.getPlayer().getScene().addEntity(this.getCurrentAvatarEntity()); + } + + /** + * Adds an avatar to the trial team. + * + * @param trialAvatar The avatar to add. + */ + public void addAvatarToTrialTeam(Avatar trialAvatar) { + // Remove the existing team's avatars. + this.getActiveTeam() + .forEach( + x -> + this.getPlayer() + .getScene() + .removeEntity(x, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE)); + // Remove the existing avatar from the teams if it exists. + this.getActiveTeam().removeIf(x -> x.getAvatar().getAvatarId() == trialAvatar.getAvatarId()); + this.getCurrentTeamInfo().getAvatars().removeIf(x -> x == trialAvatar.getAvatarId()); + // Add the avatar to the teams. + this.getActiveTeam().add(new EntityAvatar(this.getPlayer().getScene(), trialAvatar)); + this.getCurrentTeamInfo().addAvatar(trialAvatar); + this.getTrialAvatars().put(trialAvatar.getAvatarId(), trialAvatar); + } + + /** + * Get the GUID of a trial avatar. + * + * @param avatarId The avatar ID. + * @return The GUID of the avatar. + */ + public long getTrialAvatarGuid(int avatarId) { + return getTrialAvatars().values().stream() + .filter(avatar -> avatar.getTrialAvatarId() == avatarId) + .map(avatar -> avatar.getGuid()) + .findFirst() + .orElse(0L); + } + + /** Rollback changes from using a trial avatar team. */ + public void unsetTrialAvatarTeam() { + this.trialAvatarTeamPostUpdate(this.getPreviousIndex()); + this.setPreviousIndex(-1); + } + + /** Removes all avatars from the trial avatar team. */ + public void removeTrialAvatarTeam() { + this.removeTrialAvatarTeam( + this.getActiveTeam().stream().map(avatar -> avatar.getAvatar().getAvatarId()).toList()); + } + + /** + * Removes one avatar from the trial avatar team. + * + * @param avatarId The avatar ID to remove. + */ + public void removeTrialAvatarTeam(int avatarId) { + this.removeTrialAvatarTeam(List.of(avatarId)); + } + + /** + * Removes a collection of avatars from the trial avatar team. + * + * @param avatarIds The avatar IDs to remove. + */ + public void removeTrialAvatarTeam(List avatarIds) { + var player = this.getPlayer(); + + // Disable the trial team. + this.usingTrialTeam = false; + this.trialAvatarTeam = new TeamInfo(); + + // Remove the avatars from the team. + avatarIds.forEach( + avatarId -> { + this.getActiveTeam() + .forEach( + x -> + player + .getScene() + .removeEntity(x, VisionTypeOuterClass.VisionType.VISION_TYPE_REMOVE)); + this.getActiveTeam().removeIf(x -> x.getAvatar().getTrialAvatarId() == avatarId); + this.getTrialAvatars().values().removeIf(x -> x.getTrialAvatarId() == avatarId); + }); + + // Re-add the avatars to the team. + var index = 0; + for (var avatar : this.getCurrentTeamInfo().getAvatars()) { + if (this.getActiveTeam().stream() + .map(entity -> entity.getAvatar().getAvatarId()) + .toList() + .contains(avatar)) return; + + this.getActiveTeam() + .add( + index++, + new EntityAvatar(player.getScene(), player.getAvatars().getAvatarById(avatar))); + } + + this.unsetTrialAvatarTeam(); + } + + public void setupTemporaryTeam(List> guidList) { + this.temporaryTeam = + guidList.stream() + .map( + list -> { + // Sanity checks + if (list.size() == 0 || list.size() > this.getMaxTeamSize()) { + return null; + } + + // Set team data + LinkedHashSet newTeam = new LinkedHashSet<>(); + for (Long aLong : list) { + Avatar avatar = this.getPlayer().getAvatars().getAvatarByGuid(aLong); + if (avatar == null || newTeam.contains(avatar)) { + // Should never happen + return null; + } + newTeam.add(avatar); + } + + // convert to avatar ids + return newTeam.stream().map(Avatar::getAvatarId).toList(); + }) + .filter(Objects::nonNull) + .map(TeamInfo::new) + .toList(); + } + + public void useTemporaryTeam(int index) { + this.useTemporarilyTeamIndex = index; + this.updateTeamEntities(null); + } + + public void cleanTemporaryTeam() { + // check if using temporary team + if (useTemporarilyTeamIndex < 0) { + return; + } + + this.useTemporarilyTeamIndex = -1; + this.temporaryTeam = null; + this.updateTeamEntities(null); + } + + public synchronized void setCurrentTeam(int teamId) { + // + if (this.getPlayer().isInMultiplayer()) { + return; + } + + // Get team + TeamInfo teamInfo = this.getTeams().get(teamId); + if (teamInfo == null || teamInfo.getAvatars().size() == 0) { + return; + } + + // Set + this.setCurrentTeamId(teamId); + this.updateTeamEntities(new PacketChooseCurAvatarTeamRsp(teamId)); + } + + public synchronized void setTeamName(int teamId, String teamName) { + // Get team + TeamInfo teamInfo = this.getTeams().get(teamId); + if (teamInfo == null) { + return; + } + + teamInfo.setName(teamName); + + // Packet + this.getPlayer().sendPacket(new PacketChangeTeamNameRsp(teamId, teamName)); + } + + public synchronized void changeAvatar(long guid) { + EntityAvatar oldEntity = this.getCurrentAvatarEntity(); + + if (guid == oldEntity.getAvatar().getGuid()) { + return; + } + + EntityAvatar newEntity = null; + int index = -1; + for (int i = 0; i < this.getActiveTeam().size(); i++) { + if (guid == this.getActiveTeam().get(i).getAvatar().getGuid()) { + index = i; + newEntity = this.getActiveTeam().get(i); + } + } + + if (index < 0 || newEntity == oldEntity) { + return; + } + + // Set index + this.setCurrentCharacterIndex(index); + + // Old entity motion state + oldEntity.setMotionState(MotionState.MOTION_STATE_STANDBY); + + // Remove and Add + this.getPlayer().getScene().replaceEntity(oldEntity, newEntity); + this.getPlayer().sendPacket(new PacketChangeAvatarRsp(guid)); + } + + /** + * Applies 10% of the avatar's max HP as damage. This occurs when the avatar is killed by the + * void. + */ + public void applyVoidDamage() { + this.getActiveTeam() + .forEach( + entity -> { + entity.damage(entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * .1f); + player.sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + }); + } + + public void onAvatarDie(long dieGuid) { + EntityAvatar deadAvatar = this.getCurrentAvatarEntity(); + + if (deadAvatar.isAlive() || deadAvatar.getId() != dieGuid) { + return; + } + + PlayerDieType dieType = deadAvatar.getKilledType(); + int killedBy = deadAvatar.getKilledBy(); + + if (dieType == PlayerDieType.PLAYER_DIE_TYPE_DRAWN) { + // Died in water. Do not replace + // The official server has skipped this notify and will just respawn the team immediately + // after the animation. + // TODO: Perhaps find a way to get vanilla experience? + this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); + } else { + // Replacement avatar + EntityAvatar replacement = null; + int replaceIndex = -1; + + for (int i = 0; i < this.getActiveTeam().size(); i++) { + EntityAvatar entity = this.getActiveTeam().get(i); + if (entity.isAlive()) { + replaceIndex = i; + replacement = entity; + break; + } + } + + if (replacement == null) { + // No more living team members... + this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy)); + // Invoke player team death event. + PlayerTeamDeathEvent event = + new PlayerTeamDeathEvent( + this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex())); + event.call(); + } else { + // Set index and spawn replacement member + this.setCurrentCharacterIndex(replaceIndex); + this.getPlayer().getScene().addEntity(replacement); + } + } + + // Response packet + this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0)); + } + + public boolean reviveAvatar(Avatar avatar) { + for (EntityAvatar entity : this.getActiveTeam()) { + if (entity.getAvatar() == avatar) { + if (entity.isAlive()) { + return false; + } + + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f); + // Satiation is reset when reviving an avatar + player.getSatiationManager().removeSatiationDirectly(entity.getAvatar(), 15000); + this.getPlayer() + .sendPacket( + new PacketAvatarFightPropUpdateNotify( + entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); + this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + return true; + } + } + + return false; + } + + public boolean healAvatar(Avatar avatar, int healRate, int healAmount) { + for (EntityAvatar entity : this.getActiveTeam()) { + if (entity.getAvatar() == avatar) { + if (!entity.isAlive()) { + return false; + } + + entity.setFightProperty( + FightProperty.FIGHT_PROP_CUR_HP, + (float) + Math.min( + (entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) + + entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) + * (float) healRate + / 100.0 + + (float) healAmount / 100.0), + entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP))); + this.getPlayer() + .sendPacket( + new PacketAvatarFightPropUpdateNotify( + entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); + this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + return true; + } + } + return false; + } + + public void respawnTeam() { + // Make sure all team members are dead + // Drowning needs revive when there may be other team members still alive. + // for (EntityAvatar entity : getActiveTeam()) { + // if (entity.isAlive()) { + // return; + // } + // } + player + .getStaminaManager() + .stopSustainedStaminaHandler(); // prevent drowning immediately after respawn + + // Revive all team members + for (EntityAvatar entity : this.getActiveTeam()) { + entity.setFightProperty( + FightProperty.FIGHT_PROP_CUR_HP, + entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * .4f); + player.getSatiationManager().removeSatiationDirectly(entity.getAvatar(), 15000); + this.getPlayer() + .sendPacket( + new PacketAvatarFightPropUpdateNotify( + entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP)); + this.getPlayer().sendPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar())); + } + + // Teleport player and set player position + try { + this.getPlayer() + .sendPacket( + new PacketPlayerEnterSceneNotify( + this.getPlayer(), + EnterType.ENTER_TYPE_SELF, + EnterReason.Revival, + player.getSceneId(), + getRespawnPosition())); + player.getPosition().set(getRespawnPosition()); + } catch (Exception e) { + this.getPlayer() + .sendPacket( + new PacketPlayerEnterSceneNotify( + this.getPlayer(), + EnterType.ENTER_TYPE_SELF, + EnterReason.Revival, + 3, + GameConstants.START_POSITION)); + player + .getPosition() + .set(GameConstants.START_POSITION); // If something goes wrong, the resurrection is here + } + + // Packets + this.getPlayer().sendPacket(new BasePacket(PacketOpcodes.WorldPlayerReviveRsp)); + } + + public Position getRespawnPosition() { + var deathPos = this.getPlayer().getPosition(); + int sceneId = this.getPlayer().getSceneId(); + + // Get the closest trans point to where the player died. + var respawnPoint = + this.getPlayer().getUnlockedScenePoints(sceneId).stream() + .map(pointId -> GameData.getScenePointEntryById(sceneId, pointId)) + .filter(point -> point.getPointData().getType().equals("SceneTransPoint")) + .min( + (Comparator.comparingDouble( + pos -> Utils.getDist(pos.getPointData().getTranPos(), deathPos)))); + + return respawnPoint.get().getPointData().getTranPos(); + } + + public void saveAvatars() { + // Save all avatars from active team + for (EntityAvatar entity : this.getActiveTeam()) { + entity.getAvatar().save(); + } + } + + public void onPlayerLogin() { // Hack for now to fix resonances on login + this.updateTeamResonances(); + } + + public synchronized void addNewCustomTeam() { + // Sanity check - max number of teams. + if (this.teams.size() == GameConstants.MAX_TEAMS) { + player.sendPacket(new PacketAddBackupAvatarTeamRsp(Retcode.RET_FAIL)); + return; + } + + // The id of the new custom team is the lowest id in [5,MAX_TEAMS] that is not yet taken. + int id = -1; + for (int i = 5; i <= GameConstants.MAX_TEAMS; i++) { + if (!this.teams.containsKey(i)) { + id = i; + break; + } + } + + // Create the new team. + this.teams.put(id, new TeamInfo()); + + // Send packets. + player.sendPacket(new PacketAvatarTeamAllDataNotify(player)); + player.sendPacket(new PacketAddBackupAvatarTeamRsp()); + } + + public synchronized void removeCustomTeam(int id) { + // Check if the target id exists. + if (!this.teams.containsKey(id)) { + player.sendPacket(new PacketDelBackupAvatarTeamRsp(Retcode.RET_FAIL, id)); + } + + // Remove team. + this.teams.remove(id); + + // Send packets. + player.sendPacket(new PacketAvatarTeamAllDataNotify(player)); + player.sendPacket(new PacketDelBackupAvatarTeamRsp(id)); + } + + /** + * Applies abilities for the currently selected team. These abilities are sourced from the scene. + * + * @param scene The scene with the abilities to apply. + */ + public void applyAbilities(Scene scene) { + try { + var levelEntityConfig = scene.getSceneData().getLevelEntityConfig(); + var config = GameData.getConfigLevelEntityDataMap().get(levelEntityConfig); + if (config == null) return; + + var avatars = this.getPlayer().getAvatars(); + var avatarIds = scene.getSceneData().getSpecifiedAvatarList(); + var specifiedAvatarList = this.getActiveTeam(); + + if (avatarIds != null && avatarIds.size() > 0) { + // certain scene could limit specific avatars' entry + specifiedAvatarList.clear(); + for (int id : avatarIds) { + var avatar = avatars.getAvatarById(id); + if (avatar == null) continue; + + specifiedAvatarList.add(new EntityAvatar(scene, avatar)); + } + } + + for (var entityAvatar : specifiedAvatarList) { + var avatarData = entityAvatar.getAvatar().getAvatarData(); + if (avatarData == null) { + continue; + } + + avatarData.buildEmbryo(); // Create avatar abilities. + if (config.getAvatarAbilities() == null) { + continue; // continue and not break because has to rebuild ability for the next avatar if + // any + } + + for (ConfigAbilityData abilities : config.getAvatarAbilities()) { + avatarData.getAbilities().add(Utils.abilityHash(abilities.getAbilityName())); + } + } + } catch (Exception e) { + Grasscutter.getLogger() + .error( + "Error applying level entity config for scene {}", scene.getSceneData().getId(), e); + } + } + + public List getTrialAvatarParam(int trialAvatarId) { + if (GameData.getTrialAvatarCustomData() + .isEmpty()) { // use default data if custom data not available + if (GameData.getTrialAvatarDataMap().get(trialAvatarId) == null) return List.of(); + + return GameData.getTrialAvatarDataMap().get(trialAvatarId).getTrialAvatarParamList(); + } + // use custom data + if (GameData.getTrialAvatarCustomData().get(trialAvatarId) == null) return List.of(); + + val trialCustomParams = + GameData.getTrialAvatarCustomData().get(trialAvatarId).getTrialAvatarParamList(); + return trialCustomParams.isEmpty() + ? List.of() + : Stream.of(trialCustomParams.get(0).split(";")).map(Integer::parseInt).toList(); + } + + /** + * Adds a trial avatar to the player's team. + * + * @param avatarId The ID of the avatar. + * @param questMainId The quest ID associated with the quest. + * @param reason The reason for granting the avatar. + * @return True if the avatar was added, false otherwise. + */ + public boolean addTrialAvatar(int avatarId, int questMainId, GrantReason reason) { + List trialAvatarBasicParam = getTrialAvatarParam(avatarId); + if (trialAvatarBasicParam.isEmpty()) return false; + + var avatar = new Avatar(trialAvatarBasicParam.get(0)); + if (avatar.getAvatarData() == null || !this.getPlayer().hasSentLoginPackets()) return false; + + avatar.setOwner(this.getPlayer()); + // Add trial weapons and relics. + avatar.setTrialAvatarInfo(trialAvatarBasicParam.get(1), avatarId, reason, questMainId); + avatar.equipTrialItems(); + // Re-calculate stats + avatar.recalcStats(); + + // Packet, mimic official server behaviour, add to player's bag but not saving to database. + this.getPlayer().sendPacket(new PacketAvatarAddNotify(avatar, false)); + // Add to avatar to the temporary trial team. + this.addAvatarToTrialTeam(avatar); + return true; + } + + /** + * Adds a trial avatar to the player's team. + * + * @param avatarId The ID of the avatar. + * @param questMainId The quest ID associated with the quest. + */ + public void addTrialAvatar(int avatarId, int questMainId) { + this.addTrialAvatars(List.of(avatarId), questMainId, true); + + // Packet, mimic official server behaviour, necessary to stop player from modifying team. + this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify(this.getPlayer())); + } + + /** + * Adds a collection of trial avatars to the player's team. + * + * @param avatarIds List of trial avatar IDs. + */ + public void addTrialAvatars(List avatarIds) { + this.addTrialAvatars(avatarIds, 0, false); + } + + /** + * Adds a collection of trial avatars to the player's team. + * + * @param avatarIds List of trial avatar IDs. + * @param save Whether to retain the currently equipped avatars. + */ + public void addTrialAvatars(List avatarIds, boolean save) { + this.addTrialAvatars(avatarIds, 0, save); + } + + /** + * Adds a list of trial avatars to the player's team. + * + * @param avatarIds List of trial avatar IDs. + * @param questId The ID of the quest this trial team is associated with. + * @param save Whether to retain the currently equipped avatars. + */ + public void addTrialAvatars(List avatarIds, int questId, boolean save) { + this.setupTrialAvatars(save); // Perform initial setup. + + // Add the avatars to the team. + avatarIds.forEach( + avatarId -> { + var result = + this.addTrialAvatar( + avatarId, + questId, + questId == 0 + ? GrantReason.GRANT_REASON_BY_QUEST + : GrantReason.GRANT_REASON_BY_TRIAL_AVATAR_ACTIVITY); + + if (!result) throw new RuntimeException("Unable to add trial avatar to team."); + }); + + // Update the team. + this.trialAvatarTeamPostUpdate(questId == 0 ? getActiveTeam().size() - 1 : 0); + } + + /** Removes all trial avatars from the player's team. */ + public void removeTrialAvatar() { + this.removeTrialAvatar( + this.getActiveTeam().stream() + .map(EntityAvatar::getAvatar) + .map(Avatar::getAvatarId) + .toList()); + } + + /** + * Removes a trial avatar from the player's team. Additionally, unlocks the ability to change the + * team configuration. + * + * @param avatarId The ID of the avatar. + */ + public void removeTrialAvatar(int avatarId) { + this.removeTrialAvatar(List.of(avatarId)); + } + + /** + * Removes a collection of trial avatars from the player's team. + * + * @param avatarIds List of trial avatar IDs. + */ + public void removeTrialAvatar(List avatarIds) { + if (!this.isUsingTrialTeam()) throw new RuntimeException("Player is not using a trial team."); + + this.getPlayer() + .sendPacket( + new PacketAvatarDelNotify(avatarIds.stream().map(this::getTrialAvatarGuid).toList())); + this.removeTrialAvatarTeam(avatarIds); + + // Update the team. + if (avatarIds.size() == 1) this.getPlayer().sendPacket(new PacketAvatarTeamUpdateNotify()); + } +} diff --git a/src/main/java/emu/grasscutter/game/props/ItemUseAction/ItemUseUnlockHomeModule.java b/src/main/java/emu/grasscutter/game/props/ItemUseAction/ItemUseUnlockHomeModule.java index 2cb9bae14..e601e6b29 100644 --- a/src/main/java/emu/grasscutter/game/props/ItemUseAction/ItemUseUnlockHomeModule.java +++ b/src/main/java/emu/grasscutter/game/props/ItemUseAction/ItemUseUnlockHomeModule.java @@ -1,25 +1,19 @@ -package emu.grasscutter.game.props.ItemUseAction; - -import emu.grasscutter.game.props.ItemUseOp; - -public class ItemUseUnlockHomeModule extends ItemUseInt { - public ItemUseUnlockHomeModule(String[] useParam) { - super(useParam); - } - - @Override - public ItemUseOp getItemUseOp() { - return ItemUseOp.ITEM_USE_UNLOCK_HOME_MODULE; - } - - @Override - public boolean useItem(UseItemParams params) { - return true; - } - - @Override - public boolean postUseItem(UseItemParams params) { - params.player.addRealmList(this.i); - return true; - } -} +package emu.grasscutter.game.props.ItemUseAction; + +import emu.grasscutter.game.props.ItemUseOp; + +public class ItemUseUnlockHomeModule extends ItemUseInt { + public ItemUseUnlockHomeModule(String[] useParam) { + super(useParam); + } + + @Override + public ItemUseOp getItemUseOp() { + return ItemUseOp.ITEM_USE_UNLOCK_HOME_MODULE; + } + + @Override + public boolean useItem(UseItemParams params) { + return false; + } +} diff --git a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java index 9b213107f..b547707ef 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java @@ -1,95 +1,95 @@ -package emu.grasscutter.game.shop; - -import emu.grasscutter.data.common.ItemParamData; -import emu.grasscutter.data.excels.ShopGoodsData; -import java.util.ArrayList; -import java.util.List; -import lombok.Getter; -import lombok.Setter; - -public class ShopInfo { - @Getter @Setter private int goodsId = 0; - @Getter @Setter private ItemParamData goodsItem; - @Getter @Setter private int scoin = 0; - @Getter @Setter private List costItemList; - @Getter @Setter private int boughtNum = 0; - @Getter @Setter private int buyLimit = 0; - @Getter @Setter private int beginTime = 0; - @Getter @Setter private int endTime = 1924992000; - @Getter @Setter private int minLevel = 0; - @Getter @Setter private int maxLevel = 61; - @Getter @Setter private List preGoodsIdList = new ArrayList<>(); - @Getter @Setter private int mcoin = 0; - @Getter @Setter private int hcoin = 0; - @Getter @Setter private int disableType = 0; - @Getter @Setter private int secondarySheetId = 0; - - private String refreshType; - @Setter private transient ShopRefreshType shopRefreshType; - @Getter @Setter private int shopRefreshParam; - - public ShopInfo(ShopGoodsData sgd) { - this.goodsId = sgd.getGoodsId(); - this.goodsItem = new ItemParamData(sgd.getItemId(), sgd.getItemCount()); - this.scoin = sgd.getCostScoin(); - this.mcoin = sgd.getCostMcoin(); - this.hcoin = sgd.getCostHcoin(); - this.buyLimit = sgd.getBuyLimit(); - - this.minLevel = sgd.getMinPlayerLevel(); - this.maxLevel = sgd.getMaxPlayerLevel(); - this.costItemList = - sgd.getCostItems().stream() - .filter(x -> x.getId() != 0) - .map(x -> new ItemParamData(x.getId(), x.getCount())) - .toList(); - this.secondarySheetId = sgd.getSubTabId(); - this.shopRefreshType = sgd.getRefreshType(); - this.shopRefreshParam = sgd.getRefreshParam(); - } - - public ShopRefreshType getShopRefreshType() { - if (refreshType == null) return ShopRefreshType.NONE; - return switch (refreshType) { - case "SHOP_REFRESH_DAILY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_DAILY; - case "SHOP_REFRESH_WEEKLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_WEEKLY; - case "SHOP_REFRESH_MONTHLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_MONTHLY; - default -> ShopInfo.ShopRefreshType.NONE; - }; - } - - private boolean evaluateVirtualCost(ItemParamData item) { - return switch (item.getId()) { - case 201 -> { - this.hcoin += item.getCount(); - yield true; - } - case 203 -> { - this.mcoin += item.getCount(); - yield true; - } - default -> false; - }; - } - - public void removeVirtualCosts() { - if (this.costItemList != null) this.costItemList.removeIf(item -> evaluateVirtualCost(item)); - } - - public enum ShopRefreshType { - NONE(0), - SHOP_REFRESH_DAILY(1), - SHOP_REFRESH_WEEKLY(2), - SHOP_REFRESH_MONTHLY(3); - - private final int value; - - ShopRefreshType(int value) { - this.value = value; - } - - public int value() { - return value; - } - } -} +package emu.grasscutter.game.shop; + +import emu.grasscutter.data.common.ItemParamData; +import emu.grasscutter.data.excels.ShopGoodsData; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +public class ShopInfo { + @Getter @Setter private int goodsId = 0; + @Getter @Setter private ItemParamData goodsItem; + @Getter @Setter private int scoin = 0; + @Getter @Setter private List costItemList; + @Getter @Setter private int boughtNum = 0; + @Getter @Setter private int buyLimit = 0; + @Getter @Setter private int beginTime = 0; + @Getter @Setter private int endTime = 1924992000; + @Getter @Setter private int minLevel = 0; + @Getter @Setter private int maxLevel = 61; + @Getter @Setter private List preGoodsIdList = new ArrayList<>(); + @Getter @Setter private int mcoin = 0; + @Getter @Setter private int hcoin = 0; + @Getter @Setter private int disableType = 0; + @Getter @Setter private int secondarySheetId = 0; + + private String refreshType; + @Setter private transient ShopRefreshType shopRefreshType; + @Getter @Setter private int shopRefreshParam; + + public ShopInfo(ShopGoodsData sgd) { + this.goodsId = sgd.getGoodsId(); + this.goodsItem = new ItemParamData(sgd.getItemId(), sgd.getItemCount()); + this.scoin = sgd.getCostScoin(); + this.mcoin = sgd.getCostMcoin(); + this.hcoin = sgd.getCostHcoin(); + this.buyLimit = sgd.getBuyLimit(); + + this.minLevel = sgd.getMinPlayerLevel(); + this.maxLevel = sgd.getMaxPlayerLevel(); + this.costItemList = + sgd.getCostItems().stream() + .filter(x -> x.getId() != 0) + .map(x -> new ItemParamData(x.getId(), x.getCount())) + .toList(); + this.secondarySheetId = sgd.getSubTabId(); + this.shopRefreshType = sgd.getRefreshType(); + this.shopRefreshParam = sgd.getRefreshParam(); + } + + public ShopRefreshType getShopRefreshType() { + if (refreshType == null) return ShopRefreshType.NONE; + return switch (refreshType) { + case "SHOP_REFRESH_DAILY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_DAILY; + case "SHOP_REFRESH_WEEKLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_WEEKLY; + case "SHOP_REFRESH_MONTHLY" -> ShopInfo.ShopRefreshType.SHOP_REFRESH_MONTHLY; + default -> ShopInfo.ShopRefreshType.NONE; + }; + } + + private boolean evaluateVirtualCost(ItemParamData item) { + return switch (item.getId()) { + case 201 -> { + this.hcoin += item.getCount(); + yield true; + } + case 203 -> { + this.mcoin += item.getCount(); + yield true; + } + default -> false; + }; + } + + public void removeVirtualCosts() { + if (this.costItemList != null) this.costItemList.removeIf(item -> evaluateVirtualCost(item)); + } + + public enum ShopRefreshType { + NONE(0), + SHOP_REFRESH_DAILY(1), + SHOP_REFRESH_WEEKLY(2), + SHOP_REFRESH_MONTHLY(3); + + private final int value; + + ShopRefreshType(int value) { + this.value = value; + } + + public int value() { + return value; + } + } +} diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index edb6e356e..f003da410 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -1,1158 +1,1148 @@ -package emu.grasscutter.game.world; - -import emu.grasscutter.Grasscutter; -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.GameDepot; -import emu.grasscutter.data.binout.SceneNpcBornEntry; -import emu.grasscutter.data.binout.routes.Route; -import emu.grasscutter.data.excels.*; -import emu.grasscutter.data.excels.codex.CodexAnimalData; -import emu.grasscutter.data.excels.monster.MonsterData; -import emu.grasscutter.data.excels.world.WorldLevelData; -import emu.grasscutter.game.avatar.Avatar; -import emu.grasscutter.game.dungeons.DungeonManager; -import emu.grasscutter.game.dungeons.DungeonSettleListener; -import emu.grasscutter.game.dungeons.challenge.WorldChallenge; -import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; -import emu.grasscutter.game.entity.*; -import emu.grasscutter.game.entity.gadget.GadgetWorktop; -import emu.grasscutter.game.managers.blossom.BlossomManager; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.player.TeamInfo; -import emu.grasscutter.game.props.*; -import emu.grasscutter.game.quest.QuestGroupSuite; -import emu.grasscutter.game.world.data.TeleportProperties; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; -import emu.grasscutter.net.proto.EnterTypeOuterClass; -import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass; -import emu.grasscutter.net.proto.VisionTypeOuterClass.VisionType; -import emu.grasscutter.scripts.SceneIndexManager; -import emu.grasscutter.scripts.SceneScriptManager; -import emu.grasscutter.scripts.constants.EventType; -import emu.grasscutter.scripts.data.SceneBlock; -import emu.grasscutter.scripts.data.SceneGroup; -import emu.grasscutter.scripts.data.ScriptArgs; -import emu.grasscutter.server.event.player.PlayerTeleportEvent; -import emu.grasscutter.server.packet.send.*; -import emu.grasscutter.utils.KahnsSort; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; -import lombok.Getter; -import lombok.Setter; -import lombok.val; - -public final class Scene { - @Getter private final World world; - @Getter private final SceneData sceneData; - @Getter private final List players; - @Getter private final Map entities; - @Getter private final Set spawnedEntities; - @Getter private final Set deadSpawnedEntities; - @Getter private final Set loadedBlocks; - @Getter private final Set loadedGroups; - @Getter private final BlossomManager blossomManager; - private final HashSet unlockedForces; - private final List afterLoadedCallbacks = new ArrayList<>(); - private final long startWorldTime; - @Getter @Setter DungeonManager dungeonManager; - @Getter Int2ObjectMap sceneRoutes; - private Set loadedGridBlocks; - @Getter @Setter private boolean dontDestroyWhenEmpty; - @Getter private final SceneScriptManager scriptManager; - @Getter @Setter private WorldChallenge challenge; - @Getter private List dungeonSettleListeners; - @Getter @Setter private int prevScene; // Id of the previous scene - @Getter @Setter private int prevScenePoint; - @Getter @Setter private int killedMonsterCount; - private Set npcBornEntrySet; - @Getter private boolean finishedLoading = false; - @Getter private int tickCount = 0; - @Getter private boolean isPaused = false; - - public Scene(World world, SceneData sceneData) { - this.world = world; - this.sceneData = sceneData; - this.players = new CopyOnWriteArrayList<>(); - this.entities = new ConcurrentHashMap<>(); - - this.prevScene = 3; - this.sceneRoutes = GameData.getSceneRoutes(getId()); - - this.startWorldTime = world.getWorldTime(); - - this.spawnedEntities = ConcurrentHashMap.newKeySet(); - this.deadSpawnedEntities = ConcurrentHashMap.newKeySet(); - this.loadedBlocks = ConcurrentHashMap.newKeySet(); - this.loadedGroups = ConcurrentHashMap.newKeySet(); - this.loadedGridBlocks = new HashSet<>(); - this.npcBornEntrySet = ConcurrentHashMap.newKeySet(); - this.scriptManager = new SceneScriptManager(this); - this.blossomManager = new BlossomManager(this); - this.unlockedForces = new HashSet<>(); - } - - public int getId() { - return sceneData.getId(); - } - - public SceneType getSceneType() { - return getSceneData().getSceneType(); - } - - public int getPlayerCount() { - return this.getPlayers().size(); - } - - public GameEntity getEntityById(int id) { - return this.entities.get(id); - } - - public GameEntity getEntityByConfigId(int configId) { - return this.entities.values().stream() - .filter(x -> x.getConfigId() == configId) - .findFirst() - .orElse(null); - } - - public GameEntity getEntityByConfigId(int configId, int groupId) { - return this.entities.values().stream() - .filter(x -> x.getConfigId() == configId && x.getGroupId() == groupId) - .findFirst() - .orElse(null); - } - - /** - * Sets the scene's pause state. Sends the current scene's time to all players. - * - * @param paused The new pause state. - */ - public void setPaused(boolean paused) { - if (this.isPaused != paused) { - this.isPaused = paused; - this.broadcastPacket(new PacketSceneTimeNotify(this)); - } - } - - /** - * Gets the time in seconds since the scene started. - * - * @return The time in seconds since the scene started. - */ - public int getSceneTime() { - return (int) (this.getWorld().getWorldTime() - this.startWorldTime); - } - - /** - * Gets {@link Scene#getSceneTime()} in seconds. - * - * @return The time in seconds since the scene started. - */ - public int getSceneTimeSeconds() { - return this.getSceneTime() / 1000; - } - - public void addDungeonSettleObserver(DungeonSettleListener dungeonSettleListener) { - if (dungeonSettleListeners == null) { - dungeonSettleListeners = new ArrayList<>(); - } - - dungeonSettleListeners.add(dungeonSettleListener); - } - - /** - * Triggers an event in the dungeon manager. - * - * @param conditionType The condition type to trigger. - * @param params The parameters to pass to the event. - */ - public void triggerDungeonEvent(DungeonPassConditionType conditionType, int... params) { - if (this.dungeonManager == null) return; - this.dungeonManager.triggerEvent(conditionType, params); - } - - public boolean isInScene(GameEntity entity) { - return this.entities.containsKey(entity.getId()); - } - - public synchronized void addPlayer(Player player) { - // Check if player already in - if (getPlayers().contains(player)) { - return; - } - - // Remove player from prev scene - if (player.getScene() != null) { - player.getScene().removePlayer(player); - } - - // Add - getPlayers().add(player); - player.setSceneId(this.getId()); - player.setScene(this); - - this.setupPlayerAvatars(player); - } - - public synchronized void removePlayer(Player player) { - // Remove from challenge if leaving - if (this.getChallenge() != null && this.getChallenge().inProgress()) { - player.sendPacket(new PacketDungeonChallengeFinishNotify(this.getChallenge())); - } - - // Remove player from scene - getPlayers().remove(player); - player.setScene(null); - - // Remove player avatars - this.removePlayerAvatars(player); - - // Remove player gadgets - for (EntityBaseGadget gadget : player.getTeamManager().getGadgets()) { - this.removeEntity(gadget); - } - - // Deregister scene if not in use - if (this.getPlayerCount() <= 0 && !this.dontDestroyWhenEmpty) { - this.getWorld().deregisterScene(this); - } - } - - private void setupPlayerAvatars(Player player) { - // Clear entities from old team - player.getTeamManager().getActiveTeam().clear(); - - // Add new entities for player - TeamInfo teamInfo = player.getTeamManager().getCurrentTeamInfo(); - for (int avatarId : teamInfo.getAvatars()) { - EntityAvatar entity = - new EntityAvatar(player.getScene(), player.getAvatars().getAvatarById(avatarId)); - player.getTeamManager().getActiveTeam().add(entity); - } - - // Limit character index in case its out of bounds - if (player.getTeamManager().getCurrentCharacterIndex() - >= player.getTeamManager().getActiveTeam().size() - || player.getTeamManager().getCurrentCharacterIndex() < 0) { - player - .getTeamManager() - .setCurrentCharacterIndex(player.getTeamManager().getCurrentCharacterIndex() - 1); - } - } - - private synchronized void removePlayerAvatars(Player player) { - var team = player.getTeamManager().getActiveTeam(); - // removeEntities(team, VisionType.VISION_TYPE_REMOVE); // List isn't cool apparently - // :( - team.forEach(e -> removeEntity(e, VisionType.VISION_TYPE_REMOVE)); - team.clear(); - } - - public void spawnPlayer(Player player) { - var teamManager = player.getTeamManager(); - if (this.isInScene(teamManager.getCurrentAvatarEntity())) { - return; - } - - if (teamManager.getCurrentAvatarEntity().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) - <= 0f) { - teamManager.getCurrentAvatarEntity().setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f); - } - - this.addEntity(teamManager.getCurrentAvatarEntity()); - - // Notify the client of any extra skill charges - teamManager.getActiveTeam().stream() - .map(EntityAvatar::getAvatar) - .forEach(Avatar::sendSkillExtraChargeMap); - } - - private void addEntityDirectly(GameEntity entity) { - getEntities().put(entity.getId(), entity); - entity.onCreate(); // Call entity create event - } - - public synchronized void addEntity(GameEntity entity) { - this.addEntityDirectly(entity); - this.broadcastPacket(new PacketSceneEntityAppearNotify(entity)); - } - - public synchronized void addEntityToSingleClient(Player player, GameEntity entity) { - this.addEntityDirectly(entity); - player.sendPacket(new PacketSceneEntityAppearNotify(entity)); - } - - public void addEntities(Collection entities) { - addEntities(entities, VisionType.VISION_TYPE_BORN); - } - - public synchronized void addEntities( - Collection entities, VisionType visionType) { - if (entities == null || entities.isEmpty()) { - return; - } - for (GameEntity entity : entities) { - this.addEntityDirectly(entity); - } - - this.broadcastPacket(new PacketSceneEntityAppearNotify(entities, visionType)); - } - - private GameEntity removeEntityDirectly(GameEntity entity) { - var removed = getEntities().remove(entity.getId()); - if (removed != null) { - removed.onRemoved(); // Call entity remove event - } - return removed; - } - - public void removeEntity(GameEntity entity) { - this.removeEntity(entity, VisionType.VISION_TYPE_DIE); - } - - public synchronized void removeEntity(GameEntity entity, VisionType visionType) { - GameEntity removed = this.removeEntityDirectly(entity); - if (removed != null) { - this.broadcastPacket(new PacketSceneEntityDisappearNotify(removed, visionType)); - } - } - - public synchronized void removeEntities(List entity, VisionType visionType) { - var toRemove = entity.stream().map(this::removeEntityDirectly).toList(); - if (toRemove.size() > 0) { - this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, visionType)); - } - } - - public synchronized void replaceEntity(EntityAvatar oldEntity, EntityAvatar newEntity) { - this.removeEntityDirectly(oldEntity); - this.addEntityDirectly(newEntity); - this.broadcastPacket( - new PacketSceneEntityDisappearNotify(oldEntity, VisionType.VISION_TYPE_REPLACE)); - this.broadcastPacket( - new PacketSceneEntityAppearNotify( - newEntity, VisionType.VISION_TYPE_REPLACE, oldEntity.getId())); - } - - public void showOtherEntities(Player player) { - GameEntity currentEntity = player.getTeamManager().getCurrentAvatarEntity(); - List entities = - this.getEntities().values().stream().filter(entity -> entity != currentEntity).toList(); - - player.sendPacket(new PacketSceneEntityAppearNotify(entities, VisionType.VISION_TYPE_MEET)); - } - - public void handleAttack(AttackResult result) { - // GameEntity attacker = getEntityById(result.getAttackerId()); - GameEntity target = getEntityById(result.getDefenseId()); - ElementType attackType = ElementType.getTypeByValue(result.getElementType()); - - if (target == null) { - return; - } - - // Godmode check - if (target instanceof EntityAvatar) { - if (((EntityAvatar) target).getPlayer().isInGodMode()) { - return; - } - } - - // Sanity check - target.damage(result.getDamage(), result.getAttackerId(), attackType); - } - - public void killEntity(GameEntity target) { - killEntity(target, 0); - } - - public void killEntity(GameEntity target, int attackerId) { - GameEntity attacker = null; - - if (attackerId > 0) { - attacker = getEntityById(attackerId); - } - - if (attacker != null) { - // Check codex - if (attacker instanceof EntityClientGadget gadgetAttacker) { - var clientGadgetOwner = getEntityById(gadgetAttacker.getOwnerEntityId()); - if (clientGadgetOwner instanceof EntityAvatar) { - ((EntityClientGadget) attacker) - .getOwner() - .getCodex() - .checkAnimal(target, CodexAnimalData.CountType.CODEX_COUNT_TYPE_KILL); - } - } else if (attacker instanceof EntityAvatar avatarAttacker) { - avatarAttacker - .getPlayer() - .getCodex() - .checkAnimal(target, CodexAnimalData.CountType.CODEX_COUNT_TYPE_KILL); - } - } - - // Packet - this.broadcastPacket(new PacketLifeStateChangeNotify(attackerId, target, LifeState.LIFE_DEAD)); - - // Reward drop - if (target instanceof EntityMonster && this.getSceneType() != SceneType.SCENE_DUNGEON) { - getWorld().getServer().getDropSystem().callDrop((EntityMonster) target); - } - - // Remove entity from world - this.removeEntity(target); - - // Death event - target.onDeath(attackerId); - } - - public void onTick() { - // Disable ticking for the player's home world. - if (this.getSceneType() == SceneType.SCENE_HOME_WORLD - || this.getSceneType() == SceneType.SCENE_HOME_ROOM) { - return; - } - - if (this.getScriptManager().isInit()) { - this.checkBlocks(); - } else { - // TEMPORARY - this.checkSpawns(); - } - - // Triggers - this.scriptManager.checkRegions(); - - if (challenge != null) { - challenge.onCheckTimeOut(); - } - - this.blossomManager.onTick(); - - checkNpcGroup(); - - this.finishLoading(); - this.checkPlayerRespawn(); - if (this.tickCount++ % 10 == 0) broadcastPacket(new PacketSceneTimeNotify(this)); - } - - /** Validates a player's current position. Teleports the player if the player is out of bounds. */ - private void checkPlayerRespawn() { - if (this.getScriptManager().getConfig() == null) return; - var diePos = this.getScriptManager().getConfig().die_y; - - // Check players in the scene. - this.players.forEach( - player -> { - if (this.getScriptManager().getConfig() == null) return; - - // Check if we need a respawn - if (diePos >= player.getPosition().getY()) { - // Respawn the player. - this.respawnPlayer(player); - } - }); - - // Check entities in the scene. - this.getEntities() - .forEach( - (id, entity) -> { - if (diePos >= entity.getPosition().getY()) { - this.killEntity(entity); - } - }); - } - - /** - * @return The script's default location, or the player's location. - */ - public Position getDefaultLocation(Player player) { - val defaultPosition = getScriptManager().getConfig().born_pos; - return defaultPosition != null ? defaultPosition : player.getPosition(); - } - - /** - * @return The script's default rotation, or the player's rotation. - */ - private Position getDefaultRot(Player player) { - var defaultRotation = this.getScriptManager().getConfig().born_rot; - return defaultRotation != null ? defaultRotation : player.getRotation(); - } - - /** - * Gets the respawn position for the player. - * - * @param player The player to get the respawn position for. - * @return The respawn position for the player. - */ - private Position getRespawnLocation(Player player) { - // TODO: Get the last valid location the player stood on. - var lastCheckpointPos = dungeonManager != null ? dungeonManager.getRespawnLocation() : null; - return lastCheckpointPos != null ? lastCheckpointPos : getDefaultLocation(player); - } - - /** - * Gets the respawn rotation for the player. - * - * @param player The player to get the respawn rotation for. - * @return The respawn rotation for the player. - */ - private Position getRespawnRotation(Player player) { - var lastCheckpointRot = - this.dungeonManager != null ? this.dungeonManager.getRespawnRotation() : null; - return lastCheckpointRot != null ? lastCheckpointRot : this.getDefaultRot(player); - } - - /** - * Teleports the player to the respawn location. - * - * @param player The player to respawn. - * @return true if the player was successfully respawned, false otherwise. - */ - public boolean respawnPlayer(Player player) { - // Apply void damage as a penalty. - player.getTeamManager().applyVoidDamage(); - - // TODO: Respawn the player at the last valid location. - var targetPos = getRespawnLocation(player); - var targetRot = getRespawnRotation(player); - var teleportProps = - TeleportProperties.builder() - .sceneId(getId()) - .teleportTo(targetPos) - .teleportRot(targetRot) - .teleportType(PlayerTeleportEvent.TeleportType.INTERNAL) - .enterType(EnterTypeOuterClass.EnterType.ENTER_TYPE_GOTO) - .enterReason( - dungeonManager != null ? EnterReason.DungeonReviveOnWaypoint : EnterReason.Revival); - - return this.getWorld().transferPlayerToScene(player, teleportProps.build()); - } - - /** - * Invoked when the scene finishes loading. Runs all callbacks that were added with {@link - * #runWhenFinished(Runnable)}. - */ - public void finishLoading() { - if (this.finishedLoading) return; - - this.finishedLoading = true; - this.afterLoadedCallbacks.forEach(Runnable::run); - this.afterLoadedCallbacks.clear(); - } - - /** - * Adds a callback to be executed when the scene is finished loading. If the scene is already - * finished loading, the callback will be executed immediately. - * - * @param runnable The callback to be executed. - */ - public void runWhenFinished(Runnable runnable) { - if (this.isFinishedLoading()) { - runnable.run(); - return; - } - - this.afterLoadedCallbacks.add(runnable); - } - - public int getEntityLevel(int baseLevel, int worldLevelOverride) { - int level = worldLevelOverride > 0 ? worldLevelOverride + baseLevel - 22 : baseLevel; - level = Math.min(level, 100); - level = level <= 0 ? 1 : level; - - return level; - } - - public void checkNpcGroup() { - Set npcBornEntries = ConcurrentHashMap.newKeySet(); - for (Player player : this.getPlayers()) { - npcBornEntries.addAll(loadNpcForPlayer(player)); - } - - // clear the unreachable group for client - var toUnload = - this.npcBornEntrySet.stream() - .filter(i -> !npcBornEntries.contains(i)) - .map(SceneNpcBornEntry::getGroupId) - .toList(); - - if (toUnload.size() > 0) { - broadcastPacket(new PacketGroupUnloadNotify(toUnload)); - Grasscutter.getLogger().debug("Unload NPC Group {}", toUnload); - } - // exchange the new npcBornEntry Set - this.npcBornEntrySet = npcBornEntries; - } - - public synchronized void checkSpawns() { - Set loadedGridBlocks = new HashSet<>(); - for (Player player : this.getPlayers()) { - Collections.addAll( - loadedGridBlocks, - SpawnDataEntry.GridBlockId.getAdjacentGridBlockIds( - player.getSceneId(), player.getPosition())); - } - if (this.loadedGridBlocks.containsAll( - loadedGridBlocks)) { // Don't recalculate static spawns if nothing has changed - return; - } - this.loadedGridBlocks = loadedGridBlocks; - var spawnLists = GameDepot.getSpawnLists(); - Set visible = new HashSet<>(); - for (var block : loadedGridBlocks) { - var spawns = spawnLists.get(block); - if (spawns != null) { - visible.addAll(spawns); - } - } - - // World level - WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getWorld().getWorldLevel()); - int worldLevelOverride = 0; - - if (worldLevelData != null) { - worldLevelOverride = worldLevelData.getMonsterLevel(); - } - - // Todo - List toAdd = new ArrayList<>(); - List toRemove = new ArrayList<>(); - var spawnedEntities = this.getSpawnedEntities(); - for (SpawnDataEntry entry : visible) { - // If spawn entry is in our view and hasnt been spawned/killed yet, we should spawn it - if (!spawnedEntities.contains(entry) && !this.getDeadSpawnedEntities().contains(entry)) { - // Entity object holder - GameEntity entity = null; - - // Check if spawn entry is monster or gadget - if (entry.getMonsterId() > 0) { - MonsterData data = GameData.getMonsterDataMap().get(entry.getMonsterId()); - if (data == null) continue; - - int level = this.getEntityLevel(entry.getLevel(), worldLevelOverride); - - EntityMonster monster = new EntityMonster(this, data, entry.getPos(), level); - monster.getRotation().set(entry.getRot()); - monster.setGroupId(entry.getGroup().getGroupId()); - monster.setPoseId(entry.getPoseId()); - monster.setConfigId(entry.getConfigId()); - monster.setSpawnEntry(entry); - - entity = monster; - } else if (entry.getGadgetId() > 0) { - EntityGadget gadget = - new EntityGadget(this, entry.getGadgetId(), entry.getPos(), entry.getRot()); - gadget.setGroupId(entry.getGroup().getGroupId()); - gadget.setConfigId(entry.getConfigId()); - gadget.setSpawnEntry(entry); - int state = entry.getGadgetState(); - if (state > 0) { - gadget.setState(state); - } - gadget.buildContent(); - - gadget.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, Float.POSITIVE_INFINITY); - gadget.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Float.POSITIVE_INFINITY); - gadget.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, Float.POSITIVE_INFINITY); - - entity = gadget; - blossomManager.initBlossom(gadget); - } - - if (entity == null) continue; - - // Add to scene and spawned list - toAdd.add(entity); - spawnedEntities.add(entry); - } - } - - for (GameEntity entity : this.getEntities().values()) { - var spawnEntry = entity.getSpawnEntry(); - if (spawnEntry != null && !visible.contains(spawnEntry)) { - toRemove.add(entity); - spawnedEntities.remove(spawnEntry); - } - } - - if (toAdd.size() > 0) { - toAdd.forEach(this::addEntityDirectly); - this.broadcastPacket(new PacketSceneEntityAppearNotify(toAdd, VisionType.VISION_TYPE_BORN)); - } - - if (toRemove.size() > 0) { - toRemove.forEach(this::removeEntityDirectly); - this.broadcastPacket( - new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); - blossomManager.recycleGadgetEntity(toRemove); - } - } - - public List getPlayerActiveBlocks(Player player) { - // consider the borders' entities of blocks, so we check if contains by index - return SceneIndexManager.queryNeighbors( - getScriptManager().getBlocksIndex(), - player.getPosition().toXZDoubleArray(), - Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); - } - - private boolean unloadBlockIfNotVisible(Collection visible, SceneBlock block) { - if (visible.contains(block)) return false; - this.onUnloadBlock(block); - return true; - } - - public synchronized boolean loadBlock(SceneBlock block) { - if (this.loadedBlocks.contains(block)) return false; - - this.onLoadBlock(block, this.players); - this.loadedBlocks.add(block); - return true; - } - - public synchronized void checkBlocks() { - Set visible = - this.players.stream() - .map(this::getPlayerActiveBlocks) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - - this.loadedBlocks.removeIf(block -> unloadBlockIfNotVisible(visible, block)); - visible.stream() - .filter(block -> !this.loadBlock(block)) - .forEach( - block -> { - // dynamic load the groups for players in a loaded block - var toLoad = - this.players.stream() - .filter(p -> block.contains(p.getPosition())) - .map(p -> this.playerMeetGroups(p, block)) - .flatMap(Collection::stream) - .toList(); - this.onLoadGroup(toLoad); - }); - } - - public List playerMeetGroups(Player player, SceneBlock block) { - List sceneGroups = - SceneIndexManager.queryNeighbors( - block.sceneGroupIndex, - player.getPosition().toDoubleArray(), - Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); - - List groups = - sceneGroups.stream() - .filter( - group -> !scriptManager.getLoadedGroupSetPerBlock().get(block.id).contains(group)) - .peek(group -> scriptManager.getLoadedGroupSetPerBlock().get(block.id).add(group)) - .toList(); - - if (groups.size() == 0) { - return List.of(); - } - - return groups; - } - - public void onLoadBlock(SceneBlock block, List players) { - this.getScriptManager().loadBlockFromScript(block); - scriptManager.getLoadedGroupSetPerBlock().put(block.id, new HashSet<>()); - - // the groups form here is not added in current scene - var groups = - players.stream() - .filter(player -> block.contains(player.getPosition())) - .map(p -> playerMeetGroups(p, block)) - .flatMap(Collection::stream) - .toList(); - - onLoadGroup(groups); - Grasscutter.getLogger().info("Scene {} Block {} loaded.", this.getId(), block.id); - } - - public int loadDynamicGroup(int group_id) { - SceneGroup group = getScriptManager().getGroupById(group_id); - if (group == null || getScriptManager().getGroupInstanceById(group_id) != null) - return -1; // Group not found or already instanced - - onLoadGroup(new ArrayList<>(List.of(group))); - - if (GameData.getGroupReplacements().containsKey(group_id)) onRegisterGroups(); - - if (group.init_config == null) return -1; - return group.init_config.suite; - } - - public boolean unregisterDynamicGroup(int groupId) { - var group = getScriptManager().getGroupById(groupId); - if (group == null) return false; - - var block = getScriptManager().getBlocks().get(group.block_id); - this.unloadGroup(block, groupId); - return true; - } - - public void onRegisterGroups() { - var sceneGroups = this.loadedGroups; - var sceneGroupMap = - sceneGroups.stream().collect(Collectors.toMap(item -> item.id, item -> item)); - var sceneGroupsIds = sceneGroups.stream().map(group -> group.id).toList(); - var dynamicGroups = - sceneGroups.stream().filter(group -> group.dynamic_load).map(group -> group.id).toList(); - - // Create the graph - var nodes = new ArrayList(); - var groupList = new ArrayList(); - GameData.getGroupReplacements().values().stream() - .filter(replacement -> dynamicGroups.contains(replacement.id)) - .forEach( - replacement -> { - Grasscutter.getLogger().info("Graph ordering replacement {}", replacement); - replacement.replace_groups.forEach( - group -> { - nodes.add(new KahnsSort.Node(replacement.id, group)); - if (!groupList.contains(group)) groupList.add(group); - }); - - if (!groupList.contains(replacement.id)) groupList.add(replacement.id); - }); - - KahnsSort.Graph graph = new KahnsSort.Graph(nodes, groupList); - List dynamicGroupsOrdered = KahnsSort.doSort(graph); - if (dynamicGroupsOrdered == null) throw new RuntimeException("Invalid group replacement graph"); - - // Now we can start unloading and loading groups :D - dynamicGroupsOrdered.forEach( - group -> { - if (GameData.getGroupReplacements().containsKey((int) group)) { // isGroupJoinReplacement - var data = GameData.getGroupReplacements().get((int) group); - var sceneGroupReplacement = - this.loadedGroups.stream().filter(g -> g.id == group).findFirst().orElseThrow(); - if (sceneGroupReplacement.is_replaceable != null) { - var it = data.replace_groups.iterator(); - while (it.hasNext()) { - var replace_group = it.next(); - if (!sceneGroupsIds.contains(replace_group)) continue; - - // Check if we can replace this group - SceneGroup sceneGroup = sceneGroupMap.get(replace_group); - if (sceneGroup != null - && sceneGroup.is_replaceable != null - && ((sceneGroup.is_replaceable.value - && sceneGroup.is_replaceable.version - <= sceneGroupReplacement.is_replaceable.version) - || sceneGroup.is_replaceable.new_bin_only)) { - this.unloadGroup( - scriptManager.getBlocks().get(sceneGroup.block_id), replace_group); - it.remove(); - Grasscutter.getLogger().info("Graph ordering: unloaded {}", replace_group); - } - } - } - } - }); - } - - public void loadTriggerFromGroup(SceneGroup group, String triggerName) { - // Load triggers and regions - this.getScriptManager() - .registerTrigger( - group.triggers.values().stream() - .filter(p -> p.getName().contains(triggerName)) - .toList()); - group.regions.values().stream() - .filter(q -> q.config_id == Integer.parseInt(triggerName.substring(13))) - .map(region -> new EntityRegion(this, region)) - .forEach(getScriptManager()::registerRegion); - } - - public void onLoadGroup(List groups) { - if (groups == null || groups.isEmpty()) { - return; - } - - for (var group : groups) { - if (this.loadedGroups.contains(group)) continue; - - // We load the script files for the groups here - this.getScriptManager().loadGroupFromScript(group); - if (!this.scriptManager.getLoadedGroupSetPerBlock().containsKey(group.block_id)) - this.onLoadBlock(scriptManager.getBlocks().get(group.block_id), players); - this.scriptManager.getLoadedGroupSetPerBlock().get(group.block_id).add(group); - } - - // Spawn gadgets AFTER triggers are added - // TODO - var entities = new ArrayList(); - for (var group : groups) { - if (this.loadedGroups.contains(group)) continue; - - if (group.init_config == null) { - continue; - } - - var groupInstance = this.getScriptManager().getGroupInstanceById(group.id); - var cachedInstance = this.getScriptManager().getCachedGroupInstanceById(group.id); - if (cachedInstance != null) { - cachedInstance.setLuaGroup(group); - groupInstance = cachedInstance; - } - - // Load garbages - var garbageGadgets = group.getGarbageGadgets(); - - if (garbageGadgets != null) { - entities.addAll( - garbageGadgets.stream() - .map(g -> scriptManager.createGadget(group.id, group.block_id, g)) - .filter(Objects::nonNull) - .toList()); - } - - // Load suites - // int suite = group.findInitSuiteIndex(0); - this.getScriptManager() - .refreshGroup(groupInstance, 0, false); // This is what the official server does - - this.loadedGroups.add(group); - } - - this.scriptManager.meetEntities(entities); - groups.forEach( - g -> scriptManager.callEvent(new ScriptArgs(g.id, EventType.EVENT_GROUP_LOAD, g.id))); - - Grasscutter.getLogger().info("Scene {} loaded {} group(s)", this.getId(), groups.size()); - } - - public void onUnloadBlock(SceneBlock block) { - List toRemove = - this.getEntities().values().stream().filter(e -> e.getBlockId() == block.id).toList(); - - if (toRemove.size() > 0) { - toRemove.forEach(this::removeEntityDirectly); - this.broadcastPacket( - new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); - } - - for (SceneGroup group : block.groups.values()) { - if (group.triggers != null) { - group.triggers.values().forEach(getScriptManager()::deregisterTrigger); - } - if (group.regions != null) { - group.regions.values().forEach(getScriptManager()::deregisterRegion); - } - } - scriptManager.getLoadedGroupSetPerBlock().remove(block.id); - Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id); - } - - /** - * Unloads a Lua group. - * - * @param block The block that contains the group. - * @param groupId The group ID. - */ - public void unloadGroup(SceneBlock block, int groupId) { - var toRemove = - this.getEntities().values().stream() - .filter(e -> e != null && (e.getBlockId() == block.id && e.getGroupId() == groupId)) - .toList(); - - if (toRemove.size() > 0) { - toRemove.forEach(this::removeEntityDirectly); - this.broadcastPacket( - new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); - } - - var group = block.groups.get(groupId); - if (group.triggers != null) { - group.triggers.values().forEach(this.getScriptManager()::deregisterTrigger); - } - if (group.regions != null) { - group.regions.values().forEach(this.getScriptManager()::deregisterRegion); - } - - this.scriptManager.getLoadedGroupSetPerBlock().get(block.id).remove(group); - this.loadedGroups.remove(group); - - if (this.scriptManager.getLoadedGroupSetPerBlock().get(block.id).isEmpty()) { - this.scriptManager.getLoadedGroupSetPerBlock().remove(block.id); - Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id); - } - - this.broadcastPacket(new PacketGroupUnloadNotify(List.of(groupId))); - this.scriptManager.unregisterGroup(group); - } - - // Gadgets - - public void onPlayerCreateGadget(EntityClientGadget gadget) { - // Directly add - this.addEntityDirectly(gadget); - - // Add to owner's gadget list - gadget.getOwner().getTeamManager().getGadgets().add(gadget); - - // Optimization - if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { - return; - } - - this.broadcastPacketToOthers(gadget.getOwner(), new PacketSceneEntityAppearNotify(gadget)); - } - - public void onPlayerDestroyGadget(int entityId) { - GameEntity entity = getEntities().get(entityId); - - if (!(entity instanceof EntityClientGadget gadget)) { - return; - } - - // Get and remove entity - this.removeEntityDirectly(gadget); - - // Remove from owner's gadget list - gadget.getOwner().getTeamManager().getGadgets().remove(gadget); - - // Optimization - if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { - return; - } - - this.broadcastPacketToOthers( - gadget.getOwner(), - new PacketSceneEntityDisappearNotify(gadget, VisionType.VISION_TYPE_DIE)); - } - - // Broadcasting - - public void broadcastPacket(BasePacket packet) { - // Send to all players - might have to check if player has been sent data packets - for (Player player : this.getPlayers()) { - player.getSession().send(packet); - } - } - - public void broadcastPacketToOthers(Player excludedPlayer, BasePacket packet) { - // Optimization - if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == excludedPlayer) { - return; - } - // Send to all players - might have to check if player has been sent data packets - for (Player player : this.getPlayers()) { - if (player == excludedPlayer) { - continue; - } - // Send - player.getSession().send(packet); - } - } - - public void addItemEntity(int itemId, int amount, GameEntity bornForm) { - ItemData itemData = GameData.getItemDataMap().get(itemId); - if (itemData == null) { - return; - } - if (itemData.isEquip()) { - float range = (1.5f + (.05f * amount)); - for (int i = 0; i < amount; i++) { - Position pos = bornForm.getPosition().nearby2d(range).addZ(.9f); // Why Z? - EntityItem entity = new EntityItem(this, null, itemData, pos, 1); - addEntity(entity); - } - } else { - EntityItem entity = - new EntityItem( - this, null, itemData, bornForm.getPosition().clone().addZ(.9f), amount); // Why Z? - addEntity(entity); - } - } - - public void loadNpcForPlayerEnter(Player player) { - this.npcBornEntrySet.addAll(loadNpcForPlayer(player)); - } - - private List loadNpcForPlayer(Player player) { - var pos = player.getPosition(); - var data = GameData.getSceneNpcBornData().get(getId()); - if (data == null) { - return List.of(); - } - - var npcList = - SceneIndexManager.queryNeighbors( - data.getIndex(), - pos.toDoubleArray(), - Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); - - var sceneNpcBornEntries = - npcList.stream().filter(i -> !this.npcBornEntrySet.contains(i)).toList(); - - if (sceneNpcBornEntries.size() > 0) { - this.broadcastPacket(new PacketGroupSuiteNotify(sceneNpcBornEntries)); - Grasscutter.getLogger().debug("Loaded Npc Group Suite {}", sceneNpcBornEntries); - } - return npcList; - } - - public void loadGroupForQuest(List sceneGroupSuite) { - if (!scriptManager.isInit()) { - return; - } - - sceneGroupSuite.forEach( - i -> { - var group = scriptManager.getGroupById(i.getGroup()); - if (group == null) return; - - var groupInstance = scriptManager.getGroupInstanceById(i.getGroup()); - var suite = group.getSuiteByIndex(i.getSuite()); - if (suite == null || groupInstance == null) { - return; - } - - scriptManager.refreshGroup(groupInstance, i.getSuite(), false); - }); - } - - /** - * Adds an unlocked force to the scene. - * - * @param force The ID of the force to unlock. - */ - public void unlockForce(int force) { - this.unlockedForces.add(force); - this.broadcastPacket(new PacketSceneForceUnlockNotify(force, true)); - } - - /** - * Removes an unlocked force from the scene. - * - * @param force The ID of the force to lock. - */ - public void lockForce(int force) { - this.unlockedForces.remove(force); - this.broadcastPacket(new PacketSceneForceLockNotify(force)); - } - - public void selectWorktopOptionWith(SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq req) { - GameEntity entity = getEntityById(req.getGadgetEntityId()); - if (entity == null) { - return; - } - // Handle - if (entity instanceof EntityGadget gadget) { - if (gadget.getContent() instanceof GadgetWorktop worktop) { - boolean shouldDelete = worktop.onSelectWorktopOption(req); - if (shouldDelete) { - entity.getScene().removeEntity(entity, VisionType.VISION_TYPE_REMOVE); - } - } - } - } -} +package emu.grasscutter.game.world; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.GameDepot; +import emu.grasscutter.data.binout.SceneNpcBornEntry; +import emu.grasscutter.data.binout.routes.Route; +import emu.grasscutter.data.excels.*; +import emu.grasscutter.data.excels.codex.CodexAnimalData; +import emu.grasscutter.data.excels.monster.MonsterData; +import emu.grasscutter.data.excels.world.WorldLevelData; +import emu.grasscutter.data.server.Grid; +import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.dungeons.DungeonManager; +import emu.grasscutter.game.dungeons.DungeonSettleListener; +import emu.grasscutter.game.dungeons.challenge.WorldChallenge; +import emu.grasscutter.game.dungeons.enums.DungeonPassConditionType; +import emu.grasscutter.game.entity.*; +import emu.grasscutter.game.entity.gadget.GadgetWorktop; +import emu.grasscutter.game.managers.blossom.BlossomManager; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.player.TeamInfo; +import emu.grasscutter.game.props.*; +import emu.grasscutter.game.quest.QuestGroupSuite; +import emu.grasscutter.game.world.data.TeleportProperties; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; +import emu.grasscutter.net.proto.EnterTypeOuterClass; +import emu.grasscutter.net.proto.SelectWorktopOptionReqOuterClass; +import emu.grasscutter.net.proto.VisionTypeOuterClass.VisionType; +import emu.grasscutter.scripts.SceneIndexManager; +import emu.grasscutter.scripts.SceneScriptManager; +import emu.grasscutter.scripts.constants.EventType; +import emu.grasscutter.scripts.data.SceneBlock; +import emu.grasscutter.scripts.data.SceneGroup; +import emu.grasscutter.scripts.data.ScriptArgs; +import emu.grasscutter.server.event.player.PlayerTeleportEvent; +import emu.grasscutter.server.packet.send.*; +import emu.grasscutter.utils.KahnsSort; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; +import lombok.val; + +import javax.annotation.Nullable; + +public final class Scene { + @Getter private final World world; + @Getter private final SceneData sceneData; + @Getter private final List players; + @Getter private final Map entities; + @Getter private final Set spawnedEntities; + @Getter private final Set deadSpawnedEntities; + @Getter private final Set loadedBlocks; + @Getter private final Set loadedGroups; + @Getter private final BlossomManager blossomManager; + private final HashSet unlockedForces; + private final List afterLoadedCallbacks = new ArrayList<>(); + private final long startWorldTime; + @Getter @Setter DungeonManager dungeonManager; + @Getter Int2ObjectMap sceneRoutes; + private Set loadedGridBlocks; + @Getter @Setter private boolean dontDestroyWhenEmpty; + @Getter private final SceneScriptManager scriptManager; + @Getter @Setter private WorldChallenge challenge; + @Getter private List dungeonSettleListeners; + @Getter @Setter private int prevScene; // Id of the previous scene + @Getter @Setter private int prevScenePoint; + @Getter @Setter private int killedMonsterCount; + private Set npcBornEntrySet; + @Getter private boolean finishedLoading = false; + @Getter private int tickCount = 0; + @Getter private boolean isPaused = false; + + public Scene(World world, SceneData sceneData) { + this.world = world; + this.sceneData = sceneData; + this.players = new CopyOnWriteArrayList<>(); + this.entities = new ConcurrentHashMap<>(); + + this.prevScene = 3; + this.sceneRoutes = GameData.getSceneRoutes(getId()); + + this.startWorldTime = world.getWorldTime(); + + this.spawnedEntities = ConcurrentHashMap.newKeySet(); + this.deadSpawnedEntities = ConcurrentHashMap.newKeySet(); + this.loadedBlocks = ConcurrentHashMap.newKeySet(); + this.loadedGroups = ConcurrentHashMap.newKeySet(); + this.loadedGridBlocks = new HashSet<>(); + this.npcBornEntrySet = ConcurrentHashMap.newKeySet(); + this.scriptManager = new SceneScriptManager(this); + this.blossomManager = new BlossomManager(this); + this.unlockedForces = new HashSet<>(); + } + + public int getId() { + return sceneData.getId(); + } + + public SceneType getSceneType() { + return getSceneData().getSceneType(); + } + + public int getPlayerCount() { + return this.getPlayers().size(); + } + + public GameEntity getEntityById(int id) { + return this.entities.get(id); + } + + public GameEntity getEntityByConfigId(int configId) { + return this.entities.values().stream() + .filter(x -> x.getConfigId() == configId) + .findFirst() + .orElse(null); + } + + public GameEntity getEntityByConfigId(int configId, int groupId) { + return this.entities.values().stream() + .filter(x -> x.getConfigId() == configId && x.getGroupId() == groupId) + .findFirst() + .orElse(null); + } + + @Nullable public Route getSceneRouteById(int routeId) { + return sceneRoutes.get(routeId); + } + + /** + * Sets the scene's pause state. Sends the current scene's time to all players. + * + * @param paused The new pause state. + */ + public void setPaused(boolean paused) { + if (this.isPaused != paused) { + this.isPaused = paused; + this.broadcastPacket(new PacketSceneTimeNotify(this)); + } + } + + /** + * Gets the time in seconds since the scene started. + * + * @return The time in seconds since the scene started. + */ + public int getSceneTime() { + return (int) (this.getWorld().getWorldTime() - this.startWorldTime); + } + + /** + * Gets {@link Scene#getSceneTime()} in seconds. + * + * @return The time in seconds since the scene started. + */ + public int getSceneTimeSeconds() { + return this.getSceneTime() / 1000; + } + + public void addDungeonSettleObserver(DungeonSettleListener dungeonSettleListener) { + if (dungeonSettleListeners == null) { + dungeonSettleListeners = new ArrayList<>(); + } + + dungeonSettleListeners.add(dungeonSettleListener); + } + + /** + * Triggers an event in the dungeon manager. + * + * @param conditionType The condition type to trigger. + * @param params The parameters to pass to the event. + */ + public void triggerDungeonEvent(DungeonPassConditionType conditionType, int... params) { + if (this.dungeonManager == null) return; + this.dungeonManager.triggerEvent(conditionType, params); + } + + public boolean isInScene(GameEntity entity) { + return this.entities.containsKey(entity.getId()); + } + + public synchronized void addPlayer(Player player) { + // Check if player already in + if (getPlayers().contains(player)) { + return; + } + + // Remove player from prev scene + if (player.getScene() != null) { + player.getScene().removePlayer(player); + } + + // Add + getPlayers().add(player); + player.setSceneId(this.getId()); + player.setScene(this); + + this.setupPlayerAvatars(player); + } + + public synchronized void removePlayer(Player player) { + // Remove from challenge if leaving + if (this.getChallenge() != null && this.getChallenge().inProgress()) { + player.sendPacket(new PacketDungeonChallengeFinishNotify(this.getChallenge())); + } + + // Remove player from scene + getPlayers().remove(player); + player.setScene(null); + + // Remove player avatars + this.removePlayerAvatars(player); + + // Remove player gadgets + for (EntityBaseGadget gadget : player.getTeamManager().getGadgets()) { + this.removeEntity(gadget); + } + + // Deregister scene if not in use + if (this.getPlayerCount() <= 0 && !this.dontDestroyWhenEmpty) { + this.getScriptManager().onDestroy(); + this.getWorld().deregisterScene(this); + } + + this.saveGroups(); + } + + private void setupPlayerAvatars(Player player) { + // Clear entities from old team + player.getTeamManager().getActiveTeam().clear(); + + // Add new entities for player + TeamInfo teamInfo = player.getTeamManager().getCurrentTeamInfo(); + for (int avatarId : teamInfo.getAvatars()) { + Avatar avatar = player.getAvatars().getAvatarById(avatarId); + if (avatar == null) { + if (player.getTeamManager().isUsingTrialTeam()) { + avatar = player.getTeamManager().getTrialAvatars().get(avatarId); + } + if (avatar == null) continue; + } + player.getTeamManager().getActiveTeam().add(new EntityAvatar(player.getScene(), avatar)); + } + + // Limit character index in case its out of bounds + if (player.getTeamManager().getCurrentCharacterIndex() + >= player.getTeamManager().getActiveTeam().size() + || player.getTeamManager().getCurrentCharacterIndex() < 0) { + player + .getTeamManager() + .setCurrentCharacterIndex(player.getTeamManager().getCurrentCharacterIndex() - 1); + } + } + + private synchronized void removePlayerAvatars(Player player) { + var team = player.getTeamManager().getActiveTeam(); + // removeEntities(team, VisionType.VISION_TYPE_REMOVE); // List isn't cool apparently + // :( + team.forEach(e -> removeEntity(e, VisionType.VISION_TYPE_REMOVE)); + team.clear(); + } + + public void spawnPlayer(Player player) { + var teamManager = player.getTeamManager(); + if (this.isInScene(teamManager.getCurrentAvatarEntity())) { + return; + } + + if (teamManager.getCurrentAvatarEntity().getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) + <= 0f) { + teamManager.getCurrentAvatarEntity().setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 1f); + } + + this.addEntity(teamManager.getCurrentAvatarEntity()); + + // Notify the client of any extra skill charges + teamManager.getActiveTeam().stream() + .map(EntityAvatar::getAvatar) + .forEach(Avatar::sendSkillExtraChargeMap); + } + + private void addEntityDirectly(GameEntity entity) { + getEntities().put(entity.getId(), entity); + entity.onCreate(); // Call entity create event + } + + public synchronized void addEntity(GameEntity entity) { + this.addEntityDirectly(entity); + this.broadcastPacket(new PacketSceneEntityAppearNotify(entity)); + } + + public synchronized void addEntityToSingleClient(Player player, GameEntity entity) { + this.addEntityDirectly(entity); + player.sendPacket(new PacketSceneEntityAppearNotify(entity)); + } + + public void addEntities(Collection entities) { + addEntities(entities, VisionType.VISION_TYPE_BORN); + } + + public synchronized void addEntities( + Collection entities, VisionType visionType) { + if (entities == null || entities.isEmpty()) { + return; + } + for (GameEntity entity : entities) { + this.addEntityDirectly(entity); + } + + this.broadcastPacket(new PacketSceneEntityAppearNotify(entities, visionType)); + } + + private GameEntity removeEntityDirectly(GameEntity entity) { + var removed = getEntities().remove(entity.getId()); + if (removed != null) { + removed.onRemoved(); // Call entity remove event + } + return removed; + } + + public void removeEntity(GameEntity entity) { + this.removeEntity(entity, VisionType.VISION_TYPE_DIE); + } + + public synchronized void removeEntity(GameEntity entity, VisionType visionType) { + GameEntity removed = this.removeEntityDirectly(entity); + if (removed != null) { + this.broadcastPacket(new PacketSceneEntityDisappearNotify(removed, visionType)); + } + } + + public synchronized void removeEntities(List entity, VisionType visionType) { + var toRemove = + entity.stream() + .filter(Objects::nonNull) + .map(this::removeEntityDirectly) + .filter(Objects::nonNull) + .toList(); + if (toRemove.size() > 0) { + this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, visionType)); + } + } + + public synchronized void replaceEntity(EntityAvatar oldEntity, EntityAvatar newEntity) { + this.removeEntityDirectly(oldEntity); + this.addEntityDirectly(newEntity); + this.broadcastPacket( + new PacketSceneEntityDisappearNotify(oldEntity, VisionType.VISION_TYPE_REPLACE)); + this.broadcastPacket( + new PacketSceneEntityAppearNotify( + newEntity, VisionType.VISION_TYPE_REPLACE, oldEntity.getId())); + } + + public void showOtherEntities(Player player) { + GameEntity currentEntity = player.getTeamManager().getCurrentAvatarEntity(); + List entities = + this.getEntities().values().stream().filter(entity -> entity != currentEntity).toList(); + + player.sendPacket(new PacketSceneEntityAppearNotify(entities, VisionType.VISION_TYPE_MEET)); + } + + public void handleAttack(AttackResult result) { + // GameEntity attacker = getEntityById(result.getAttackerId()); + GameEntity target = getEntityById(result.getDefenseId()); + ElementType attackType = ElementType.getTypeByValue(result.getElementType()); + + if (target == null) { + return; + } + + // Godmode check + if (target instanceof EntityAvatar) { + if (((EntityAvatar) target).getPlayer().isInGodMode()) { + return; + } + } + + // Sanity check + target.damage(result.getDamage(), result.getAttackerId(), attackType); + } + + public void killEntity(GameEntity target) { + killEntity(target, 0); + } + + public void killEntity(GameEntity target, int attackerId) { + GameEntity attacker = null; + + if (attackerId > 0) { + attacker = getEntityById(attackerId); + } + + if (attacker != null) { + // Check codex + if (attacker instanceof EntityClientGadget gadgetAttacker) { + var clientGadgetOwner = getEntityById(gadgetAttacker.getOwnerEntityId()); + if (clientGadgetOwner instanceof EntityAvatar) { + ((EntityClientGadget) attacker) + .getOwner() + .getCodex() + .checkAnimal(target, CodexAnimalData.CountType.CODEX_COUNT_TYPE_KILL); + } + } else if (attacker instanceof EntityAvatar avatarAttacker) { + avatarAttacker + .getPlayer() + .getCodex() + .checkAnimal(target, CodexAnimalData.CountType.CODEX_COUNT_TYPE_KILL); + } + } + + // Packet + this.broadcastPacket(new PacketLifeStateChangeNotify(attackerId, target, LifeState.LIFE_DEAD)); + + // Reward drop + if (target instanceof EntityMonster && this.getSceneType() != SceneType.SCENE_DUNGEON) { + getWorld().getServer().getDropSystem().callDrop((EntityMonster) target); + } + + // Remove entity from world + this.removeEntity(target); + + // Death event + target.onDeath(attackerId); + this.triggerDungeonEvent( + DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER_COUNT, ++killedMonsterCount); + } + + public void onTick() { + // Disable ticking for the player's home world. + if (this.getSceneType() == SceneType.SCENE_HOME_WORLD + || this.getSceneType() == SceneType.SCENE_HOME_ROOM) { + this.finishLoading(); + return; + } + + if (this.getScriptManager().isInit()) { + // this.checkBlocks(); + this.checkGroups(); + } else { + // TEMPORARY + this.checkSpawns(); + } + + // Triggers + this.scriptManager.checkRegions(); + + if (challenge != null) { + challenge.onCheckTimeOut(); + } + + var sceneTime = getSceneTimeSeconds(); + getEntities().forEach((eid, e) -> e.onTick(sceneTime)); + + blossomManager.onTick(); + + checkNpcGroup(); + + this.finishLoading(); + this.checkPlayerRespawn(); + if (this.tickCount++ % 10 == 0) broadcastPacket(new PacketSceneTimeNotify(this)); + } + + /** Validates a player's current position. Teleports the player if the player is out of bounds. */ + private void checkPlayerRespawn() { + if (this.getScriptManager().getConfig() == null) return; + var diePos = this.getScriptManager().getConfig().die_y; + + // Check players in the scene. + this.players.forEach( + player -> { + if (this.getScriptManager().getConfig() == null) return; + + // Check if we need a respawn + if (diePos >= player.getPosition().getY()) { + // Respawn the player. + this.respawnPlayer(player); + } + }); + + // Check entities in the scene. + this.getEntities() + .forEach( + (id, entity) -> { + if (diePos >= entity.getPosition().getY()) { + this.killEntity(entity); + } + }); + } + + /** + * @return The script's default location, or the player's location. + */ + public Position getDefaultLocation(Player player) { + val defaultPosition = getScriptManager().getConfig().born_pos; + return defaultPosition != null ? defaultPosition : player.getPosition(); + } + + /** + * @return The script's default rotation, or the player's rotation. + */ + private Position getDefaultRot(Player player) { + var defaultRotation = this.getScriptManager().getConfig().born_rot; + return defaultRotation != null ? defaultRotation : player.getRotation(); + } + + /** + * Gets the respawn position for the player. + * + * @param player The player to get the respawn position for. + * @return The respawn position for the player. + */ + private Position getRespawnLocation(Player player) { + // TODO: Get the last valid location the player stood on. + var lastCheckpointPos = dungeonManager != null ? dungeonManager.getRespawnLocation() : null; + return lastCheckpointPos != null ? lastCheckpointPos : getDefaultLocation(player); + } + + /** + * Gets the respawn rotation for the player. + * + * @param player The player to get the respawn rotation for. + * @return The respawn rotation for the player. + */ + private Position getRespawnRotation(Player player) { + var lastCheckpointRot = + this.dungeonManager != null ? this.dungeonManager.getRespawnRotation() : null; + return lastCheckpointRot != null ? lastCheckpointRot : this.getDefaultRot(player); + } + + /** + * Teleports the player to the respawn location. + * + * @param player The player to respawn. + * @return true if the player was successfully respawned, false otherwise. + */ + public boolean respawnPlayer(Player player) { + // Apply void damage as a penalty. + player.getTeamManager().applyVoidDamage(); + + // TODO: Respawn the player at the last valid location. + var targetPos = getRespawnLocation(player); + var targetRot = getRespawnRotation(player); + var teleportProps = + TeleportProperties.builder() + .sceneId(getId()) + .teleportTo(targetPos) + .teleportRot(targetRot) + .teleportType(PlayerTeleportEvent.TeleportType.INTERNAL) + .enterType(EnterTypeOuterClass.EnterType.ENTER_TYPE_GOTO) + .enterReason( + dungeonManager != null ? EnterReason.DungeonReviveOnWaypoint : EnterReason.Revival); + + return this.getWorld().transferPlayerToScene(player, teleportProps.build()); + } + + /** + * Invoked when the scene finishes loading. Runs all callbacks that were added with {@link + * #runWhenFinished(Runnable)}. + */ + public void finishLoading() { + if (this.finishedLoading) return; + + this.finishedLoading = true; + this.afterLoadedCallbacks.forEach(Runnable::run); + this.afterLoadedCallbacks.clear(); + } + + /** + * Adds a callback to be executed when the scene is finished loading. If the scene is already + * finished loading, the callback will be executed immediately. + * + * @param runnable The callback to be executed. + */ + public void runWhenFinished(Runnable runnable) { + if (this.isFinishedLoading()) { + runnable.run(); + return; + } + + this.afterLoadedCallbacks.add(runnable); + } + + public int getEntityLevel(int baseLevel, int worldLevelOverride) { + int level = worldLevelOverride > 0 ? worldLevelOverride + baseLevel - 22 : baseLevel; + level = Math.min(level, 100); + level = level <= 0 ? 1 : level; + + return level; + } + + public void checkNpcGroup() { + Set npcBornEntries = ConcurrentHashMap.newKeySet(); + for (Player player : this.getPlayers()) { + npcBornEntries.addAll(loadNpcForPlayer(player)); + } + + // clear the unreachable group for client + var toUnload = + this.npcBornEntrySet.stream() + .filter(i -> !npcBornEntries.contains(i)) + .map(SceneNpcBornEntry::getGroupId) + .toList(); + + if (toUnload.size() > 0) { + broadcastPacket(new PacketGroupUnloadNotify(toUnload)); + Grasscutter.getLogger().debug("Unload NPC Group {}", toUnload); + } + // exchange the new npcBornEntry Set + this.npcBornEntrySet = npcBornEntries; + } + + public synchronized void checkSpawns() { + Set loadedGridBlocks = new HashSet<>(); + for (Player player : this.getPlayers()) { + Collections.addAll( + loadedGridBlocks, + SpawnDataEntry.GridBlockId.getAdjacentGridBlockIds( + player.getSceneId(), player.getPosition())); + } + if (this.loadedGridBlocks.containsAll( + loadedGridBlocks)) { // Don't recalculate static spawns if nothing has changed + return; + } + this.loadedGridBlocks = loadedGridBlocks; + var spawnLists = GameDepot.getSpawnLists(); + Set visible = new HashSet<>(); + for (var block : loadedGridBlocks) { + var spawns = spawnLists.get(block); + if (spawns != null) { + visible.addAll(spawns); + } + } + + // World level + WorldLevelData worldLevelData = GameData.getWorldLevelDataMap().get(getWorld().getWorldLevel()); + int worldLevelOverride = 0; + + if (worldLevelData != null) { + worldLevelOverride = worldLevelData.getMonsterLevel(); + } + + // Todo + List toAdd = new ArrayList<>(); + List toRemove = new ArrayList<>(); + var spawnedEntities = this.getSpawnedEntities(); + for (SpawnDataEntry entry : visible) { + // If spawn entry is in our view and hasnt been spawned/killed yet, we should spawn it + if (!spawnedEntities.contains(entry) && !this.getDeadSpawnedEntities().contains(entry)) { + // Entity object holder + GameEntity entity = null; + + // Check if spawn entry is monster or gadget + if (entry.getMonsterId() > 0) { + MonsterData data = GameData.getMonsterDataMap().get(entry.getMonsterId()); + if (data == null) continue; + + int level = this.getEntityLevel(entry.getLevel(), worldLevelOverride); + + EntityMonster monster = new EntityMonster(this, data, entry.getPos(), level); + monster.getRotation().set(entry.getRot()); + monster.setGroupId(entry.getGroup().getGroupId()); + monster.setPoseId(entry.getPoseId()); + monster.setConfigId(entry.getConfigId()); + monster.setSpawnEntry(entry); + + entity = monster; + } else if (entry.getGadgetId() > 0) { + EntityGadget gadget = + new EntityGadget(this, entry.getGadgetId(), entry.getPos(), entry.getRot()); + gadget.setGroupId(entry.getGroup().getGroupId()); + gadget.setConfigId(entry.getConfigId()); + gadget.setSpawnEntry(entry); + int state = entry.getGadgetState(); + if (state > 0) { + gadget.setState(state); + } + gadget.buildContent(); + + gadget.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, Float.POSITIVE_INFINITY); + gadget.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, Float.POSITIVE_INFINITY); + gadget.setFightProperty(FightProperty.FIGHT_PROP_MAX_HP, Float.POSITIVE_INFINITY); + + entity = gadget; + blossomManager.initBlossom(gadget); + } + + if (entity == null) continue; + + // Add to scene and spawned list + toAdd.add(entity); + spawnedEntities.add(entry); + } + } + + for (GameEntity entity : this.getEntities().values()) { + var spawnEntry = entity.getSpawnEntry(); + if (spawnEntry != null && !visible.contains(spawnEntry)) { + toRemove.add(entity); + spawnedEntities.remove(spawnEntry); + } + } + + if (toAdd.size() > 0) { + toAdd.forEach(this::addEntityDirectly); + this.broadcastPacket(new PacketSceneEntityAppearNotify(toAdd, VisionType.VISION_TYPE_BORN)); + } + + if (toRemove.size() > 0) { + toRemove.forEach(this::removeEntityDirectly); + this.broadcastPacket( + new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); + blossomManager.recycleGadgetEntity(toRemove); + } + } + + public List getPlayerActiveBlocks(Player player) { + // consider the borders' entities of blocks, so we check if contains by index + return SceneIndexManager.queryNeighbors( + getScriptManager().getBlocksIndex(), + player.getPosition().toXZDoubleArray(), + Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); + } + + public Set getPlayerActiveGroups(Player player) { + // consider the borders' entities of blocks, so we check if contains by index + Position playerPosition = player.getPosition(); + Set activeGroups = new HashSet<>(); + for (int i = 0; i < 4; i++) { + Grid grid = getScriptManager().getGroupGrids().get(i); + + activeGroups.addAll(grid.getNearbyGroups(i, playerPosition)); + } + + return activeGroups; + } + + public synchronized boolean loadBlock(SceneBlock block) { + if (this.loadedBlocks.contains(block)) return false; + + this.onLoadBlock(block, this.players); + this.loadedBlocks.add(block); + return true; + } + + public synchronized void checkGroups() { + Set visible = + this.players.stream() + .map(player -> this.getPlayerActiveGroups(player)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + + Iterator it = this.loadedGroups.iterator(); + while (it.hasNext()) { + SceneGroup group = it.next(); + if (!visible.contains(group.id) && !group.dynamic_load) + unloadGroup(scriptManager.getBlocks().get(group.block_id), group.id); + } + + List toLoad = + visible.stream() + .filter(g -> this.loadedGroups.stream().filter(gr -> gr.id == g).count() == 0) + .map( + g -> { + for (var b : scriptManager.getBlocks().values()) { + loadBlock(b); + SceneGroup group = b.groups.getOrDefault(g, null); + if (group != null && !group.dynamic_load) return group; + } + + return null; + }) + .filter(Objects::nonNull) + .toList(); + + this.onLoadGroup(toLoad); + if (!toLoad.isEmpty()) this.onRegisterGroups(); + } + + public void onLoadBlock(SceneBlock block, List players) { + this.getScriptManager().loadBlockFromScript(block); + scriptManager.getLoadedGroupSetPerBlock().put(block.id, new HashSet<>()); + + Grasscutter.getLogger().info("Scene {} Block {} loaded.", this.getId(), block.id); + } + + public int loadDynamicGroup(int group_id) { + SceneGroup group = getScriptManager().getGroupById(group_id); + if (group == null || getScriptManager().getGroupInstanceById(group_id) != null) + return -1; // Group not found or already instanced + + this.onLoadGroup(new ArrayList<>(List.of(group))); + + if (GameData.getGroupReplacements().containsKey(group_id)) onRegisterGroups(); + + if (group.init_config == null) return -1; + return group.init_config.suite; + } + + public boolean unregisterDynamicGroup(int groupId) { + var group = getScriptManager().getGroupById(groupId); + if (group == null) return false; + + var block = getScriptManager().getBlocks().get(group.block_id); + this.unloadGroup(block, groupId); + return true; + } + + public void onRegisterGroups() { + var sceneGroups = this.loadedGroups; + var sceneGroupMap = + sceneGroups.stream().collect(Collectors.toMap(item -> item.id, item -> item)); + var sceneGroupsIds = sceneGroups.stream().map(group -> group.id).toList(); + var dynamicGroups = + sceneGroups.stream().filter(group -> group.dynamic_load).map(group -> group.id).toList(); + + // Create the graph + var nodes = new ArrayList(); + var groupList = new ArrayList(); + GameData.getGroupReplacements().values().stream() + .filter(replacement -> dynamicGroups.contains(replacement.id)) + .forEach( + replacement -> { + Grasscutter.getLogger().info("Graph ordering replacement {}", replacement); + replacement.replace_groups.forEach( + group -> { + nodes.add(new KahnsSort.Node(replacement.id, group)); + if (!groupList.contains(group)) groupList.add(group); + }); + + if (!groupList.contains(replacement.id)) groupList.add(replacement.id); + }); + + KahnsSort.Graph graph = new KahnsSort.Graph(nodes, groupList); + List dynamicGroupsOrdered = KahnsSort.doSort(graph); + + // Now we can start unloading and loading groups :D + dynamicGroupsOrdered.forEach( + group -> { + if (GameData.getGroupReplacements().containsKey((int) group)) { // isGroupJoinReplacement + var data = GameData.getGroupReplacements().get((int) group); + var sceneGroupReplacement = + this.loadedGroups.stream().filter(g -> g.id == group).findFirst().orElseThrow(); + if (sceneGroupReplacement.is_replaceable != null) { + var it = data.replace_groups.iterator(); + while (it.hasNext()) { + var replace_group = it.next(); + if (!sceneGroupsIds.contains(replace_group)) continue; + + // Check if we can replace this group + SceneGroup sceneGroup = sceneGroupMap.get(replace_group); + if (sceneGroup != null + && sceneGroup.is_replaceable != null + && ((sceneGroup.is_replaceable.value + && sceneGroup.is_replaceable.version + <= sceneGroupReplacement.is_replaceable.version) + || sceneGroup.is_replaceable.new_bin_only)) { + this.unloadGroup( + scriptManager.getBlocks().get(sceneGroup.block_id), replace_group); + it.remove(); + Grasscutter.getLogger().info("Graph ordering: unloaded {}", replace_group); + } + } + } + } + }); + } + + public void loadTriggerFromGroup(SceneGroup group, String triggerName) { + // Load triggers and regions + this.getScriptManager() + .registerTrigger( + group.triggers.values().stream() + .filter(p -> p.getName().contains(triggerName)) + .toList()); + group.regions.values().stream() + .filter(q -> q.config_id == Integer.parseInt(triggerName.substring(13))) + .map(region -> new EntityRegion(this, region)) + .forEach(getScriptManager()::registerRegion); + } + + public void onLoadGroup(List groups) { + if (groups == null || groups.isEmpty()) { + return; + } + + for (var group : groups) { + if (this.loadedGroups.contains(group)) continue; + + // We load the script files for the groups here + this.getScriptManager().loadGroupFromScript(group); + if (!this.scriptManager.getLoadedGroupSetPerBlock().containsKey(group.block_id)) + this.onLoadBlock(scriptManager.getBlocks().get(group.block_id), players); + this.scriptManager.getLoadedGroupSetPerBlock().get(group.block_id).add(group); + } + + // Spawn gadgets AFTER triggers are added + // TODO + var entities = new ArrayList(); + for (var group : groups) { + if (this.loadedGroups.contains(group)) continue; + + if (group.init_config == null) { + continue; + } + + var groupInstance = this.getScriptManager().getGroupInstanceById(group.id); + var cachedInstance = this.getScriptManager().getCachedGroupInstanceById(group.id); + if (cachedInstance != null) { + cachedInstance.setLuaGroup(group); + groupInstance = cachedInstance; + } + + // Load garbages + var garbageGadgets = group.getGarbageGadgets(); + + if (garbageGadgets != null) { + entities.addAll( + garbageGadgets.stream() + .map(g -> scriptManager.createGadget(group.id, group.block_id, g)) + .filter(Objects::nonNull) + .toList()); + } + + // Load suites + // int suite = group.findInitSuiteIndex(0); + this.getScriptManager() + .refreshGroup(groupInstance, 0, false); // This is what the official server does + + this.loadedGroups.add(group); + } + + this.scriptManager.meetEntities(entities); + groups.forEach( + g -> scriptManager.callEvent(new ScriptArgs(g.id, EventType.EVENT_GROUP_LOAD, g.id))); + + Grasscutter.getLogger().info("Scene {} loaded {} group(s)", this.getId(), groups.size()); + } + + public void unloadGroup(SceneBlock block, int group_id) { + List toRemove = + this.getEntities().values().stream() + .filter(e -> e != null && (e.getBlockId() == block.id && e.getGroupId() == group_id)) + .toList(); + + if (toRemove.size() > 0) { + toRemove.forEach(this::removeEntityDirectly); + this.broadcastPacket( + new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); + } + + var group = block.groups.get(group_id); + if (group.triggers != null) { + group.triggers.values().forEach(getScriptManager()::deregisterTrigger); + } + if (group.regions != null) { + group.regions.values().forEach(getScriptManager()::deregisterRegion); + } + + scriptManager.getLoadedGroupSetPerBlock().get(block.id).remove(group); + this.loadedGroups.remove(group); + + if (this.scriptManager.getLoadedGroupSetPerBlock().get(block.id).isEmpty()) { + this.scriptManager.getLoadedGroupSetPerBlock().remove(block.id); + Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id); + } + + this.broadcastPacket(new PacketGroupUnloadNotify(List.of(group_id))); + this.scriptManager.unregisterGroup(group); + } + + // Gadgets + + public void onPlayerCreateGadget(EntityClientGadget gadget) { + // Directly add + this.addEntityDirectly(gadget); + + // Add to owner's gadget list + gadget.getOwner().getTeamManager().getGadgets().add(gadget); + + // Optimization + if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { + return; + } + + this.broadcastPacketToOthers(gadget.getOwner(), new PacketSceneEntityAppearNotify(gadget)); + } + + public void onPlayerDestroyGadget(int entityId) { + GameEntity entity = getEntities().get(entityId); + + if (!(entity instanceof EntityClientGadget gadget)) { + return; + } + + // Get and remove entity + this.removeEntityDirectly(gadget); + + // Remove from owner's gadget list + gadget.getOwner().getTeamManager().getGadgets().remove(gadget); + + // Optimization + if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == gadget.getOwner()) { + return; + } + + this.broadcastPacketToOthers( + gadget.getOwner(), + new PacketSceneEntityDisappearNotify(gadget, VisionType.VISION_TYPE_DIE)); + } + + // Broadcasting + + public void broadcastPacket(BasePacket packet) { + // Send to all players - might have to check if player has been sent data packets + for (Player player : this.getPlayers()) { + player.getSession().send(packet); + } + } + + public void broadcastPacketToOthers(Player excludedPlayer, BasePacket packet) { + // Optimization + if (this.getPlayerCount() == 1 && this.getPlayers().get(0) == excludedPlayer) { + return; + } + // Send to all players - might have to check if player has been sent data packets + for (Player player : this.getPlayers()) { + if (player == excludedPlayer) { + continue; + } + // Send + player.getSession().send(packet); + } + } + + public void addItemEntity(int itemId, int amount, GameEntity bornForm) { + ItemData itemData = GameData.getItemDataMap().get(itemId); + if (itemData == null) { + return; + } + if (itemData.isEquip()) { + float range = (1.5f + (.05f * amount)); + for (int i = 0; i < amount; i++) { + Position pos = bornForm.getPosition().nearby2d(range).addZ(.9f); // Why Z? + EntityItem entity = new EntityItem(this, null, itemData, pos, 1); + addEntity(entity); + } + } else { + EntityItem entity = + new EntityItem( + this, null, itemData, bornForm.getPosition().clone().addZ(.9f), amount); // Why Z? + addEntity(entity); + } + } + + public void loadNpcForPlayerEnter(Player player) { + this.npcBornEntrySet.addAll(loadNpcForPlayer(player)); + } + + private List loadNpcForPlayer(Player player) { + var pos = player.getPosition(); + var data = GameData.getSceneNpcBornData().get(getId()); + if (data == null) { + return List.of(); + } + + var npcList = + SceneIndexManager.queryNeighbors( + data.getIndex(), + pos.toDoubleArray(), + Grasscutter.getConfig().server.game.loadEntitiesForPlayerRange); + + var sceneNpcBornEntries = + npcList.stream().filter(i -> !this.npcBornEntrySet.contains(i)).toList(); + + if (sceneNpcBornEntries.size() > 0) { + this.broadcastPacket(new PacketGroupSuiteNotify(sceneNpcBornEntries)); + Grasscutter.getLogger().debug("Loaded Npc Group Suite {}", sceneNpcBornEntries); + } + return npcList; + } + + public void loadGroupForQuest(List sceneGroupSuite) { + if (!scriptManager.isInit()) { + return; + } + + sceneGroupSuite.forEach( + i -> { + var group = scriptManager.getGroupById(i.getGroup()); + if (group == null) return; + + var groupInstance = scriptManager.getGroupInstanceById(i.getGroup()); + var suite = group.getSuiteByIndex(i.getSuite()); + if (suite == null || groupInstance == null) { + return; + } + + scriptManager.refreshGroup(groupInstance, i.getSuite(), false); + }); + } + + /** + * Adds an unlocked force to the scene. + * + * @param force The ID of the force to unlock. + */ + public void unlockForce(int force) { + this.unlockedForces.add(force); + this.broadcastPacket(new PacketSceneForceUnlockNotify(force, true)); + } + + /** + * Removes an unlocked force from the scene. + * + * @param force The ID of the force to lock. + */ + public void lockForce(int force) { + this.unlockedForces.remove(force); + this.broadcastPacket(new PacketSceneForceLockNotify(force)); + } + + public void selectWorktopOptionWith(SelectWorktopOptionReqOuterClass.SelectWorktopOptionReq req) { + GameEntity entity = getEntityById(req.getGadgetEntityId()); + if (entity == null) { + return; + } + // Handle + if (entity instanceof EntityGadget gadget) { + if (gadget.getContent() instanceof GadgetWorktop worktop) { + boolean shouldDelete = worktop.onSelectWorktopOption(req); + if (shouldDelete) { + entity.getScene().removeEntity(entity, VisionType.VISION_TYPE_REMOVE); + } + } + } + } + + public void saveGroups() { + this.getScriptManager().getCachedGroupInstances().values().forEach(SceneGroupInstance::save); + } +} diff --git a/src/main/java/emu/grasscutter/game/world/World.java b/src/main/java/emu/grasscutter/game/world/World.java index c1e0a85c6..3bec2cad2 100644 --- a/src/main/java/emu/grasscutter/game/world/World.java +++ b/src/main/java/emu/grasscutter/game/world/World.java @@ -1,502 +1,513 @@ -package emu.grasscutter.game.world; - -import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT; - -import emu.grasscutter.data.GameData; -import emu.grasscutter.data.excels.dungeon.DungeonData; -import emu.grasscutter.game.player.Player; -import emu.grasscutter.game.player.Player.SceneLoadState; -import emu.grasscutter.game.props.EnterReason; -import emu.grasscutter.game.props.EntityIdType; -import emu.grasscutter.game.props.SceneType; -import emu.grasscutter.game.quest.enums.QuestContent; -import emu.grasscutter.game.world.data.TeleportProperties; -import emu.grasscutter.net.packet.BasePacket; -import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType; -import emu.grasscutter.scripts.data.SceneConfig; -import emu.grasscutter.server.event.player.PlayerTeleportEvent; -import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType; -import emu.grasscutter.server.game.GameServer; -import emu.grasscutter.server.packet.send.*; -import emu.grasscutter.utils.ConversionUtils; -import emu.grasscutter.utils.Position; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.stream.Collectors; -import lombok.Getter; -import lombok.val; - -public class World implements Iterable { - @Getter private final GameServer server; - @Getter private final Player host; - @Getter private final List players; - @Getter private final Int2ObjectMap scenes; - - @Getter private final int levelEntityId; - private int nextEntityId = 0; - private int nextPeerId = 0; - private int worldLevel; - - private final boolean isMultiplayer; - - private long lastUpdateTime; - @Getter private int tickCount = 0; - @Getter private boolean isPaused = false; - @Getter private long currentWorldTime = 0; - - public World(Player player) { - this(player, false); - } - - public World(Player player, boolean isMultiplayer) { - this.host = player; - this.server = player.getServer(); - this.players = Collections.synchronizedList(new ArrayList<>()); - this.scenes = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); - - this.levelEntityId = this.getNextEntityId(EntityIdType.MPLEVEL); - this.worldLevel = player.getWorldLevel(); - this.isMultiplayer = isMultiplayer; - - this.lastUpdateTime = System.currentTimeMillis(); - this.currentWorldTime = host.getPlayerGameTime(); - - this.host.getServer().registerWorld(this); - } - - /** - * Gets the peer ID of the world's host. - * - * @return The peer ID of the world's host. 0 if the host is null. - */ - public int getHostPeerId() { - return this.getHost() == null ? 0 : this.getHost().getPeerId(); - } - - public int getNextPeerId() { - return ++this.nextPeerId; - } - - public int getWorldLevel() { - return worldLevel; - } - - public void setWorldLevel(int worldLevel) { - this.worldLevel = worldLevel; - } - - /** - * Gets an associated scene by ID. Creates a new instance of the scene if it doesn't exist. - * - * @param sceneId The scene ID. - * @return The scene. - */ - public Scene getSceneById(int sceneId) { - // Get scene normally - var scene = this.getScenes().get(sceneId); - if (scene != null) { - return scene; - } - - // Create scene from scene data if it doesn't exist - var sceneData = GameData.getSceneDataMap().get(sceneId); - if (sceneData != null) { - scene = new Scene(this, sceneData); - this.registerScene(scene); - return scene; - } - - return null; - } - - public int getPlayerCount() { - return this.getPlayers().size(); - } - - public boolean isMultiplayer() { - return isMultiplayer; - } - - /** - * Gets the next entity ID for the specified entity type. - * - * @param idType The entity type. - * @return The next entity ID. - */ - public int getNextEntityId(EntityIdType idType) { - return (idType.getId() << 24) + ++this.nextEntityId; - } - - public synchronized void addPlayer(Player player) { - // Check if player already in - if (this.getPlayers().contains(player)) { - return; - } - - // Remove player from prev world - if (player.getWorld() != null) { - player.getWorld().removePlayer(player); - } - - // Register - player.setWorld(this); - this.getPlayers().add(player); - - // Set player variables - player.setPeerId(this.getNextPeerId()); - player.getTeamManager().setEntityId(this.getNextEntityId(EntityIdType.TEAM)); - - // Copy main team to multiplayer team - if (this.isMultiplayer()) { - player - .getTeamManager() - .getMpTeam() - .copyFrom( - player.getTeamManager().getCurrentSinglePlayerTeamInfo(), - player.getTeamManager().getMaxTeamSize()); - player.getTeamManager().setCurrentCharacterIndex(0); - } - - // Add to scene - Scene scene = this.getSceneById(player.getSceneId()); - scene.addPlayer(player); - - // Info packet for other players - if (this.getPlayers().size() > 1) { - this.updatePlayerInfos(player); - } - } - - public synchronized void removePlayer(Player player) { - // Remove team entities - player.sendPacket( - new PacketDelTeamEntityNotify( - player.getSceneId(), - this.getPlayers().stream() - .map(p -> p.getTeamManager().getEntityId()) - .collect(Collectors.toList()))); - - // Deregister - this.getPlayers().remove(player); - player.setWorld(null); - - // Remove from scene - Scene scene = this.getSceneById(player.getSceneId()); - scene.removePlayer(player); - - // Info packet for other players - if (this.getPlayers().size() > 0) { - this.updatePlayerInfos(player); - } - - // Disband world if host leaves - if (this.getHost() == player) { - List kicked = new ArrayList<>(this.getPlayers()); - for (Player victim : kicked) { - World world = new World(victim); - world.addPlayer(victim); - - victim.sendPacket( - new PacketPlayerEnterSceneNotify( - victim, - EnterType.ENTER_TYPE_SELF, - EnterReason.TeamKick, - victim.getSceneId(), - victim.getPosition())); - } - } - } - - public void registerScene(Scene scene) { - this.getScenes().put(scene.getId(), scene); - } - - public void deregisterScene(Scene scene) { - this.getScenes().remove(scene.getId()); - } - - public boolean transferPlayerToScene(Player player, int sceneId, Position pos) { - return this.transferPlayerToScene(player, sceneId, TeleportType.INTERNAL, null, pos); - } - - public boolean transferPlayerToScene( - Player player, int sceneId, TeleportType teleportType, Position pos) { - return this.transferPlayerToScene(player, sceneId, teleportType, null, pos); - } - - public boolean transferPlayerToScene(Player player, int sceneId, DungeonData data) { - return this.transferPlayerToScene(player, sceneId, TeleportType.DUNGEON, data, null); - } - - public boolean transferPlayerToScene( - Player player, - int sceneId, - TeleportType teleportType, - DungeonData dungeonData, - Position teleportTo) { - EnterReason enterReason = - switch (teleportType) { - // shouldn't affect the teleportation, but its clearer when inspecting the packets - // TODO add more conditions for different reason. - case INTERNAL -> EnterReason.TransPoint; - case WAYPOINT -> EnterReason.TransPoint; - case MAP -> EnterReason.TransPoint; - case COMMAND -> EnterReason.Gm; - case SCRIPT -> EnterReason.Lua; - case CLIENT -> EnterReason.ClientTransmit; - case DUNGEON -> EnterReason.DungeonEnter; - default -> EnterReason.None; - }; - return transferPlayerToScene( - player, sceneId, teleportType, enterReason, dungeonData, teleportTo); - } - - public boolean transferPlayerToScene( - Player player, - int sceneId, - TeleportType teleportType, - EnterReason enterReason, - DungeonData dungeonData, - Position teleportTo) { - // Get enter types - val teleportProps = - TeleportProperties.builder() - .sceneId(sceneId) - .teleportType(teleportType) - .enterReason(enterReason) - .teleportTo(teleportTo) - .enterType(EnterType.ENTER_TYPE_JUMP); - - val sceneData = GameData.getSceneDataMap().get(sceneId); - if (dungeonData != null) { - teleportProps.enterType(EnterType.ENTER_TYPE_DUNGEON).enterReason(EnterReason.DungeonEnter); - } else if (player.getSceneId() == sceneId) { - teleportProps.enterType(EnterType.ENTER_TYPE_GOTO); - } else if (sceneData != null && sceneData.getSceneType() == SceneType.SCENE_HOME_WORLD) { - // Home - teleportProps.enterType(EnterType.ENTER_TYPE_SELF_HOME).enterReason(EnterReason.EnterHome); - } - return transferPlayerToScene(player, teleportProps.build()); - } - - public boolean transferPlayerToScene(Player player, TeleportProperties teleportProperties) { - // Call player teleport event. - PlayerTeleportEvent event = - new PlayerTeleportEvent(player, teleportProperties, player.getPosition()); - // Call event & check if it was canceled. - event.call(); - if (event.isCanceled()) { - return false; // Teleport was canceled. - } - - if (GameData.getSceneDataMap().get(teleportProperties.getSceneId()) == null) { - return false; - } - - Scene oldScene = null; - if (player.getScene() != null) { - oldScene = player.getScene(); - - // Don't deregister scenes if the player is going to tp back into them - if (oldScene.getId() == teleportProperties.getSceneId()) { - oldScene.setDontDestroyWhenEmpty(true); - } - - oldScene.removePlayer(player); - } - - var newScene = this.getSceneById(teleportProperties.getSceneId()); - newScene.addPlayer(player); - player.getTeamManager().applyAbilities(newScene); - - SceneConfig config = newScene.getScriptManager().getConfig(); - if (teleportProperties.getTeleportTo() == null && config != null) { - if (config.born_pos != null) { - teleportProperties.setTeleportTo(newScene.getScriptManager().getConfig().born_pos); - } - if (config.born_rot != null) { - teleportProperties.setTeleportRot(config.born_rot); - } - } - - // Set player position and rotation - if (teleportProperties.getTeleportTo() != null) { - player.getPosition().set(teleportProperties.getTeleportTo()); - } - if (teleportProperties.getTeleportRot() != null) { - player.getRotation().set(teleportProperties.getTeleportRot()); - } - - if (oldScene != null && newScene != oldScene) { - newScene.setPrevScene(oldScene.getId()); - oldScene.setDontDestroyWhenEmpty(false); - } - - // Teleport packet - player.sendPacket(new PacketPlayerEnterSceneNotify(player, teleportProperties)); - - if (teleportProperties.getTeleportType() != TeleportType.INTERNAL - && teleportProperties.getTeleportType() != SCRIPT) { - player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_ANY_MANUAL_TRANSPORT); - } - - return true; - } - - private void updatePlayerInfos(Player paramPlayer) { - for (Player player : this.getPlayers()) { - // Dont send packets if player is logging in and filter out joining player - if (!player.hasSentLoginPackets() || player == paramPlayer) { - continue; - } - - // Update team of all players since max players has been changed - Probably not the best way - // to do it - if (this.isMultiplayer()) { - player - .getTeamManager() - .getMpTeam() - .copyFrom( - player.getTeamManager().getMpTeam(), player.getTeamManager().getMaxTeamSize()); - player.getTeamManager().updateTeamEntities(null); - } - - // Dont send packets if player is loading into the scene - if (player.getSceneLoadState().getValue() < SceneLoadState.INIT.getValue()) { - // World player info packets - player.getSession().send(new PacketWorldPlayerInfoNotify(this)); - player.getSession().send(new PacketScenePlayerInfoNotify(this)); - player.getSession().send(new PacketWorldPlayerRTTNotify(this)); - - // Team packets - player.getSession().send(new PacketSyncTeamEntityNotify(player)); - player.getSession().send(new PacketSyncScenePlayTeamEntityNotify(player)); - } - } - } - - public void broadcastPacket(BasePacket packet) { - // Send to all players - might have to check if player has been sent data packets - for (Player player : this.getPlayers()) { - player.getSession().send(packet); - } - } - - /** - * Invoked every game tick. - * - * @return True if the world should be removed. - */ - public boolean onTick() { - // Check if there are players in this world. - if (this.getPlayerCount() == 0) return true; - // Tick all associated scenes. - this.getScenes().forEach((k, scene) -> scene.onTick()); - - // sync time every 10 seconds - if (this.tickCount % 10 == 0) { - this.getPlayers().forEach(p -> p.sendPacket(new PacketPlayerGameTimeNotify(p))); - } - - // store updated world time every 60 seconds. (in-game hour) - if (this.tickCount % 60 == 0) { - this.getHost().updatePlayerGameTime(currentWorldTime); - } - - this.tickCount++; - return false; - } - - public void close() {} - - /** Returns the in-game world time in real milliseconds. */ - public long getWorldTime() { - if (!this.isPaused) { - var newUpdateTime = System.currentTimeMillis(); - this.currentWorldTime += (newUpdateTime - lastUpdateTime); - this.lastUpdateTime = newUpdateTime; - } - - return currentWorldTime; - } - - /** Returns the current in game days world time in ingame minutes (0-1439) */ - public int getGameTime() { - return (int) (getTotalGameTimeMinutes() % 1440); - } - - /** Returns the current in game days world time in ingame hours (0-23) */ - public int getGameTimeHours() { - return this.getGameTime() / 60; - } - - /** Returns the total number of in game days that got completed since the beginning of the game */ - public long getTotalGameTimeDays() { - return ConversionUtils.gameTimeToDays(getTotalGameTimeMinutes()); - } - - /** - * Returns the total number of in game hours that got completed since the beginning of the game - */ - public long getTotalGameTimeHours() { - return ConversionUtils.gameTimeToHours(getTotalGameTimeMinutes()); - } - - /** Returns the elapsed in-game minutes since the creation of the world. */ - public long getTotalGameTimeMinutes() { - return this.getWorldTime() / 1000; - } - - /** - * Sets the world's pause status. Updates players and scenes accordingly. - * - * @param paused True if the world should be paused. - */ - public void setPaused(boolean paused) { - this.getWorldTime(); // Update the world time. - - // If the world is being un-paused, update the last update time. - if (this.isPaused != paused && !paused) { - this.lastUpdateTime = System.currentTimeMillis(); - } - - this.isPaused = paused; - this.getPlayers().forEach(player -> player.setPaused(paused)); - this.getScenes().forEach((key, scene) -> scene.setPaused(paused)); - } - - /** - * Changes the time of the world. - * - * @param time The new time in minutes. - * @param days The number of days to add. - */ - public void changeTime(int time, int days) { - // Calculate time differences. - var currentTime = this.getGameTime(); - var diff = time - currentTime; - if (diff < 0) diff = 1440 + diff; - - // Update the world time. - this.currentWorldTime += days * 1440 * 1000L + diff * 1000L; - - // Update all players. - this.host.updatePlayerGameTime(currentWorldTime); - this.players.forEach( - player -> - player - .getQuestManager() - .queueEvent( - QuestContent.QUEST_CONTENT_GAME_TIME_TICK, this.getGameTimeHours(), days)); - } - - @Override - public Iterator iterator() { - return this.getPlayers().iterator(); - } -} +package emu.grasscutter.game.world; + +import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.excels.dungeon.DungeonData; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.player.Player.SceneLoadState; +import emu.grasscutter.game.props.EnterReason; +import emu.grasscutter.game.props.EntityIdType; +import emu.grasscutter.game.props.SceneType; +import emu.grasscutter.game.quest.enums.QuestContent; +import emu.grasscutter.game.world.data.TeleportProperties; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.proto.EnterTypeOuterClass.EnterType; +import emu.grasscutter.scripts.data.SceneConfig; +import emu.grasscutter.server.event.player.PlayerTeleportEvent; +import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType; +import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.server.packet.send.*; +import emu.grasscutter.utils.ConversionUtils; +import emu.grasscutter.utils.Position; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.val; + +public class World implements Iterable { + @Getter private final GameServer server; + @Getter private final Player host; + @Getter private final List players; + @Getter private final Int2ObjectMap scenes; + + @Getter private final int levelEntityId; + private int nextEntityId = 0; + private int nextPeerId = 0; + private int worldLevel; + + private final boolean isMultiplayer; + + private long lastUpdateTime; + @Getter private int tickCount = 0; + @Getter private boolean isPaused = false; + @Getter private long currentWorldTime = 0; + + public World(Player player) { + this(player, false); + } + + public World(Player player, boolean isMultiplayer) { + this.host = player; + this.server = player.getServer(); + this.players = Collections.synchronizedList(new ArrayList<>()); + this.scenes = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); + + this.levelEntityId = this.getNextEntityId(EntityIdType.MPLEVEL); + this.worldLevel = player.getWorldLevel(); + this.isMultiplayer = isMultiplayer; + + this.lastUpdateTime = System.currentTimeMillis(); + this.currentWorldTime = host.getPlayerGameTime(); + + this.host.getServer().registerWorld(this); + } + + /** + * Gets the peer ID of the world's host. + * + * @return The peer ID of the world's host. 0 if the host is null. + */ + public int getHostPeerId() { + return this.getHost() == null ? 0 : this.getHost().getPeerId(); + } + + public int getNextPeerId() { + return ++this.nextPeerId; + } + + public int getWorldLevel() { + return worldLevel; + } + + public void setWorldLevel(int worldLevel) { + this.worldLevel = worldLevel; + } + + /** + * Gets an associated scene by ID. Creates a new instance of the scene if it doesn't exist. + * + * @param sceneId The scene ID. + * @return The scene. + */ + public Scene getSceneById(int sceneId) { + // Get scene normally + var scene = this.getScenes().get(sceneId); + if (scene != null) { + return scene; + } + + // Create scene from scene data if it doesn't exist + var sceneData = GameData.getSceneDataMap().get(sceneId); + if (sceneData != null) { + scene = new Scene(this, sceneData); + this.registerScene(scene); + return scene; + } + + return null; + } + + public int getPlayerCount() { + return this.getPlayers().size(); + } + + public boolean isMultiplayer() { + return isMultiplayer; + } + + /** + * Gets the next entity ID for the specified entity type. + * + * @param idType The entity type. + * @return The next entity ID. + */ + public int getNextEntityId(EntityIdType idType) { + return (idType.getId() << 24) + ++this.nextEntityId; + } + + public synchronized void addPlayer(Player player) { + // Check if player already in + if (this.getPlayers().contains(player)) { + return; + } + + // Remove player from prev world + if (player.getWorld() != null) { + player.getWorld().removePlayer(player); + } + + // Register + player.setWorld(this); + this.getPlayers().add(player); + + // Set player variables + player.setPeerId(this.getNextPeerId()); + player.getTeamManager().setEntityId(this.getNextEntityId(EntityIdType.TEAM)); + + // Copy main team to multiplayer team + if (this.isMultiplayer()) { + player + .getTeamManager() + .getMpTeam() + .copyFrom( + player.getTeamManager().getCurrentSinglePlayerTeamInfo(), + player.getTeamManager().getMaxTeamSize()); + player.getTeamManager().setCurrentCharacterIndex(0); + } + + // Add to scene + Scene scene = this.getSceneById(player.getSceneId()); + scene.addPlayer(player); + + // Info packet for other players + if (this.getPlayers().size() > 1) { + this.updatePlayerInfos(player); + } + } + + public synchronized void removePlayer(Player player) { + // Remove team entities + player.sendPacket( + new PacketDelTeamEntityNotify( + player.getSceneId(), + this.getPlayers().stream() + .map(p -> p.getTeamManager().getEntityId()) + .collect(Collectors.toList()))); + + // Deregister + this.getPlayers().remove(player); + player.setWorld(null); + + // Remove from scene + Scene scene = this.getSceneById(player.getSceneId()); + scene.removePlayer(player); + + // Info packet for other players + if (this.getPlayers().size() > 0) { + this.updatePlayerInfos(player); + } + + // Disband world if host leaves + if (this.getHost() == player) { + List kicked = new ArrayList<>(this.getPlayers()); + for (Player victim : kicked) { + World world = new World(victim); + world.addPlayer(victim); + + victim.sendPacket( + new PacketPlayerEnterSceneNotify( + victim, + EnterType.ENTER_TYPE_SELF, + EnterReason.TeamKick, + victim.getSceneId(), + victim.getPosition())); + } + } + } + + public void registerScene(Scene scene) { + this.getScenes().put(scene.getId(), scene); + } + + public void deregisterScene(Scene scene) { + scene.saveGroups(); + this.getScenes().remove(scene.getId()); + } + + public void save() { + this.getScenes().values().forEach(Scene::saveGroups); + } + + public boolean transferPlayerToScene(Player player, int sceneId, Position pos) { + return this.transferPlayerToScene(player, sceneId, TeleportType.INTERNAL, null, pos); + } + + public boolean transferPlayerToScene( + Player player, int sceneId, TeleportType teleportType, Position pos) { + return this.transferPlayerToScene(player, sceneId, teleportType, null, pos); + } + + public boolean transferPlayerToScene(Player player, int sceneId, DungeonData data) { + return this.transferPlayerToScene(player, sceneId, TeleportType.DUNGEON, data, null); + } + + public boolean transferPlayerToScene( + Player player, + int sceneId, + TeleportType teleportType, + DungeonData dungeonData, + Position teleportTo) { + EnterReason enterReason = + switch (teleportType) { + // shouldn't affect the teleportation, but its clearer when inspecting the packets + // TODO add more conditions for different reason. + case INTERNAL -> EnterReason.TransPoint; + case WAYPOINT -> EnterReason.TransPoint; + case MAP -> EnterReason.TransPoint; + case COMMAND -> EnterReason.Gm; + case SCRIPT -> EnterReason.Lua; + case CLIENT -> EnterReason.ClientTransmit; + case DUNGEON -> EnterReason.DungeonEnter; + default -> EnterReason.None; + }; + return transferPlayerToScene( + player, sceneId, teleportType, enterReason, dungeonData, teleportTo); + } + + public boolean transferPlayerToScene( + Player player, + int sceneId, + TeleportType teleportType, + EnterReason enterReason, + DungeonData dungeonData, + Position teleportTo) { + // Get enter types + val teleportProps = + TeleportProperties.builder() + .sceneId(sceneId) + .teleportType(teleportType) + .enterReason(enterReason) + .teleportTo(teleportTo) + .enterType(EnterType.ENTER_TYPE_JUMP); + + val sceneData = GameData.getSceneDataMap().get(sceneId); + if (dungeonData != null) { + teleportProps.enterType(EnterType.ENTER_TYPE_DUNGEON).enterReason(EnterReason.DungeonEnter); + } else if (player.getSceneId() == sceneId) { + teleportProps.enterType(EnterType.ENTER_TYPE_GOTO); + } else if (sceneData != null && sceneData.getSceneType() == SceneType.SCENE_HOME_WORLD) { + // Home + teleportProps.enterType(EnterType.ENTER_TYPE_SELF_HOME).enterReason(EnterReason.EnterHome); + } + return transferPlayerToScene(player, teleportProps.build()); + } + + public boolean transferPlayerToScene(Player player, TeleportProperties teleportProperties) { + // Call player teleport event. + PlayerTeleportEvent event = + new PlayerTeleportEvent(player, teleportProperties, player.getPosition()); + // Call event & check if it was canceled. + event.call(); + if (event.isCanceled()) { + return false; // Teleport was canceled. + } + + if (GameData.getSceneDataMap().get(teleportProperties.getSceneId()) == null) { + return false; + } + + Scene oldScene = null; + + if (player.getScene() != null) { + oldScene = player.getScene(); + + // Don't deregister scenes if the player is going to tp back into them + if (oldScene.getId() == teleportProperties.getSceneId()) { + oldScene.setDontDestroyWhenEmpty(true); + } + + oldScene.removePlayer(player); + } + + var newScene = this.getSceneById(teleportProperties.getSceneId()); + newScene.addPlayer(player); + player.setAvatarsAbilityForScene(newScene); + // Dungeon + // Dungeon system is handling this already + // if(dungeonData!=null){ + // var dungeonManager = new DungeonManager(newScene, dungeonData); + // dungeonManager.startDungeon(); + // } + SceneConfig config = newScene.getScriptManager().getConfig(); + if (teleportProperties.getTeleportTo() == null && config != null) { + if (config.born_pos != null) { + teleportProperties.setTeleportTo(newScene.getScriptManager().getConfig().born_pos); + } + if (config.born_rot != null) { + teleportProperties.setTeleportRot(config.born_rot); + } + } + + // Set player position and rotation + if (teleportProperties.getTeleportTo() != null) { + player.getPosition().set(teleportProperties.getTeleportTo()); + } + if (teleportProperties.getTeleportRot() != null) { + player.getRotation().set(teleportProperties.getTeleportRot()); + } + + if (oldScene != null && newScene != oldScene) { + newScene.setPrevScene(oldScene.getId()); + oldScene.setDontDestroyWhenEmpty(false); + } + + // Teleport packet + player.sendPacket(new PacketPlayerEnterSceneNotify(player, teleportProperties)); + + if (teleportProperties.getTeleportType() != TeleportType.INTERNAL + && teleportProperties.getTeleportType() != SCRIPT) { + player.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_ANY_MANUAL_TRANSPORT); + } + + return true; + } + + private void updatePlayerInfos(Player paramPlayer) { + for (Player player : this.getPlayers()) { + // Dont send packets if player is logging in and filter out joining player + if (!player.hasSentLoginPackets() || player == paramPlayer) { + continue; + } + + // Update team of all players since max players has been changed - Probably not the best way + // to do it + if (this.isMultiplayer()) { + player + .getTeamManager() + .getMpTeam() + .copyFrom( + player.getTeamManager().getMpTeam(), player.getTeamManager().getMaxTeamSize()); + player.getTeamManager().updateTeamEntities(null); + } + + // Dont send packets if player is loading into the scene + if (player.getSceneLoadState().getValue() < SceneLoadState.INIT.getValue()) { + // World player info packets + player.getSession().send(new PacketWorldPlayerInfoNotify(this)); + player.getSession().send(new PacketScenePlayerInfoNotify(this)); + player.getSession().send(new PacketWorldPlayerRTTNotify(this)); + + // Team packets + player.getSession().send(new PacketSyncTeamEntityNotify(player)); + player.getSession().send(new PacketSyncScenePlayTeamEntityNotify(player)); + } + } + } + + public void broadcastPacket(BasePacket packet) { + // Send to all players - might have to check if player has been sent data packets + for (Player player : this.getPlayers()) { + player.getSession().send(packet); + } + } + + /** + * Invoked every game tick. + * + * @return True if the world should be removed. + */ + public boolean onTick() { + // Check if there are players in this world. + if (this.getPlayerCount() == 0) return true; + // Tick all associated scenes. + this.getScenes().forEach((k, scene) -> scene.onTick()); + + // sync time every 10 seconds + if (this.tickCount % 10 == 0) { + this.getPlayers().forEach(p -> p.sendPacket(new PacketPlayerGameTimeNotify(p))); + } + + // store updated world time every 60 seconds. (in-game hour) + if (this.tickCount % 60 == 0) { + this.getHost().updatePlayerGameTime(currentWorldTime); + } + + this.tickCount++; + return false; + } + + public void close() {} + + /** Returns the in-game world time in real milliseconds. */ + public long getWorldTime() { + if (!this.isPaused) { + var newUpdateTime = System.currentTimeMillis(); + this.currentWorldTime += (newUpdateTime - lastUpdateTime); + this.lastUpdateTime = newUpdateTime; + } + + return currentWorldTime; + } + + /** Returns the current in game days world time in ingame minutes (0-1439) */ + public int getGameTime() { + return (int) (getTotalGameTimeMinutes() % 1440); + } + + /** Returns the current in game days world time in ingame hours (0-23) */ + public int getGameTimeHours() { + return this.getGameTime() / 60; + } + + /** Returns the total number of in game days that got completed since the beginning of the game */ + public long getTotalGameTimeDays() { + return ConversionUtils.gameTimeToDays(getTotalGameTimeMinutes()); + } + + /** + * Returns the total number of in game hours that got completed since the beginning of the game + */ + public long getTotalGameTimeHours() { + return ConversionUtils.gameTimeToHours(getTotalGameTimeMinutes()); + } + + /** Returns the elapsed in-game minutes since the creation of the world. */ + public long getTotalGameTimeMinutes() { + return this.getWorldTime() / 1000; + } + + /** + * Sets the world's pause status. Updates players and scenes accordingly. + * + * @param paused True if the world should be paused. + */ + public void setPaused(boolean paused) { + this.getWorldTime(); // Update the world time. + + // If the world is being un-paused, update the last update time. + if (this.isPaused != paused && !paused) { + this.lastUpdateTime = System.currentTimeMillis(); + } + + this.isPaused = paused; + this.getPlayers().forEach(player -> player.setPaused(paused)); + this.getScenes().forEach((key, scene) -> scene.setPaused(paused)); + } + + /** + * Changes the time of the world. + * + * @param time The new time in minutes. + * @param days The number of days to add. + */ + public void changeTime(int time, int days) { + // Calculate time differences. + var currentTime = this.getGameTime(); + var diff = time - currentTime; + if (diff < 0) diff = 1440 + diff; + + // Update the world time. + this.currentWorldTime += days * 1440 * 1000L + diff * 1000L; + + // Update all players. + this.host.updatePlayerGameTime(currentWorldTime); + this.players.forEach( + player -> + player + .getQuestManager() + .queueEvent( + QuestContent.QUEST_CONTENT_GAME_TIME_TICK, this.getGameTimeHours(), days)); + } + + @Override + public Iterator iterator() { + return this.getPlayers().iterator(); + } +}