package emu.grasscutter.game; import java.time.Instant; import java.util.*; import dev.morphia.annotations.*; import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.data.GameData; import emu.grasscutter.data.def.PlayerLevelData; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.avatar.AvatarProfileData; import emu.grasscutter.game.avatar.AvatarStorage; import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.entity.EntityItem; import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.friends.FriendsList; import emu.grasscutter.game.friends.PlayerProfile; import emu.grasscutter.game.gacha.PlayerGachaInfo; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.player.PlayerBirthday; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; import emu.grasscutter.net.proto.HeadImageOuterClass.HeadImage; import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType; import emu.grasscutter.net.proto.MpSettingTypeOuterClass.MpSettingType; import emu.grasscutter.net.proto.OnlinePlayerInfoOuterClass.OnlinePlayerInfo; import emu.grasscutter.net.proto.PlayerApplyEnterMpReasonOuterClass.PlayerApplyEnterMpReason; import emu.grasscutter.net.proto.PlayerApplyEnterMpResultNotifyOuterClass; import emu.grasscutter.net.proto.PlayerLocationInfoOuterClass.PlayerLocationInfo; import emu.grasscutter.net.proto.PlayerWorldLocationInfoOuterClass; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.*; import emu.grasscutter.utils.Position; import emu.grasscutter.utils.DateHelper; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import java.util.*; @Entity(value = "players", useDiscriminator = false) public class Player { @Id private int id; @Indexed(options = @IndexOptions(unique = true)) private String accountId; @Transient private Account account; private String nickname; private String signature; private int headImage; private int nameCardId = 210001; private Position pos; private Position rotation; private PlayerBirthday birthday; private Map properties; private Set nameCardList; private Set flyCloakList; private Set costumeList; @Transient private long nextGuid = 0; @Transient private int peerId; @Transient private World world; @Transient private Scene scene; @Transient private GameSession session; @Transient private AvatarStorage avatars; @Transient private Inventory inventory; @Transient private FriendsList friendsList; private TeamManager teamManager; private PlayerGachaInfo gachaInfo; private PlayerProfile playerProfile; private boolean showAvatar; private ArrayList shownAvatars; private Set rewardedLevels; private ArrayList mail; private int sceneId; private int regionId; private int mainCharacterId; private boolean godmode; private boolean moonCard; private Date moonCardStartTime; private int moonCardDuration; private Set moonCardGetTimes; @Transient private boolean paused; @Transient private int enterSceneToken; @Transient private SceneLoadState sceneState; @Transient private boolean hasSentAvatarDataNotify; @Transient private long nextSendPlayerLocTime = 0; @Transient private final Int2ObjectMap coopRequests; @Transient private final InvokeHandler combatInvokeHandler; @Transient private final InvokeHandler abilityInvokeHandler; @Transient private final InvokeHandler clientAbilityInitFinishHandler; @Deprecated @SuppressWarnings({"rawtypes", "unchecked"}) // Morphia only! public Player() { this.inventory = new Inventory(this); this.avatars = new AvatarStorage(this); this.friendsList = new FriendsList(this); this.pos = new Position(); this.rotation = new Position(); this.properties = new HashMap<>(); for (PlayerProperty prop : PlayerProperty.values()) { if (prop.getId() < 10000) { continue; } this.properties.put(prop.getId(), 0); } this.gachaInfo = new PlayerGachaInfo(); this.nameCardList = new HashSet<>(); this.flyCloakList = new HashSet<>(); this.costumeList = new HashSet<>(); this.mail = new ArrayList<>(); this.setSceneId(3); this.setRegionId(1); this.sceneState = SceneLoadState.NONE; this.coopRequests = new Int2ObjectOpenHashMap<>(); this.combatInvokeHandler = new InvokeHandler(PacketCombatInvocationsNotify.class); this.abilityInvokeHandler = new InvokeHandler(PacketAbilityInvocationsNotify.class); this.clientAbilityInitFinishHandler = new InvokeHandler(PacketClientAbilityInitFinishNotify.class); this.birthday = new PlayerBirthday(); this.rewardedLevels = new HashSet<>(); this.moonCardGetTimes = new HashSet<>(); } // On player creation public Player(GameSession session) { this(); this.account = session.getAccount(); this.accountId = this.getAccount().getId(); this.session = session; this.nickname = "Traveler"; this.signature = ""; this.teamManager = new TeamManager(this); this.birthday = new PlayerBirthday(); this.setProperty(PlayerProperty.PROP_PLAYER_LEVEL, 1); this.setProperty(PlayerProperty.PROP_IS_SPRING_AUTO_USE, 1); this.setProperty(PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT, 50); this.setProperty(PlayerProperty.PROP_IS_FLYABLE, 1); this.setProperty(PlayerProperty.PROP_IS_TRANSFERABLE, 1); this.setProperty(PlayerProperty.PROP_MAX_STAMINA, 24000); this.setProperty(PlayerProperty.PROP_CUR_PERSIST_STAMINA, 24000); this.setProperty(PlayerProperty.PROP_PLAYER_RESIN, 160); this.getFlyCloakList().add(140001); this.getNameCardList().add(210001); this.getPos().set(GameConstants.START_POSITION); this.getRotation().set(0, 307, 0); } public int getUid() { return id; } public void setUid(int id) { this.id = id; } public long getNextGameGuid() { long nextId = ++this.nextGuid; return ((long) this.getUid() << 32) + nextId; } public Account getAccount() { return account; } public void setAccount(Account account) { this.account = account; this.account.setPlayerId(getUid()); } public GameSession getSession() { return session; } public void setSession(GameSession session) { this.session = session; } public boolean isOnline() { return this.getSession() != null && this.getSession().isActive(); } public GameServer getServer() { return this.getSession().getServer(); } public synchronized World getWorld() { return this.world; } public synchronized void setWorld(World world) { this.world = world; } public synchronized Scene getScene() { return scene; } public synchronized void setScene(Scene scene) { this.scene = scene; } public int getGmLevel() { return 1; } public String getNickname() { return nickname; } public void setNickname(String nickName) { this.nickname = nickName; this.updateProfile(); } public int getHeadImage() { return headImage; } public void setHeadImage(int picture) { this.headImage = picture; this.updateProfile(); } public String getSignature() { return signature; } public void setSignature(String signature) { this.signature = signature; this.updateProfile(); } public Position getPos() { return pos; } public Position getRotation() { return rotation; } public int getLevel() { return this.getProperty(PlayerProperty.PROP_PLAYER_LEVEL); } public int getExp() { return this.getProperty(PlayerProperty.PROP_PLAYER_EXP); } public int getWorldLevel() { return this.getProperty(PlayerProperty.PROP_PLAYER_WORLD_LEVEL); } public void setWorldLevel(int level) { this.setProperty(PlayerProperty.PROP_PLAYER_WORLD_LEVEL, level); this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_WORLD_LEVEL)); } public int getPrimogems() { return this.getProperty(PlayerProperty.PROP_PLAYER_HCOIN); } public void setPrimogems(int primogem) { this.setProperty(PlayerProperty.PROP_PLAYER_HCOIN, primogem); this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_HCOIN)); } public int getMora() { return this.getProperty(PlayerProperty.PROP_PLAYER_SCOIN); } public void setMora(int mora) { this.setProperty(PlayerProperty.PROP_PLAYER_SCOIN, mora); this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_SCOIN)); } private int getExpRequired(int level) { PlayerLevelData levelData = GameData.getPlayerLevelDataMap().get(level); return levelData != null ? levelData.getExp() : 0; } private float getExpModifier() { return Grasscutter.getConfig().getGameServerOptions().getGameRates().ADVENTURE_EXP_RATE; } // Affected by exp rate public void earnExp(int exp) { addExpDirectly((int) (exp * getExpModifier())); } // Directly give player exp public void addExpDirectly(int gain) { boolean hasLeveledUp = false; int level = getLevel(); int exp = getExp(); int reqExp = getExpRequired(level); exp += gain; while (exp >= reqExp && reqExp > 0) { exp -= reqExp; level += 1; reqExp = getExpRequired(level); hasLeveledUp = true; } if (hasLeveledUp) { // Set level property this.setProperty(PlayerProperty.PROP_PLAYER_LEVEL, level); // Update social status this.updateProfile(); // Update player with packet this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_LEVEL)); } // Set exp this.setProperty(PlayerProperty.PROP_PLAYER_EXP, exp); // Update player with packet this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_EXP)); } private void updateProfile() { getProfile().syncWithCharacter(this); } public boolean isFirstLoginEnterScene() { return !this.hasSentAvatarDataNotify; } public TeamManager getTeamManager() { return this.teamManager; } public PlayerGachaInfo getGachaInfo() { return gachaInfo; } public PlayerProfile getProfile() { if (this.playerProfile == null) { this.playerProfile = new PlayerProfile(this); } return playerProfile; } public Map getProperties() { return properties; } public void setProperty(PlayerProperty prop, int value) { getProperties().put(prop.getId(), value); } public int getProperty(PlayerProperty prop) { return getProperties().get(prop.getId()); } public Set getFlyCloakList() { return flyCloakList; } public Set getCostumeList() { return costumeList; } public Set getNameCardList() { return this.nameCardList; } public MpSettingType getMpSetting() { return MpSettingType.MP_SETTING_ENTER_AFTER_APPLY; // TEMP } public synchronized Int2ObjectMap getCoopRequests() { return coopRequests; } public InvokeHandler getCombatInvokeHandler() { return this.combatInvokeHandler; } public InvokeHandler getAbilityInvokeHandler() { return this.abilityInvokeHandler; } public InvokeHandler getClientAbilityInitFinishHandler() { return clientAbilityInitFinishHandler; } public AvatarStorage getAvatars() { return avatars; } public Inventory getInventory() { return inventory; } public FriendsList getFriendsList() { return this.friendsList; } public int getEnterSceneToken() { return enterSceneToken; } public void setEnterSceneToken(int enterSceneToken) { this.enterSceneToken = enterSceneToken; } public int getNameCardId() { return nameCardId; } public void setNameCardId(int nameCardId) { this.nameCardId = nameCardId; this.updateProfile(); } public int getMainCharacterId() { return mainCharacterId; } public void setMainCharacterId(int mainCharacterId) { this.mainCharacterId = mainCharacterId; } public int getPeerId() { return peerId; } public void setPeerId(int peerId) { this.peerId = peerId; } public int getClientTime() { return session.getClientTime(); } public long getLastPingTime() { return session.getLastPingTime(); } public boolean isPaused() { return paused; } public void setPaused(boolean newPauseState) { boolean oldPauseState = this.paused; this.paused = newPauseState; if (newPauseState && !oldPauseState) { this.onPause(); } else if (oldPauseState && !newPauseState) { this.onUnpause(); } } public SceneLoadState getSceneLoadState() { return sceneState; } public void setSceneLoadState(SceneLoadState sceneState) { this.sceneState = sceneState; } public boolean isInMultiplayer() { return this.getWorld() != null && this.getWorld().isMultiplayer(); } public int getSceneId() { return sceneId; } public void setSceneId(int sceneId) { this.sceneId = sceneId; } public int getRegionId() { return regionId; } public void setRegionId(int regionId) { this.regionId = regionId; } public boolean inMoonCard() { return moonCard; } public void setMoonCard(boolean moonCard) { this.moonCard = moonCard; } public void addMoonCardDays(int days) { this.moonCardDuration += days; } public int getMoonCardDuration() { return moonCardDuration; } public void setMoonCardDuration(int moonCardDuration) { this.moonCardDuration = moonCardDuration; } public Date getMoonCardStartTime() { return moonCardStartTime; } public void setMoonCardStartTime(Date moonCardStartTime) { this.moonCardStartTime = moonCardStartTime; } public Set getMoonCardGetTimes() { return moonCardGetTimes; } public void setMoonCardGetTimes(Set moonCardGetTimes) { this.moonCardGetTimes = moonCardGetTimes; } public int getMoonCardRemainDays() { Calendar remainCalendar = Calendar.getInstance(); remainCalendar.setTime(moonCardStartTime); remainCalendar.add(Calendar.DATE, moonCardDuration); Date theLastDay = remainCalendar.getTime(); Date now = DateHelper.onlyYearMonthDay(new Date()); return (int) ((theLastDay.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)); // By copilot } public void rechargeMoonCard() { LinkedList items = new LinkedList(); for (int i = 0; i < 300; i++) { items.add(new GameItem(203)); } inventory.addItems(items); if (!moonCard) { moonCard = true; Date now = new Date(); moonCardStartTime = DateHelper.onlyYearMonthDay(now); moonCardDuration = 30; } else { moonCardDuration += 30; } if (!moonCardGetTimes.contains(moonCardStartTime)) { moonCardGetTimes.add(moonCardStartTime); } } public void getTodayMoonCard() { if (!moonCard) { return; } Date now = DateHelper.onlyYearMonthDay(new Date()); if (moonCardGetTimes.contains(now)) { return; } Date stopTime = new Date(); Calendar stopCalendar = Calendar.getInstance(); stopCalendar.setTime(stopTime); stopCalendar.add(Calendar.DATE, moonCardDuration); stopTime = stopCalendar.getTime(); if (now.after(stopTime)) { moonCard = false; return; } moonCardGetTimes.add(now); addMoonCardDays(1); GameItem item = new GameItem(201, 90); getInventory().addItem(item, ActionReason.BlessingRedeemReward); session.send(new PacketCardProductRewardNotify(getMoonCardRemainDays())); } public boolean inGodmode() { return godmode; } public void setGodmode(boolean godmode) { this.godmode = godmode; } public boolean hasSentAvatarDataNotify() { return hasSentAvatarDataNotify; } public void setHasSentAvatarDataNotify(boolean hasSentAvatarDataNotify) { this.hasSentAvatarDataNotify = hasSentAvatarDataNotify; } public void addAvatar(Avatar avatar) { boolean result = getAvatars().addAvatar(avatar); if (result) { // Add starting weapon getAvatars().addStartingWeapon(avatar); // Done if (hasSentAvatarDataNotify()) { // Recalc stats avatar.recalcStats(); // Packet sendPacket(new PacketAvatarAddNotify(avatar, false)); } } else { // Failed adding avatar } } public void addFlycloak(int flycloakId) { this.getFlyCloakList().add(flycloakId); this.sendPacket(new PacketAvatarGainFlycloakNotify(flycloakId)); } public void addCostume(int costumeId) { this.getCostumeList().add(costumeId); this.sendPacket(new PacketAvatarGainCostumeNotify(costumeId)); } public void addNameCard(int nameCardId) { this.getNameCardList().add(nameCardId); this.sendPacket(new PacketUnlockNameCardNotify(nameCardId)); } public void setNameCard(int nameCardId) { if (!this.getNameCardList().contains(nameCardId)) { return; } this.setNameCardId(nameCardId); this.sendPacket(new PacketSetNameCardRsp(nameCardId)); } public void dropMessage(Object message) { this.sendPacket(new PacketPrivateChatNotify(GameConstants.SERVER_CONSOLE_UID, getUid(), message.toString())); } /** * Sends a message to another player. * * @param sender The sender of the message. * @param message The message to send. */ public void sendMessage(Player sender, Object message) { this.sendPacket(new PacketPrivateChatNotify(sender.getUid(), this.getUid(), message.toString())); } // ---------------------MAIL------------------------ public List getAllMail() { return this.mail; } public void sendMail(Mail message) { this.mail.add(message); this.save(); Grasscutter.getLogger().info("Mail sent to user [" + this.getUid() + ":" + this.getNickname() + "]!"); if(this.isOnline()) { this.sendPacket(new PacketMailChangeNotify(this, message)); } // TODO: setup a way for the mail notification to show up when someone receives mail when they were offline } public boolean deleteMail(int mailId) { Mail message = getMail(mailId); if(message != null) { int index = getMailId(message); message.expireTime = (int) Instant.now().getEpochSecond(); // Just set the mail as expired for now. I don't want to implement a counter specifically for an account... this.replaceMailByIndex(index, message); return true; } return false; } public Mail getMail(int index) { return this.mail.get(index); } public int getMailId(Mail message) { return this.mail.indexOf(message); } public boolean replaceMailByIndex(int index, Mail message) { if(getMail(index) != null) { this.mail.set(index, message); this.save(); return true; } else { return false; } } public void interactWith(int gadgetEntityId) { GameEntity entity = getScene().getEntityById(gadgetEntityId); if (entity == null) { return; } // Delete entity.getScene().removeEntity(entity); // Handle if (entity instanceof EntityItem) { // Pick item EntityItem drop = (EntityItem) entity; GameItem item = new GameItem(drop.getItemData(), drop.getCount()); // Add to inventory boolean success = getInventory().addItem(item, ActionReason.SubfieldDrop); if (success) { this.sendPacket(new PacketGadgetInteractRsp(drop, InteractType.INTERACT_PICK_ITEM)); } } return; } public void onPause() { } public void onUnpause() { } public void sendPacket(BasePacket packet) { if (this.hasSentAvatarDataNotify) { this.getSession().send(packet); } } public OnlinePlayerInfo getOnlinePlayerInfo() { OnlinePlayerInfo.Builder onlineInfo = OnlinePlayerInfo.newBuilder() .setUid(this.getUid()) .setNickname(this.getNickname()) .setPlayerLevel(this.getLevel()) .setMpSettingType(this.getMpSetting()) .setNameCardId(this.getNameCardId()) .setSignature(this.getSignature()) .setAvatarId(HeadImage.newBuilder().setAvatarId(this.getHeadImage()).getAvatarId()); if (this.getWorld() != null) { onlineInfo.setCurPlayerNumInWorld(this.getWorld().getPlayers().indexOf(this) + 1); } else { onlineInfo.setCurPlayerNumInWorld(1); } return onlineInfo.build(); } public PlayerBirthday getBirthday() { return this.birthday; } public void setBirthday(int d, int m) { this.birthday = new PlayerBirthday(d, m); this.updateProfile(); } public boolean hasBirthday() { return this.birthday.getDay() > 0; } public Set getRewardedLevels() { return rewardedLevels; } public void setRewardedLevels(Set rewardedLevels) { this.rewardedLevels = rewardedLevels; } public SocialDetail.Builder getSocialDetail() { SocialDetail.Builder social = SocialDetail.newBuilder() .setUid(this.getUid()) .setAvatarId(HeadImage.newBuilder().setAvatarId(this.getHeadImage()).getAvatarId()) .setNickname(this.getNickname()) .setSignature(this.getSignature()) .setLevel(this.getLevel()) .setBirthday(this.getBirthday().getFilledProtoWhenNotEmpty()) .setWorldLevel(this.getWorldLevel()) .setNameCardId(this.getNameCardId()) .setFinishAchievementNum(0); return social; } public PlayerWorldLocationInfoOuterClass.PlayerWorldLocationInfo getWorldPlayerLocationInfo() { return PlayerWorldLocationInfoOuterClass.PlayerWorldLocationInfo.newBuilder() .setSceneId(this.getSceneId()) .setPlayerLoc(this.getPlayerLocationInfo()) .build(); } public PlayerLocationInfo getPlayerLocationInfo() { return PlayerLocationInfo.newBuilder() .setUid(this.getUid()) .setPos(this.getPos().toProto()) .setRot(this.getRotation().toProto()) .build(); } public synchronized void onTick() { // Check ping if (this.getLastPingTime() > System.currentTimeMillis() + 60000) { this.getSession().close(); return; } // Check co-op requests Iterator it = this.getCoopRequests().values().iterator(); while (it.hasNext()) { CoopRequest req = it.next(); if (req.isExpired()) { req.getRequester().sendPacket(new PacketPlayerApplyEnterMpResultNotify(this, false, PlayerApplyEnterMpResultNotifyOuterClass.PlayerApplyEnterMpResultNotify.Reason.SYSTEM_JUDGE)); it.remove(); } } // Ping if (this.getWorld() != null) { // RTT notify - very important to send this often this.sendPacket(new PacketWorldPlayerRTTNotify(this.getWorld())); // Update player locations if in multiplayer every 5 seconds long time = System.currentTimeMillis(); if (this.getWorld().isMultiplayer() && this.getScene() != null && time > nextSendPlayerLocTime) { this.sendPacket(new PacketWorldPlayerLocationNotify(this.getWorld())); this.sendPacket(new PacketScenePlayerLocationNotify(this.getScene())); this.resetSendPlayerLocTime(); } } } public void resetSendPlayerLocTime() { this.nextSendPlayerLocTime = System.currentTimeMillis() + 5000; } @PostLoad private void onLoad() { this.getTeamManager().setPlayer(this); } public void save() { DatabaseHelper.savePlayer(this); } public void onLogin() { // Make sure these exist if (this.getTeamManager() == null) { this.teamManager = new TeamManager(this); } if (this.getProfile().getUid() == 0) { this.getProfile().syncWithCharacter(this); } // Check if player object exists in server // TODO - optimize Player exists = this.getServer().getPlayerByUid(getUid()); if (exists != null) { exists.getSession().close(); } // Load from db this.getAvatars().loadFromDatabase(); this.getInventory().loadFromDatabase(); this.getAvatars().postLoad(); this.getFriendsList().loadFromDatabase(); // Create world World world = new World(this); world.addPlayer(this); // Add to gameserver if (getSession().isActive()) { getServer().registerPlayer(this); getProfile().setPlayer(this); // Set online } // Multiplayer setting this.setProperty(PlayerProperty.PROP_PLAYER_MP_SETTING_TYPE, this.getMpSetting().getNumber()); this.setProperty(PlayerProperty.PROP_IS_MP_MODE_AVAILABLE, 1); // Packets session.send(new PacketPlayerDataNotify(this)); // Player data session.send(new PacketStoreWeightLimitNotify()); session.send(new PacketPlayerStoreNotify(this)); session.send(new PacketAvatarDataNotify(this)); session.send(new PacketPlayerEnterSceneNotify(this)); // Enter game world session.send(new PacketPlayerLevelRewardUpdateNotify(rewardedLevels)); session.send(new PacketOpenStateUpdateNotify()); // First notify packets sent this.setHasSentAvatarDataNotify(true); } public void onLogout() { // Leave world if (this.getWorld() != null) { this.getWorld().removePlayer(this); } // Status stuff this.getProfile().syncWithCharacter(this); this.getProfile().setPlayer(null); // Set offline this.getCoopRequests().clear(); // Save to db this.save(); this.getTeamManager().saveAvatars(); this.getFriendsList().save(); } public enum SceneLoadState { NONE(0), LOADING(1), INIT(2), LOADED(3); private final int value; private SceneLoadState(int value) { this.value = value; } public int getValue() { return this.value; } } }