Copy some files from Grasscutter-Quests

NOT completely finished, nor is it completely done. Protocol issues remain! (including lack of packet IDs)
This commit is contained in:
KingRainbow44
2023-04-01 18:06:30 -04:00
parent 262ee38ded
commit daa51e53b7
381 changed files with 10285 additions and 9150 deletions

View File

@@ -5,7 +5,7 @@ import emu.grasscutter.GameConstants;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.PlayerLevelData;
import emu.grasscutter.data.excels.WeatherData;
import emu.grasscutter.data.excels.world.WeatherData;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.CoopRequest;
@@ -44,6 +44,7 @@ import emu.grasscutter.game.props.ClimateType;
import emu.grasscutter.game.props.PlayerProperty;
import emu.grasscutter.game.props.WatcherTriggerType;
import emu.grasscutter.game.quest.QuestManager;
import emu.grasscutter.game.quest.enums.QuestContent;
import emu.grasscutter.game.quest.enums.QuestTrigger;
import emu.grasscutter.game.shop.ShopLimit;
import emu.grasscutter.game.tower.TowerData;
@@ -93,235 +94,122 @@ import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
@Entity(value = "players", useDiscriminator = false)
public class Player {
private transient final Int2ObjectMap<CoopRequest> coopRequests; // Synchronized getter
@Getter
private transient final Queue<AttackResult> attackResults;
@Getter
private transient final InvokeHandler<CombatInvokeEntry> combatInvokeHandler;
@Getter
private transient final InvokeHandler<AbilityInvokeEntry> abilityInvokeHandler;
@Getter
private transient final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler;
@Id
private int id;
@Indexed(options = @IndexOptions(unique = true))
private String accountId;
@Setter
private transient Account account;
@Getter
@Setter
private transient GameSession session;
@Getter
private String nickname;
@Getter
private String signature;
@Getter
private int headImage;
@Getter
private int nameCardId = 210001;
@Getter
private final Position position;
@Getter
@Setter
private Position prevPos;
@Getter
private final Position rotation;
@Getter
private PlayerBirthday birthday;
@Getter
private PlayerCodex codex;
@Getter
@Setter
private boolean showAvatars;
@Getter
@Setter
private List<Integer> showAvatarList;
@Getter
@Setter
private List<Integer> showNameCardList;
@Getter
private final Map<Integer, Integer> properties;
@Getter
@Setter
private int currentRealmId;
@Getter
@Setter
private int widgetId;
@Getter
@Setter
private int sceneId;
@Getter
@Setter
private int regionId;
@Getter
private int mainCharacterId;
@Setter
private boolean godmode; // Getter is inGodmode
@Id private int id;
@Indexed(options = @IndexOptions(unique = true)) private String accountId;
@Setter private transient Account account;
@Getter @Setter private transient GameSession session;
@Getter private String nickname;
@Getter private String signature;
@Getter private int headImage;
@Getter private int nameCardId = 210001;
@Getter private Position position;
@Getter @Setter private Position prevPos;
@Getter private Position rotation;
@Getter private PlayerBirthday birthday;
@Getter private PlayerCodex codex;
@Getter @Setter private boolean showAvatars;
@Getter @Setter private List<Integer> showAvatarList;
@Getter @Setter private List<Integer> showNameCardList;
@Getter private Map<Integer, Integer> properties;
@Getter @Setter private int currentRealmId;
@Getter @Setter private int widgetId;
@Getter @Setter private int sceneId;
@Getter @Setter private int regionId;
@Getter private int mainCharacterId;
@Setter private boolean godmode; // Getter is inGodmode
private boolean stamina; // Getter is getUnlimitedStamina, Setter is setUnlimitedStamina
@Getter
private final Set<Integer> nameCardList;
@Getter
private final Set<Integer> flyCloakList;
@Getter
private final Set<Integer> costumeList;
@Getter
@Setter
private Set<Integer> rewardedLevels;
@Getter
@Setter
private Set<Integer> homeRewardedLevels;
@Getter
@Setter
private Set<Integer> realmList;
@Getter
@Setter
private Set<Integer> seenRealmList;
@Getter
private final Set<Integer> unlockedForgingBlueprints;
@Getter
private final Set<Integer> unlockedCombines;
@Getter
private final Set<Integer> unlockedFurniture;
@Getter
private final Set<Integer> unlockedFurnitureSuite;
@Getter
private final Map<Long, ExpeditionInfo> expeditionInfo;
@Getter
private final Map<Integer, Integer> unlockedRecipies;
@Getter
private final List<ActiveForgeData> activeForges;
@Getter
private final Map<Integer, ActiveCookCompoundData> activeCookCompounds;
@Getter
private final Map<Integer, Integer> questGlobalVariables;
@Getter
private final Map<Integer, Integer> openStates;
@Getter
@Setter
private Map<Integer, Set<Integer>> unlockedSceneAreas;
@Getter
@Setter
private Map<Integer, Set<Integer>> unlockedScenePoints;
@Getter
@Setter
private List<Integer> chatEmojiIdList;
@Transient
private long nextGuid = 0;
@Transient
@Getter
@Setter
private int peerId;
@Transient
private World world; // Synchronized getter and setter
@Transient
private Scene scene; // Synchronized getter and setter
@Transient
@Getter
private int weatherId = 0;
@Transient
@Getter
private ClimateType climate = ClimateType.CLIMATE_SUNNY;
@Getter private Set<Integer> nameCardList;
@Getter private Set<Integer> flyCloakList;
@Getter private Set<Integer> costumeList;
@Getter @Setter private Set<Integer> rewardedLevels;
@Getter @Setter private Set<Integer> homeRewardedLevels;
@Getter @Setter private Set<Integer> realmList;
@Getter @Setter private Set<Integer> seenRealmList;
@Getter private Set<Integer> unlockedForgingBlueprints;
@Getter private Set<Integer> unlockedCombines;
@Getter private Set<Integer> unlockedFurniture;
@Getter private Set<Integer> unlockedFurnitureSuite;
@Getter private Map<Long, ExpeditionInfo> expeditionInfo;
@Getter private Map<Integer, Integer> unlockedRecipies;
@Getter private List<ActiveForgeData> activeForges;
@Getter private Map<Integer, ActiveCookCompoundData> activeCookCompounds;
@Getter private Map<Integer, Integer> questGlobalVariables;
@Getter private Map<Integer, Integer> openStates;
@Getter @Setter private Map<Integer, Set<Integer>> unlockedSceneAreas;
@Getter @Setter private Map<Integer, Set<Integer>> unlockedScenePoints;
@Getter @Setter private List<Integer> chatEmojiIdList;
@Transient private long nextGuid = 0;
@Transient @Getter @Setter private int peerId;
@Transient private World world; // Synchronized getter and setter
@Transient private Scene scene; // Synchronized getter and setter
@Transient @Getter private int weatherId = 0;
@Transient @Getter private ClimateType climate = ClimateType.CLIMATE_SUNNY;
// Player managers go here
@Getter
private final transient AvatarStorage avatars;
@Getter
private final transient Inventory inventory;
@Getter
private final transient FriendsList friendsList;
@Getter
private final transient MailHandler mailHandler;
@Getter
@Setter
private transient MessageHandler messageHandler;
@Getter
private final transient AbilityManager abilityManager;
@Getter
@Setter
private transient QuestManager questManager;
@Getter
private final transient TowerManager towerManager;
@Getter
private transient SotSManager sotsManager;
@Getter
private transient MapMarksManager mapMarksManager;
@Getter
private transient StaminaManager staminaManager;
@Getter
private transient EnergyManager energyManager;
@Getter
private transient ResinManager resinManager;
@Getter
private transient ForgingManager forgingManager;
@Getter
private transient DeforestationManager deforestationManager;
@Getter
private transient FurnitureManager furnitureManager;
@Getter
private transient BattlePassManager battlePassManager;
@Getter
private transient CookingManager cookingManager;
@Getter
private transient CookingCompoundManager cookingCompoundManager;
@Getter
private transient ActivityManager activityManager;
@Getter
private final transient PlayerBuffManager buffManager;
@Getter
private transient PlayerProgressManager progressManager;
@Getter
private transient SatiationManager satiationManager;
@Getter private transient AvatarStorage avatars;
@Getter private transient Inventory inventory;
@Getter private transient FriendsList friendsList;
@Getter private transient MailHandler mailHandler;
@Getter @Setter private transient MessageHandler messageHandler;
@Getter private transient AbilityManager abilityManager;
@Getter @Setter private transient QuestManager questManager;
@Getter private transient TowerManager towerManager;
@Getter private transient SotSManager sotsManager;
@Getter private transient MapMarksManager mapMarksManager;
@Getter private transient StaminaManager staminaManager;
@Getter private transient EnergyManager energyManager;
@Getter private transient ResinManager resinManager;
@Getter private transient ForgingManager forgingManager;
@Getter private transient DeforestationManager deforestationManager;
@Getter private transient FurnitureManager furnitureManager;
@Getter private transient BattlePassManager battlePassManager;
@Getter private transient CookingManager cookingManager;
@Getter private transient CookingCompoundManager cookingCompoundManager;
@Getter private transient ActivityManager activityManager;
@Getter private transient PlayerBuffManager buffManager;
@Getter private transient PlayerProgressManager progressManager;
@Getter private transient SatiationManager satiationManager;
// Manager data (Save-able to the database)
@Getter
private transient Achievements achievements;
@Getter private transient Achievements achievements;
private PlayerProfile playerProfile; // Getter has null-check
@Getter
private TeamManager teamManager;
@Getter private TeamManager teamManager;
private TowerData towerData; // Getter has null-check
@Getter
private final PlayerGachaInfo gachaInfo;
@Getter private PlayerGachaInfo gachaInfo;
private PlayerCollectionRecords collectionRecordStore; // Getter has null-check
@Getter
private final ArrayList<ShopLimit> shopLimit;
@Getter
private transient GameHome home;
@Setter
private boolean moonCard; // Getter is inMoonCard
@Getter
@Setter
private Date moonCardStartTime;
@Getter
@Setter
private int moonCardDuration;
@Getter
@Setter
private Set<Date> moonCardGetTimes;
@Transient
@Getter
private boolean paused;
@Transient
@Getter
@Setter
private int enterSceneToken;
@Transient
@Getter
@Setter
private SceneLoadState sceneLoadState = SceneLoadState.NONE;
@Transient
private boolean hasSentLoginPackets;
@Transient
private long nextSendPlayerLocTime = 0;
@Getter
@Setter
private long springLastUsed;
@Getter private ArrayList<ShopLimit> shopLimit;
@Getter private transient GameHome home;
@Setter private boolean moonCard; // Getter is inMoonCard
@Getter @Setter private Date moonCardStartTime;
@Getter @Setter private int moonCardDuration;
@Getter @Setter private Set<Date> moonCardGetTimes;
@Transient @Getter private boolean paused;
@Transient @Getter @Setter private int enterSceneToken;
@Transient @Getter @Setter private SceneLoadState sceneLoadState = SceneLoadState.NONE;
@Transient private boolean hasSentLoginPackets;
@Transient private long nextSendPlayerLocTime = 0;
private transient final Int2ObjectMap<CoopRequest> coopRequests; // Synchronized getter
@Getter private transient final Queue<AttackResult> attackResults;
@Getter private transient final InvokeHandler<CombatInvokeEntry> combatInvokeHandler;
@Getter private transient final InvokeHandler<AbilityInvokeEntry> abilityInvokeHandler;
@Getter private transient final InvokeHandler<AbilityInvokeEntry> clientAbilityInitFinishHandler;
@Getter @Setter private long springLastUsed;
private HashMap<String, MapMark> mapMarks; // Getter makes an empty hashmap - maybe do this elsewhere?
@Getter
@Setter
private int nextResinRefresh;
@Getter
@Setter
private int lastDailyReset;
@Getter
private final transient MpSettingType mpSetting = MpSettingType.MP_SETTING_TYPE_ENTER_AFTER_APPLY; // TODO
@Getter @Setter private int nextResinRefresh;
@Getter @Setter private int lastDailyReset;
@Getter private transient MpSettingType mpSetting = MpSettingType.MP_SETTING_TYPE_ENTER_AFTER_APPLY;
@Getter private long playerGameTime = 0;
@Getter private PlayerProgress playerProgress;
@Getter private Set<Integer> activeQuestTimers;
@Deprecated
@SuppressWarnings({"rawtypes", "unchecked"}) // Morphia only!
@@ -432,6 +320,18 @@ public class Player {
this.satiationManager = new SatiationManager(this);
}
/**
* Updates the player's game time if it has changed.
*
* @param gameTime The new game time.
*/
public void updatePlayerGameTime(long gameTime) {
if (this.playerGameTime == gameTime) return;
this.playerGameTime = gameTime;
this.save();
}
public int getUid() {
return id;
}
@@ -717,7 +617,8 @@ public class Player {
// If trigger hasn't been fired yet
if (!Boolean.TRUE.equals(quest.getTriggers().put("ENTER_REGION_" + region.config_id, true))) {
//getSession().send(new PacketServerCondMeetQuestListUpdateNotify());
getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_TRIGGER_FIRE, quest.getTriggerData().get("ENTER_REGION_" + region.config_id).getId(), 0);
getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_TRIGGER_FIRE,
quest.getTriggerData().get("ENTER_REGION_" + region.config_id).getId(), 0);
}
}
});
@@ -730,7 +631,8 @@ public class Player {
// If trigger hasn't been fired yet
if (!Boolean.TRUE.equals(quest.getTriggers().put("LEAVE_REGION_" + region.config_id, true))) {
getSession().send(new PacketServerCondMeetQuestListUpdateNotify());
getQuestManager().triggerEvent(QuestTrigger.QUEST_CONTENT_TRIGGER_FIRE, quest.getTriggerData().get("LEAVE_REGION_" + region.config_id).getId(), 0);
getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_TRIGGER_FIRE,
quest.getTriggerData().get("LEAVE_REGION_" + region.config_id).getId(), 0);
}
}
});
@@ -959,6 +861,11 @@ public class Player {
this.sendPacket(new PacketSetNameCardRsp(nameCardId));
}
/**
* Sends a message to this player.
*
* @param message The message to send.
*/
public void dropMessage(Object message) {
if (this.messageHandler != null) {
this.messageHandler.append(message.toString());

View File

@@ -1,6 +1,5 @@
package emu.grasscutter.game.player;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.data.excels.BuffData;
@@ -17,13 +16,14 @@ import java.util.Objects;
import java.util.Optional;
import lombok.Getter;
public class PlayerBuffManager extends BasePlayerManager {
public final class PlayerBuffManager extends BasePlayerManager {
private final List<PlayerBuff> pendingBuffs;
private final Int2ObjectMap<PlayerBuff> buffs; // Server buffs
private int nextBuffUid;
public PlayerBuffManager(Player player) {
super(player);
this.buffs = new Int2ObjectOpenHashMap<>();
this.pendingBuffs = new ArrayList<>();
}
@@ -92,42 +92,40 @@ public class PlayerBuffManager extends BasePlayerManager {
*/
public synchronized boolean addBuff(int buffId, float duration, Avatar target) {
// Get buff excel data
BuffData buffData = GameData.getBuffDataMap().get(buffId);
var buffData = GameData.getBuffDataMap().get(buffId);
if (buffData == null) return false;
boolean success = false;
// Perform onAdded actions
success |=
var success =
Optional.ofNullable(GameData.getAbilityData(buffData.getAbilityName()))
.map(data -> data.modifiers.get(buffData.getModifierName()))
.map(modifier -> modifier.onAdded)
.map(
onAdded -> {
var s = false;
for (var a : onAdded) {
Grasscutter.getLogger().debug("onAdded exists");
if (Objects.requireNonNull(a.type) == AbilityModifierAction.Type.HealHP) {
Grasscutter.getLogger().debug("Attempting heal");
var shouldHeal = false;
for (var ability : onAdded) {
if (Objects.requireNonNull(ability.type) == AbilityModifierAction.Type.HealHP) {
if (target == null) continue;
var maxHp = target.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
var amount = a.amount.get() + a.amountByTargetMaxHPRatio.get() * maxHp;
var amount =
ability.amount.get() + ability.amountByTargetMaxHPRatio.get() * maxHp;
target.getAsEntity().heal(amount);
s = true;
Grasscutter.getLogger().debug("Healed {}", amount);
shouldHeal = true;
}
}
return s;
return shouldHeal;
})
.orElse(false);
Grasscutter.getLogger().debug("Oh no");
// Set duration
if (duration < 0f) {
duration = buffData.getTime();
}
// Dont add buff if duration is equal or less than 0
// Don't add buff if duration is equal or less than 0
if (duration <= 0) {
return success;
}

View File

@@ -3,7 +3,7 @@ package emu.grasscutter.game.player;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Transient;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.CodexAnimalData;
import emu.grasscutter.data.excels.codex.CodexAnimalData;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.inventory.GameItem;

View File

@@ -1,63 +1,60 @@
package emu.grasscutter.game.player;
import dev.morphia.annotations.Entity;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.val;
import java.util.Map;
/**
* Tracks progress the player made in the world, like obtained items, seen characters and more
*/
@Entity
public class PlayerProgress {
@Getter private Map<Integer, ItemEntry> itemHistory;
// keep track of EXEC_ADD_QUEST_PROGRESS count, will be used in CONTENT_ADD_QUEST_PROGRESS
// not sure where to put this, this should be saved to DB but not to individual quest, since
// it will be hard to loop and compare
private Map<Integer, Integer> questProgressCountMap;
public PlayerProgress(){
this.questProgressCountMap = new Int2IntOpenHashMap();
this.itemHistory = new Int2ObjectOpenHashMap<>();
}
public boolean hasPlayerObtainedItemHistorically(int itemId){
return itemHistory.containsKey(itemId);
}
public int addToItemHistory(int itemId, int count){
val itemEntry = itemHistory.computeIfAbsent(itemId, (key) -> new ItemEntry(itemId));
return itemEntry.addToObtainedCount(count);
}
public int getCurrentProgress(int progressId){
return questProgressCountMap.getOrDefault(progressId, -1);
}
public int addToCurrentProgress(int progressId, int count){
return questProgressCountMap.merge(progressId, count, Integer::sum);
}
@Entity
@NoArgsConstructor
public static class ItemEntry{
@Getter private int itemId;
@Getter @Setter private int obtainedCount;
ItemEntry(int itemId){
this.itemId = itemId;
}
int addToObtainedCount(int amount){
this.obtainedCount+=amount;
return this.obtainedCount;
}
}
}
package emu.grasscutter.game.player;
import dev.morphia.annotations.Entity;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.Map;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.val;
/** Tracks progress the player made in the world, like obtained items, seen characters and more */
@Entity
public class PlayerProgress {
@Getter private Map<Integer, ItemEntry> itemHistory;
// keep track of EXEC_ADD_QUEST_PROGRESS count, will be used in CONTENT_ADD_QUEST_PROGRESS
// not sure where to put this, this should be saved to DB but not to individual quest, since
// it will be hard to loop and compare
private Map<Integer, Integer> questProgressCountMap;
public PlayerProgress() {
this.questProgressCountMap = new Int2IntOpenHashMap();
this.itemHistory = new Int2ObjectOpenHashMap<>();
}
public boolean hasPlayerObtainedItemHistorically(int itemId) {
return itemHistory.containsKey(itemId);
}
public int addToItemHistory(int itemId, int count) {
val itemEntry = itemHistory.computeIfAbsent(itemId, (key) -> new ItemEntry(itemId));
return itemEntry.addToObtainedCount(count);
}
public int getCurrentProgress(int progressId) {
return questProgressCountMap.getOrDefault(progressId, -1);
}
public int addToCurrentProgress(int progressId, int count) {
return questProgressCountMap.merge(progressId, count, Integer::sum);
}
@Entity
@NoArgsConstructor
public static class ItemEntry {
@Getter private int itemId;
@Getter @Setter private int obtainedCount;
ItemEntry(int itemId) {
this.itemId = itemId;
}
int addToObtainedCount(int amount) {
this.obtainedCount += amount;
return this.obtainedCount;
}
}
}

View File

@@ -6,7 +6,6 @@ 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.QuestState;
import emu.grasscutter.game.quest.enums.QuestTrigger;
import emu.grasscutter.net.proto.RetcodeOuterClass.Retcode;
import emu.grasscutter.server.packet.send.*;
import java.util.Set;
@@ -222,7 +221,7 @@ public class PlayerProgressManager extends BasePlayerDataManager {
// Fire quest trigger for trans point unlock.
this.player
.getQuestManager()
.triggerEvent(QuestTrigger.QUEST_CONTENT_UNLOCK_TRANS_POINT, sceneId, pointId);
.triggerEvent(QuestContent.QUEST_CONTENT_UNLOCK_TRANS_POINT, sceneId, pointId);
// Send packet.
this.player.sendPacket(new PacketScenePointUnlockNotify(sceneId, pointId));

View File

@@ -5,14 +5,17 @@ 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.excels.AvatarSkillDepotData;
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;
@@ -20,18 +23,21 @@ 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.Int2ObjectMap;
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 class TeamManager extends BasePlayerDataManager {
@@ -45,8 +51,16 @@ public class TeamManager extends BasePlayerDataManager {
@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<TeamInfo> 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<Integer, Avatar> trialAvatars;
@Transient @Getter @Setter
private int previousIndex = -1; // index of character selection in team before adding trial avatar
public TeamManager() {
this.mpTeam = new TeamInfo();
@@ -270,6 +284,18 @@ public class TeamManager extends BasePlayerDataManager {
}
}
/** 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) {
@@ -277,9 +303,9 @@ public class TeamManager extends BasePlayerDataManager {
}
// If current team has changed
EntityAvatar currentEntity = this.getCurrentAvatarEntity();
Int2ObjectMap<EntityAvatar> existingAvatars = new Int2ObjectOpenHashMap<>();
int prevSelectedAvatarIndex = -1;
var currentEntity = this.getCurrentAvatarEntity();
var existingAvatars = new Int2ObjectOpenHashMap<EntityAvatar>();
var prevSelectedAvatarIndex = -1;
for (EntityAvatar entity : this.getActiveTeam()) {
existingAvatars.put(entity.getAvatar().getAvatarId(), entity);
@@ -290,9 +316,8 @@ public class TeamManager extends BasePlayerDataManager {
// Add back entities into team
for (int i = 0; i < this.getCurrentTeamInfo().getAvatars().size(); i++) {
int avatarId = this.getCurrentTeamInfo().getAvatars().get(i);
var avatarId = (int) this.getCurrentTeamInfo().getAvatars().get(i);
EntityAvatar entity;
if (existingAvatars.containsKey(avatarId)) {
entity = existingAvatars.get(avatarId);
existingAvatars.remove(avatarId);
@@ -309,7 +334,7 @@ public class TeamManager extends BasePlayerDataManager {
}
// Unload removed entities
for (EntityAvatar entity : existingAvatars.values()) {
for (var entity : existingAvatars.values()) {
this.getPlayer().getScene().removeEntity(entity);
entity.getAvatar().save();
}
@@ -323,18 +348,11 @@ public class TeamManager extends BasePlayerDataManager {
}
this.currentCharacterIndex = prevSelectedAvatarIndex;
// Update team resonances
this.updateTeamResonances();
// Update properties.
// Notify player.
this.updateTeamProperties();
// Packets
this.getPlayer().getWorld().broadcastPacket(new PacketSceneTeamUpdateNotify(this.getPlayer()));
// Skill charges packet - Yes, this is official server behavior as of 2.6.0
this.getActiveTeam().stream()
.map(EntityAvatar::getAvatar)
.forEach(Avatar::sendSkillExtraChargeMap);
// Run callback
// Send response packet.
if (responsePacket != null) {
this.getPlayer().sendPacket(responsePacket);
}
@@ -402,6 +420,138 @@ public class TeamManager extends BasePlayerDataManager {
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<Integer> 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<List<Long>> guidList) {
this.temporaryTeam =
guidList.stream()
@@ -725,4 +875,195 @@ public class TeamManager extends BasePlayerDataManager {
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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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());
}
}