diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 7889286ef..c76f46bf0 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -29,19 +29,16 @@ import emu.grasscutter.game.managers.MapMarkManager.*; import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.World; import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.proto.*; import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; import emu.grasscutter.net.proto.AttackResultOuterClass.AttackResult; import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; 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.PlayerApplyEnterMpResultNotifyOuterClass; import emu.grasscutter.net.proto.PlayerLocationInfoOuterClass.PlayerLocationInfo; -import emu.grasscutter.net.proto.PlayerWorldLocationInfoOuterClass; import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; -import emu.grasscutter.net.proto.ShowAvatarInfoOuterClass; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; -import emu.grasscutter.net.proto.SocialShowAvatarInfoOuterClass; import emu.grasscutter.server.event.player.PlayerJoinEvent; import emu.grasscutter.server.event.player.PlayerQuitEvent; import emu.grasscutter.server.game.GameServer; @@ -59,6 +56,9 @@ import java.util.concurrent.LinkedBlockingQueue; @Entity(value = "players", useDiscriminator = false) public class Player { + @Transient private static int GlobalMaximumSpringVolume = 8500000; + @Transient private static int GlobalMaximumStamina = 24000; + @Id private int id; @Indexed(options = @IndexOptions(unique = true)) private String accountId; @@ -395,12 +395,14 @@ public class Player { return playerProfile; } + // TODO: Based on the proto, property value could be int or float. + // Although there's no float value at this moment, our code should be prepared for float values. public Map getProperties() { return properties; } - public void setProperty(PlayerProperty prop, int value) { - getProperties().put(prop.getId(), value); + public boolean setProperty(PlayerProperty prop, int value) { + return setPropertyWithSanityCheck(prop, value); } public int getProperty(PlayerProperty prop) { @@ -1107,4 +1109,102 @@ public class Player { public void setMessageHandler(MessageHandler messageHandler) { this.messageHandler = messageHandler; } + + private void saveSanityCheckedProperty(PlayerProperty prop, int value) { + getProperties().put(prop.getId(), value); + } + + private boolean setPropertyWithSanityCheck(PlayerProperty prop, int value) { + if (prop == PlayerProperty.PROP_EXP) { // 1001 + if (!(value >= 0)) { return false; } + } else if (prop == PlayerProperty.PROP_BREAK_LEVEL) { // 1002 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_SATIATION_VAL) { // 1003 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_SATIATION_PENALTY_TIME) { // 1004 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_LEVEL) { // 4001 + if (!(value >= 0 && value <= 90)) { return false; } + } else if (prop == PlayerProperty.PROP_LAST_CHANGE_AVATAR_TIME) { // 10001 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_MAX_SPRING_VOLUME) { // 10002 + if (!(value >= 0 && value <= GlobalMaximumSpringVolume)) { return false; } + } else if (prop == PlayerProperty.PROP_CUR_SPRING_VOLUME) { // 10003 + int playerMaximumSpringVolume = getProperty(PlayerProperty.PROP_MAX_SPRING_VOLUME); + if (!(value >= 0 && value <= playerMaximumSpringVolume)) { return false; } + } else if (prop == PlayerProperty.PROP_IS_SPRING_AUTO_USE) { // 10004 + if (!(value >= 0 && value <= 1)) { return false; } + } else if (prop == PlayerProperty.PROP_SPRING_AUTO_USE_PERCENT) { // 10005 + if (!(value >= 0 && value <= 100)) { return false; } + } else if (prop == PlayerProperty.PROP_IS_FLYABLE) { // 10006 + if (!(0 <= value && value <= 1)) { return false; } + } else if (prop == PlayerProperty.PROP_IS_WEATHER_LOCKED) { // 10007 + if (!(0 <= value && value <= 1)) { return false; } + } else if (prop == PlayerProperty.PROP_IS_GAME_TIME_LOCKED) { // 10008 + if (!(0 <= value && value <= 1)) { return false; } + } else if (prop == PlayerProperty.PROP_IS_TRANSFERABLE) { // 10009 + if (!(0 <= value && value <= 1)) { return false; } + } else if (prop == PlayerProperty.PROP_MAX_STAMINA) { // 10010 + if (!(value >= 0 && value <= GlobalMaximumStamina)) { return false; } + } else if (prop == PlayerProperty.PROP_CUR_PERSIST_STAMINA) { // 10011 + int playerMaximumStamina = getProperty(PlayerProperty.PROP_MAX_STAMINA); + if (!(value >= 0 && value <= playerMaximumStamina)) { return false; } + } else if (prop == PlayerProperty.PROP_CUR_TEMPORARY_STAMINA) { // 10012 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_PLAYER_LEVEL) { // 10013 + if (!(0 < value && value <= 90)) { return false; } + } else if (prop == PlayerProperty.PROP_PLAYER_EXP) { // 10014 + if (!(0 <= value)) { return false; } + } else if (prop == PlayerProperty.PROP_PLAYER_HCOIN) { // 10015 + // see 10015 + } else if (prop == PlayerProperty.PROP_PLAYER_SCOIN) { // 10016 + // See 10015 + } else if (prop == PlayerProperty.PROP_PLAYER_MP_SETTING_TYPE) { // 10017 + if (!(0 <= value && value <= 2)) { return false; } + } else if (prop == PlayerProperty.PROP_IS_MP_MODE_AVAILABLE) { // 10018 + if (!(0 <= value && value <= 1)) { return false; } + } else if (prop == PlayerProperty.PROP_PLAYER_WORLD_LEVEL) { // 10019 + if (!(0 <= value && value <= 8)) { return false; } + } else if (prop == PlayerProperty.PROP_PLAYER_RESIN) { // 10020 + // Do not set 160 as a cap, because player can have more than 160 when they use fragile resin. + if (!(0 <= value)) { return false; } + } else if (prop == PlayerProperty.PROP_PLAYER_WAIT_SUB_HCOIN) { // 10022 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_PLAYER_WAIT_SUB_SCOIN) { // 10023 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_IS_ONLY_MP_WITH_PS_PLAYER) { // 10024 + if (!(0 <= value && value <= 1)) { return false; } + } else if (prop == PlayerProperty.PROP_PLAYER_MCOIN) { // 10025 + // see 10015 + } else if (prop == PlayerProperty.PROP_PLAYER_WAIT_SUB_MCOIN) { // 10026 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_PLAYER_LEGENDARY_KEY) { // 10027 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_IS_HAS_FIRST_SHARE) { // 10028 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_PLAYER_FORGE_POINT) { // 10029 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_CUR_CLIMATE_METER) { // 10035 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_CUR_CLIMATE_TYPE) { // 10036 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_CUR_CLIMATE_AREA_ID) { // 10037 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_CUR_CLIMATE_AREA_CLIMATE_TYPE) { // 10038 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_PLAYER_WORLD_LEVEL_LIMIT) { // 10039 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_PLAYER_WORLD_LEVEL_ADJUST_CD) { // 10040 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_PLAYER_LEGENDARY_DAILY_TASK_NUM) { // 10041 + // TODO: implement sanity check + } else if (prop == PlayerProperty.PROP_PLAYER_HOME_COIN) { // 10042 + if (!(0 <= value)) { return false; } + } else if (prop == PlayerProperty.PROP_PLAYER_WAIT_SUB_HOME_COIN) { // 10043 + // TODO: implement sanity check + } + saveSanityCheckedProperty(prop, value); + return false; + } + } diff --git a/src/main/java/emu/grasscutter/game/props/PlayerProperty.java b/src/main/java/emu/grasscutter/game/props/PlayerProperty.java index 9369c1605..3cb67d9bb 100644 --- a/src/main/java/emu/grasscutter/game/props/PlayerProperty.java +++ b/src/main/java/emu/grasscutter/game/props/PlayerProperty.java @@ -6,65 +6,67 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; public enum PlayerProperty { - PROP_EXP (1001), - PROP_BREAK_LEVEL (1002), - PROP_SATIATION_VAL (1003), - PROP_SATIATION_PENALTY_TIME (1004), - PROP_LEVEL (4001), - PROP_LAST_CHANGE_AVATAR_TIME (10001), - PROP_MAX_SPRING_VOLUME (10002), - PROP_CUR_SPRING_VOLUME (10003), - PROP_IS_SPRING_AUTO_USE (10004), - PROP_SPRING_AUTO_USE_PERCENT (10005), - PROP_IS_FLYABLE (10006), - PROP_IS_WEATHER_LOCKED (10007), - PROP_IS_GAME_TIME_LOCKED (10008), - PROP_IS_TRANSFERABLE (10009), - PROP_MAX_STAMINA (10010), - PROP_CUR_PERSIST_STAMINA (10011), - PROP_CUR_TEMPORARY_STAMINA (10012), - PROP_PLAYER_LEVEL (10013), - PROP_PLAYER_EXP (10014), - PROP_PLAYER_HCOIN (10015), // Primogem - PROP_PLAYER_SCOIN (10016), // Mora - PROP_PLAYER_MP_SETTING_TYPE (10017), - PROP_IS_MP_MODE_AVAILABLE (10018), - PROP_PLAYER_WORLD_LEVEL (10019), - PROP_PLAYER_RESIN (10020), - PROP_PLAYER_WAIT_SUB_HCOIN (10022), - PROP_PLAYER_WAIT_SUB_SCOIN (10023), - PROP_IS_ONLY_MP_WITH_PS_PLAYER (10024), - PROP_PLAYER_MCOIN (10025), // Genesis Crystal - PROP_PLAYER_WAIT_SUB_MCOIN (10026), - PROP_PLAYER_LEGENDARY_KEY (10027), - PROP_IS_HAS_FIRST_SHARE (10028), - PROP_PLAYER_FORGE_POINT (10029), - PROP_CUR_CLIMATE_METER (10035), - PROP_CUR_CLIMATE_TYPE (10036), - PROP_CUR_CLIMATE_AREA_ID (10037), - PROP_CUR_CLIMATE_AREA_CLIMATE_TYPE (10038), - PROP_PLAYER_WORLD_LEVEL_LIMIT (10039), - PROP_PLAYER_WORLD_LEVEL_ADJUST_CD (10040), - PROP_PLAYER_LEGENDARY_DAILY_TASK_NUM (10041), - PROP_PLAYER_HOME_COIN (10042), - PROP_PLAYER_WAIT_SUB_HOME_COIN (10043); - - private final int id; - private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); - - static { - Stream.of(values()).forEach(e -> map.put(e.getId(), e)); - } - - private PlayerProperty(int id) { - this.id = id; - } + PROP_EXP (1001), + PROP_BREAK_LEVEL (1002), + PROP_SATIATION_VAL (1003), + PROP_SATIATION_PENALTY_TIME (1004), + PROP_LEVEL (4001), + PROP_LAST_CHANGE_AVATAR_TIME (10001), + PROP_MAX_SPRING_VOLUME (10002), // Maximum volume of the Statue of the Seven for the player [0, 8500000] + PROP_CUR_SPRING_VOLUME (10003), // Current volume of the Statue of the Seven [0, PROP_MAX_SPRING_VOLUME] + PROP_IS_SPRING_AUTO_USE (10004), // Auto HP recovery when approaching the Statue of the Seven [0, 1] + PROP_SPRING_AUTO_USE_PERCENT (10005), // Auto HP recovery percentage [0, 100] + PROP_IS_FLYABLE (10006), // Are you in a state that disables your flying ability? e.g. new player [0, 1] + PROP_IS_WEATHER_LOCKED (10007), + PROP_IS_GAME_TIME_LOCKED (10008), + PROP_IS_TRANSFERABLE (10009), + PROP_MAX_STAMINA (10010), // Maximum stamina of the player (0 - 24000) + PROP_CUR_PERSIST_STAMINA (10011), // Used stamina of the player (0 - PROP_MAX_STAMINA) + PROP_CUR_TEMPORARY_STAMINA (10012), + PROP_PLAYER_LEVEL (10013), + PROP_PLAYER_EXP (10014), + PROP_PLAYER_HCOIN (10015), // Primogem (-inf, +inf) + // It is known that Mihoyo will make Primogem negative in the cases that a player spends + // his gems and then got a money refund, so negative is allowed. + PROP_PLAYER_SCOIN (10016), // Mora [0, +inf) + PROP_PLAYER_MP_SETTING_TYPE (10017), // Do you allow other players to join your game? [0=no 1=direct 2=approval] + PROP_IS_MP_MODE_AVAILABLE (10018), // Are you not in a quest or something that disables MP? [0, 1] + PROP_PLAYER_WORLD_LEVEL (10019), // [0, 8] + PROP_PLAYER_RESIN (10020), // Original Resin [0, +inf) + PROP_PLAYER_WAIT_SUB_HCOIN (10022), + PROP_PLAYER_WAIT_SUB_SCOIN (10023), + PROP_IS_ONLY_MP_WITH_PS_PLAYER (10024), // Is only MP with PlayStation players? [0, 1] + PROP_PLAYER_MCOIN (10025), // Genesis Crystal (-inf, +inf) see 10015 + PROP_PLAYER_WAIT_SUB_MCOIN (10026), + PROP_PLAYER_LEGENDARY_KEY (10027), + PROP_IS_HAS_FIRST_SHARE (10028), + PROP_PLAYER_FORGE_POINT (10029), + PROP_CUR_CLIMATE_METER (10035), + PROP_CUR_CLIMATE_TYPE (10036), + PROP_CUR_CLIMATE_AREA_ID (10037), + PROP_CUR_CLIMATE_AREA_CLIMATE_TYPE (10038), + PROP_PLAYER_WORLD_LEVEL_LIMIT (10039), + PROP_PLAYER_WORLD_LEVEL_ADJUST_CD (10040), + PROP_PLAYER_LEGENDARY_DAILY_TASK_NUM (10041), + PROP_PLAYER_HOME_COIN (10042), // Realm currency [0, +inf) + PROP_PLAYER_WAIT_SUB_HOME_COIN (10043); - public int getId() { - return id; - } - - public static PlayerProperty getPropById(int value) { - return map.getOrDefault(value, null); - } + private final int id; + private static final Int2ObjectMap map = new Int2ObjectOpenHashMap<>(); + + static { + Stream.of(values()).forEach(e -> map.put(e.getId(), e)); + } + + PlayerProperty(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static PlayerProperty getPropById(int value) { + return map.getOrDefault(value, null); + } } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerPropReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerPropReq.java new file mode 100644 index 000000000..bbe8647da --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerSetPlayerPropReq.java @@ -0,0 +1,37 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.PropValueOuterClass.PropValue; +import emu.grasscutter.net.proto.SetPlayerPropReqOuterClass.SetPlayerPropReq; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketSetPlayerNameRsp; +import emu.grasscutter.server.packet.send.PacketSetPlayerPropRsp; + +import java.util.ArrayList; +import java.util.List; + +@Opcodes(PacketOpcodes.SetPlayerPropReq) +public class HandlerSetPlayerPropReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + // Auto template + SetPlayerPropReq req = SetPlayerPropReq.parseFrom(payload); + Player player = session.getPlayer(); + List propList = req.getPropListList(); + for (int i = 0; i < propList.size(); i++) { + if (!player.setProperty(PlayerProperty.getPropById(propList.get(i).getType()), (int)propList.get(i).getVal())) { + session.send(new PacketSetPlayerPropRsp(1)); + } + } + player.save(); + session.send(new PacketSetPlayerPropRsp(0)); + } + + + +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketSetPlayerPropRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketSetPlayerPropRsp.java new file mode 100644 index 000000000..9ce77c915 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketSetPlayerPropRsp.java @@ -0,0 +1,19 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.SetPlayerPropRspOuterClass; +import emu.grasscutter.net.proto.SetPlayerPropRspOuterClass.SetPlayerPropRsp; + +public class PacketSetPlayerPropRsp extends BasePacket { + + public PacketSetPlayerPropRsp(int retCode) { + super(PacketOpcodes.SetPlayerPropRsp); + SetPlayerPropRspOuterClass.SetPlayerPropRsp.Builder proto = SetPlayerPropRspOuterClass.SetPlayerPropRsp.newBuilder(); + if (retCode != 0) { + proto.setRetcode(retCode); + } + this.setData(proto.build()); + } +}