diff --git a/build.gradle b/build.gradle index 70f6a9358..1acd90dae 100644 --- a/build.gradle +++ b/build.gradle @@ -197,4 +197,8 @@ javadoc { if(JavaVersion.current().isJava9Compatible()) { options.addBooleanOption('html5', true) } +} + +processResources { + dependsOn "generateProto" } \ No newline at end of file diff --git a/proto/ChangeMailStarNotify.proto b/proto/ChangeMailStarNotify.proto new file mode 100644 index 000000000..e5347c86b --- /dev/null +++ b/proto/ChangeMailStarNotify.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message ChangeMailStarNotify { + repeated uint32 mail_id_list = 1; + bool is_star = 2; +} diff --git a/proto/DelMailReq.proto b/proto/DelMailReq.proto new file mode 100644 index 000000000..ac72fce2b --- /dev/null +++ b/proto/DelMailReq.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message DelMailReq { + repeated uint32 mail_id_list = 1; +} diff --git a/proto/DelMailRsp.proto b/proto/DelMailRsp.proto new file mode 100644 index 000000000..cd20e6fa9 --- /dev/null +++ b/proto/DelMailRsp.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message DelMailRsp { + int32 retcode = 1; + repeated uint32 mail_id_list = 2; +} diff --git a/proto/EquipParam.proto b/proto/EquipParam.proto new file mode 100644 index 000000000..52d24943e --- /dev/null +++ b/proto/EquipParam.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message EquipParam { + uint32 item_id = 1; + uint32 item_num = 2; + uint32 item_level = 3; + uint32 promote_level = 4; +} diff --git a/proto/GetAllMailReq.proto b/proto/GetAllMailReq.proto new file mode 100644 index 000000000..8c6bce100 --- /dev/null +++ b/proto/GetAllMailReq.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message GetAllMailReq { + bool is_gift_mail = 1; +} diff --git a/proto/GetAllMailRsp.proto b/proto/GetAllMailRsp.proto new file mode 100644 index 000000000..0613899cb --- /dev/null +++ b/proto/GetAllMailRsp.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "MailData.proto"; + +message GetAllMailRsp { + int32 retcode = 1; + repeated MailData mail_list = 2; + bool is_truncated = 3; +} diff --git a/proto/GetMailItemReq.proto b/proto/GetMailItemReq.proto new file mode 100644 index 000000000..259c8ba5c --- /dev/null +++ b/proto/GetMailItemReq.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message GetMailItemReq { + repeated uint32 mail_id_list = 1; +} diff --git a/proto/GetMailItemRsp.proto b/proto/GetMailItemRsp.proto new file mode 100644 index 000000000..e14bd54c8 --- /dev/null +++ b/proto/GetMailItemRsp.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "EquipParam.proto"; + +message GetMailItemRsp { + int32 retcode = 1; + repeated uint32 mail_id_list = 2; + repeated EquipParam item_list = 3; +} diff --git a/proto/MailChangeNotify.proto b/proto/MailChangeNotify.proto new file mode 100644 index 000000000..1e362f6ff --- /dev/null +++ b/proto/MailChangeNotify.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "MailData.proto"; + +message MailChangeNotify { + repeated MailData mail_list = 1; + repeated uint32 del_mail_id_list = 2; +} diff --git a/proto/MailData.proto b/proto/MailData.proto new file mode 100644 index 000000000..ba606c2e6 --- /dev/null +++ b/proto/MailData.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "MailTextContent.proto"; +import "MailItem.proto"; + +message MailData { + uint32 mail_id = 1; + MailTextContent mail_text_content = 4; + repeated MailItem item_list = 7; + uint32 send_time = 8; + uint32 expire_time = 9; + uint32 importance = 10; + bool is_read = 11; + bool is_attachment_got = 12; + uint32 config_id = 13; + repeated string argument_list = 14; + uint32 state_value = 15; +} diff --git a/proto/MailItem.proto b/proto/MailItem.proto new file mode 100644 index 000000000..da657815e --- /dev/null +++ b/proto/MailItem.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +import "ItemParam.proto"; +import "MaterialDeleteInfo.proto"; + +message MailItem { + ItemParam item_param = 1; + MaterialDeleteInfo delete_info = 2; +} diff --git a/proto/MailTextContent.proto b/proto/MailTextContent.proto new file mode 100644 index 000000000..097710f87 --- /dev/null +++ b/proto/MailTextContent.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message MailTextContent { + string title = 1; + string content = 2; + string sender = 3; +} diff --git a/proto/ReadMailNotify.proto b/proto/ReadMailNotify.proto new file mode 100644 index 000000000..f485c8a6c --- /dev/null +++ b/proto/ReadMailNotify.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message ReadMailNotify { + repeated uint32 mail_id_list = 1; +} diff --git a/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java new file mode 100644 index 000000000..7973bafbe --- /dev/null +++ b/src/main/java/emu/grasscutter/command/commands/SendMailCommand.java @@ -0,0 +1,210 @@ +package emu.grasscutter.command.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.Command; +import emu.grasscutter.command.CommandHandler; +import emu.grasscutter.database.DatabaseHelper; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.Mail; +import emu.grasscutter.server.packet.send.PacketMailChangeNotify; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; + +@Command(label = "sendmail", usage = "sendmail [templateId]", + description = "Sends mail to the specified user. The usage of this command changes based on it's composition state.", permission = "server.sendmail") +public class SendMailCommand implements CommandHandler { + + // TODO: You should be able to do /sendmail and then just send subsequent messages until you finish + // However, due to the current nature of the command system, I don't think this is possible without rewriting + // the command system (again). For now this will do + + // Key = User that is constructing the mail. + private static HashMap mailBeingConstructed = new HashMap(); + + // Yes this is awful and I hate it. + @Override + public void execute(GenshinPlayer sender, List args) { + int senderId; + if(sender != null) { + senderId = sender.getUid(); + } else { + senderId = -1; + } + + if (!mailBeingConstructed.containsKey(senderId)) { + switch (args.size()) { + case 1 -> { + MailBuilder mailBuilder; + switch (args.get(0).toLowerCase()) { + case "help" -> { + CommandHandler.sendMessage(sender, this.getClass().getAnnotation(Command.class).description() + "\nUsage: " + this.getClass().getAnnotation(Command.class).usage()); + return; + } + case "all" -> mailBuilder = new MailBuilder(true, new Mail()); + default -> { + if (DatabaseHelper.getPlayerById(Integer.parseInt(args.get(0))) != null) { + mailBuilder = new MailBuilder(Integer.parseInt(args.get(0)), new Mail()); + break; + } else { + CommandHandler.sendMessage(sender, "The user with an id of '" + args.get(0) + "' does not exist"); + return; + } + } + } + mailBeingConstructed.put(senderId, mailBuilder); + CommandHandler.sendMessage(sender, "Starting composition of message.\nPlease use `/sendmail ` to continue.\nYou can use `/sendmail stop` at any time"); + } + case 2 -> CommandHandler.sendMessage(sender, "Mail templates coming soon implemented..."); + default -> CommandHandler.sendMessage(sender, "Invalid arguments.\nUsage `/sendmail <userId|all|help> [templateId]`"); + } + } else { + MailBuilder mailBuilder = mailBeingConstructed.get(senderId); + + if (args.size() >= 1) { + switch (args.get(0).toLowerCase()) { + case "stop" -> { + mailBeingConstructed.remove(senderId); + CommandHandler.sendMessage(sender, "Message sending cancelled"); + return; + } + case "finish" -> { + if (mailBuilder.constructionStage == 3) { + if (mailBuilder.sendToAll == false) { + Grasscutter.getGameServer().getPlayerByUid(mailBuilder.recipient, true).sendMail(mailBuilder.mail); + CommandHandler.sendMessage(sender, "Message sent to user " + mailBuilder.recipient + "!"); + } else { + for (GenshinPlayer player : DatabaseHelper.getAllPlayers()) { + Grasscutter.getGameServer().getPlayerByUid(player.getUid(), true).sendMail(mailBuilder.mail); + } + CommandHandler.sendMessage(sender, "Message sent to all users!"); + } + mailBeingConstructed.remove(senderId); + } else { + CommandHandler.sendMessage(sender, "Message composition not at final stage.\nPlease use `/sendmail " + getConstructionArgs(mailBuilder.constructionStage) + "` or `/sendmail stop` to cancel"); + } + return; + } + case "help" -> { + CommandHandler.sendMessage(sender, "Please use `/sendmail " + getConstructionArgs(mailBuilder.constructionStage) + "`"); + return; + } + default -> { + switch (mailBuilder.constructionStage) { + case 0 -> { + String title = String.join(" ", args.subList(0, args.size())); + mailBuilder.mail.mailContent.title = title; + CommandHandler.sendMessage(sender, "Message title set as '" + title + "'.\nUse '/sendmail <content>' to continue."); + mailBuilder.constructionStage++; + } + case 1 -> { + String contents = String.join(" ", args.subList(0, args.size())); + mailBuilder.mail.mailContent.content = contents; + CommandHandler.sendMessage(sender, "Message contents set as '" + contents + "'.\nUse '/sendmail <sender>' to continue."); + mailBuilder.constructionStage++; + } + case 2 -> { + String msgSender = String.join(" ", args.subList(0, args.size())); + mailBuilder.mail.mailContent.sender = msgSender; + CommandHandler.sendMessage(sender, "Message sender set as '" + msgSender + "'.\nUse '/sendmail <itemId|itemName|finish> [amount] [level]' to continue."); + mailBuilder.constructionStage++; + } + case 3 -> { + // Literally just copy-pasted from the give command lol. + int item, lvl, amount = 1; + switch (args.size()) { + default -> { // *No args* + CommandHandler.sendMessage(sender, "Usage: give [player] <itemId|itemName> [amount]"); + return; + } + case 1 -> { // <itemId|itemName> + try { + item = Integer.parseInt(args.get(0)); + lvl = 1; + } catch (NumberFormatException ignored) { + // TODO: Parse from item name using GM Handbook. + CommandHandler.sendMessage(sender, "Invalid item id."); + return; + } + } + case 2 -> { // <itemId|itemName> [amount] + lvl = 1; + item = Integer.parseInt(args.get(0)); + amount = Integer.parseInt(args.get(1)); + } + case 3 -> { // <itemId|itemName> [amount] [level] + try { + item = Integer.parseInt(args.get(0)); + amount = Integer.parseInt(args.get(1)); + lvl = Integer.parseInt(args.get(2)); + + } catch (NumberFormatException ignored) { + // TODO: Parse from item name using GM Handbook. + CommandHandler.sendMessage(sender, "Invalid item or player ID."); + return; + } + } + } + mailBuilder.mail.itemList.add(new Mail.MailItem(item, amount, lvl)); + CommandHandler.sendMessage(sender, String.format("Attached %s of %s (level %s) to the message.\nContinue adding more items or use `/sendmail finish` to send the message.", amount, item, lvl)); + } + } + } + } + } else { + CommandHandler.sendMessage(sender, "Invalid arguments \n Please use `/sendmail " + getConstructionArgs(mailBuilder.constructionStage)); + } + } + } + + private String getConstructionArgs(int stage) { + switch (stage) { + case 0 -> { + return "<title>"; + } + case 1 -> { + return "<message>"; + } + case 2 -> { + return "<sender>"; + + } + case 3 -> { + return "<itemId|itemName|finish> [amount] [level]"; + } + default -> { + Thread.dumpStack(); + return "ERROR: invalid construction stage " + stage + ". Check console for stacktrace."; + } + } + } + + public static class MailBuilder { + public int recipient; + public boolean sendToAll; + public int constructionStage; + public Mail mail; + + public MailBuilder(int recipient, Mail mail) { + this.recipient = recipient; + this.sendToAll = false; + this.constructionStage = 0; + this.mail = mail; + } + + public MailBuilder(boolean sendToAll, Mail mail) { + if (sendToAll) { + this.recipient = 0; + this.sendToAll = true; + this.constructionStage = 0; + this.mail = mail; + } else { + Grasscutter.getLogger().error("Please use MailBuilder(int, mail) when not sending to all"); + Thread.dumpStack(); + } + } + } +} diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index fc24b70e6..aa2fbe251 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -90,6 +90,10 @@ public final class DatabaseHelper { return DatabaseManager.getDatastore().find(Account.class).filter(Filters.eq("username", username)).delete().getDeletedCount() > 0; } + public static List<GenshinPlayer> getAllPlayers() { + return DatabaseManager.getDatastore().find(GenshinPlayer.class).stream().toList(); + } + public static GenshinPlayer getPlayerById(int id) { return DatabaseManager.getDatastore().find(GenshinPlayer.class).filter(Filters.eq("_id", id)).first(); } diff --git a/src/main/java/emu/grasscutter/game/GenshinPlayer.java b/src/main/java/emu/grasscutter/game/GenshinPlayer.java index 4c5afb88f..490a8dbdc 100644 --- a/src/main/java/emu/grasscutter/game/GenshinPlayer.java +++ b/src/main/java/emu/grasscutter/game/GenshinPlayer.java @@ -1,8 +1,12 @@ package emu.grasscutter.game; +import java.time.Instant; +import java.util.*; + import dev.morphia.annotations.*; import emu.grasscutter.GenshinConstants; import emu.grasscutter.Grasscutter; +import emu.grasscutter.command.CommandHandler; import emu.grasscutter.data.GenshinData; import emu.grasscutter.data.def.PlayerLevelData; import emu.grasscutter.database.DatabaseHelper; @@ -76,6 +80,7 @@ public class GenshinPlayer { private boolean showAvatar; private ArrayList<AvatarProfileData> shownAvatars; private Set<Integer> rewardedLevels; + private ArrayList<Mail> mail; private int sceneId; private int regionId; @@ -119,6 +124,8 @@ public class GenshinPlayer { this.flyCloakList = new HashSet<>(); this.costumeList = new HashSet<>(); + this.mail = new ArrayList<>(); + this.setSceneId(3); this.setRegionId(1); this.sceneState = SceneLoadState.NONE; @@ -666,6 +673,47 @@ public class GenshinPlayer { this.sendPacket(new PacketPrivateChatNotify(sender.getUid(), this.getUid(), message.toString())); } + // ---------------------MAIL------------------------ + + public List<Mail> 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) { GenshinEntity entity = getScene().getEntityById(gadgetEntityId); diff --git a/src/main/java/emu/grasscutter/game/Mail.java b/src/main/java/emu/grasscutter/game/Mail.java new file mode 100644 index 000000000..3561a110e --- /dev/null +++ b/src/main/java/emu/grasscutter/game/Mail.java @@ -0,0 +1,95 @@ +package emu.grasscutter.game; + +import dev.morphia.annotations.Entity; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +@Entity +public class Mail { + + public MailContent mailContent; + public List<MailItem> itemList; + public long sendTime; + public long expireTime; + public int importance; + public boolean isRead; + public boolean isAttachmentGot; + public int stateValue; + + public Mail() { + this(new MailContent(), new ArrayList<MailItem>(), (int) Instant.now().getEpochSecond() + 604800); // TODO: add expire time to send mail command + } + + public Mail(MailContent mailContent, List<MailItem> itemList, long expireTime) { + this(mailContent, itemList, expireTime, 0); + } + + public Mail(MailContent mailContent, List<MailItem> itemList, long expireTime, int importance) { + this(mailContent, itemList, expireTime, importance, 1); + } + + public Mail(MailContent mailContent, List<MailItem> itemList, long expireTime, int importance, int state) { + this.mailContent = mailContent; + this.itemList = itemList; + this.sendTime = (int) Instant.now().getEpochSecond(); + this.expireTime = expireTime; + this.importance = importance; // Starred mail, 0 = No star, 1 = Star. + this.isRead = false; + this.isAttachmentGot = false; + this.stateValue = state; // Different mailboxes, 1 = Default, 3 = Gift-box. + } + + @Entity + public static class MailContent { + public String title; + public String content; + public String sender; + + public MailContent() { + this.title = ""; + this.content = "loading..."; + this.sender = "loading"; + } + + public MailContent(String title, String content) { + this(title, content, "Server"); + } + + public MailContent(String title, String content, GenshinPlayer sender) { + this(title, content, sender.getNickname()); + } + + public MailContent(String title, String content, String sender) { + this.title = title; + this.content = content; + this.sender = sender; + } + } + + @Entity + public static class MailItem { + public int itemId; + public int itemCount; + public int itemLevel; + + public MailItem() { + this.itemId = 11101; + this.itemCount = 1; + this.itemLevel = 1; + } + + public MailItem(int itemId) { + this(itemId, 1); + } + + public MailItem(int itemId, int itemCount) { this(itemId, itemCount, 1); } + + public MailItem(int itemId, int itemCount, int itemLevel) { + this.itemId = itemId; + this.itemCount = itemCount; + this.itemLevel = itemLevel; + } + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerChangeMailStarNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerChangeMailStarNotify.java new file mode 100644 index 000000000..ede997f27 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerChangeMailStarNotify.java @@ -0,0 +1,34 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.Mail; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.ChangeMailStarNotifyOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketMailChangeNotify; + +import java.util.ArrayList; +import java.util.List; + +@Opcodes(PacketOpcodes.ChangeMailStarNotify) +public class HandlerChangeMailStarNotify extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + ChangeMailStarNotifyOuterClass.ChangeMailStarNotify req = ChangeMailStarNotifyOuterClass.ChangeMailStarNotify.parseFrom(payload); + + List<Mail> updatedMail = new ArrayList<>(); + + for (int mailId : req.getMailIdListList()) { + Mail message = session.getPlayer().getMail(mailId); + + message.importance = req.getIsStar() == true ? 1 : 0; + + session.getPlayer().replaceMailByIndex(mailId, message); + updatedMail.add(message); + } + + session.send(new PacketMailChangeNotify(session.getPlayer(), updatedMail)); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerDelMailReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDelMailReq.java new file mode 100644 index 000000000..4c6473996 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerDelMailReq.java @@ -0,0 +1,21 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.DelMailReqOuterClass; +import emu.grasscutter.net.proto.DeleteFriendReqOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketDelMailRsp; + +@Opcodes(PacketOpcodes.DelMailReq) +public class HandlerDelMailReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + DelMailReqOuterClass.DelMailReq req = DelMailReqOuterClass.DelMailReq.parseFrom(payload); + + session.send(new PacketDelMailRsp(session.getPlayer(), req.getMailIdListList())); + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetAllMailReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetAllMailReq.java new file mode 100644 index 000000000..899ba6d95 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetAllMailReq.java @@ -0,0 +1,21 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.proto.GetAllMailReqOuterClass; +import emu.grasscutter.net.proto.GetPlayerTokenReqOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketGetAllMailRsp; +import emu.grasscutter.server.packet.send.PacketGetGachaInfoRsp; + +@Opcodes(PacketOpcodes.GetAllMailReq) +public class HandlerGetAllMailReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + GetAllMailReqOuterClass.GetAllMailReq req = GetAllMailReqOuterClass.GetAllMailReq.parseFrom(payload); + session.send(new PacketGetAllMailRsp(session.getPlayer(), req.getIsGiftMail())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetMailItemReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetMailItemReq.java new file mode 100644 index 000000000..f00bf911e --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetMailItemReq.java @@ -0,0 +1,20 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.GetMailItemReqOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketGetMailItemRsp; + +@Opcodes(PacketOpcodes.GetMailItemReq) +public class HandlerGetMailItemReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + GetMailItemReqOuterClass.GetMailItemReq req = GetMailItemReqOuterClass.GetMailItemReq.parseFrom(payload); + session.send(new PacketGetMailItemRsp(session.getPlayer(), req.getMailIdListList())); + } + +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerReadMailNotify.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerReadMailNotify.java new file mode 100644 index 000000000..ac5ef9657 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerReadMailNotify.java @@ -0,0 +1,35 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.game.Mail; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.GetAllMailReqOuterClass; +import emu.grasscutter.net.proto.ReadMailNotifyOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketMailChangeNotify; + +import java.util.ArrayList; +import java.util.List; + +@Opcodes(PacketOpcodes.ReadMailNotify) +public class HandlerReadMailNotify extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + ReadMailNotifyOuterClass.ReadMailNotify req = ReadMailNotifyOuterClass.ReadMailNotify.parseFrom(payload); + + List<Mail> updatedMail = new ArrayList<>(); + + for (int mailId : req.getMailIdListList()) { + Mail message = session.getPlayer().getMail(mailId); + + message.isRead = true; + + session.getPlayer().replaceMailByIndex(mailId, message); + updatedMail.add(message); + } + + session.send(new PacketMailChangeNotify(session.getPlayer(), updatedMail)); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketDelMailRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketDelMailRsp.java new file mode 100644 index 000000000..6a11521a5 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketDelMailRsp.java @@ -0,0 +1,29 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.net.packet.GenshinPacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.DelMailRspOuterClass.DelMailRsp; + +import java.util.ArrayList; +import java.util.List; + +public class PacketDelMailRsp extends GenshinPacket { + + public PacketDelMailRsp(GenshinPlayer player, List<Integer> toDeleteIds) { + super(PacketOpcodes.DelMailRsp); + + DelMailRsp.Builder proto = DelMailRsp.newBuilder(); + + List<Integer> deletedIds = new ArrayList<>(); + + for(int mailId : toDeleteIds) { + if(player.deleteMail(mailId)) { + deletedIds.add(mailId); + } + } + + this.setData(proto.build()); + player.getSession().send(new PacketMailChangeNotify(player, null, deletedIds)); + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetAllMailRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetAllMailRsp.java new file mode 100644 index 000000000..1185a0df5 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetAllMailRsp.java @@ -0,0 +1,95 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.Mail; +import emu.grasscutter.net.packet.GenshinPacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.GetAllMailRspOuterClass.GetAllMailRsp; +import emu.grasscutter.net.proto.ItemParamOuterClass; +import emu.grasscutter.net.proto.MailDataOuterClass; +import emu.grasscutter.net.proto.MailDataOuterClass.MailData; +import emu.grasscutter.net.proto.MailItemOuterClass; +import emu.grasscutter.net.proto.MailTextContentOuterClass.MailTextContent; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +public class PacketGetAllMailRsp extends GenshinPacket { + + public PacketGetAllMailRsp(GenshinPlayer player, boolean isGiftMail) { + super(PacketOpcodes.GetAllMailRsp); + + if (isGiftMail) { + // TODO: Gift Mail + // Make sure to send the stupid empty packet + Base64.Decoder decoder = Base64.getDecoder(); + byte[] rsp = decoder.decode("IAE="); + try { + GetAllMailRsp var = GetAllMailRsp.parseFrom(rsp); + this.setData(var.toBuilder().build()); + } catch (Exception e) { + } + + } else { + if (player.getAllMail().size() != 0) { // Make sure the player has mail + GetAllMailRsp.Builder proto = GetAllMailRsp.newBuilder(); + List<MailData> mailDataList = new ArrayList<MailData>(); + + for (Mail message : player.getAllMail()) { + + if(message.stateValue == 1) { // Make sure it isn't a gift + if (message.expireTime > (int) Instant.now().getEpochSecond()) { // Make sure the message isn't expired (The game won't show expired mail, but I don't want to send unnecessary information). + if(mailDataList.size() <= 1000) { // Make sure that there isn't over 1000 messages in the mailbox. (idk what will happen if there is but the game probably won't like it.) + MailTextContent.Builder mailTextContent = MailTextContent.newBuilder(); + mailTextContent.setTitle(message.mailContent.title); + mailTextContent.setContent(message.mailContent.content); + mailTextContent.setSender(message.mailContent.sender); + + List<MailItemOuterClass.MailItem> mailItems = new ArrayList<>(); + + for (Mail.MailItem item : message.itemList) { + MailItemOuterClass.MailItem.Builder mailItem = MailItemOuterClass.MailItem.newBuilder(); + ItemParamOuterClass.ItemParam.Builder itemParam = ItemParamOuterClass.ItemParam.newBuilder(); + itemParam.setItemId(item.itemId); + itemParam.setCount(item.itemCount); + mailItem.setItemParam(itemParam.build()); + + mailItems.add(mailItem.build()); + } + + MailDataOuterClass.MailData.Builder mailData = MailDataOuterClass.MailData.newBuilder(); + mailData.setMailId(player.getMailId(message)); + mailData.setMailTextContent(mailTextContent.build()); + mailData.addAllItemList(mailItems); + mailData.setSendTime((int) message.sendTime); + mailData.setExpireTime((int) message.expireTime); + mailData.setImportance(message.importance); + mailData.setIsRead(message.isRead); + mailData.setIsAttachmentGot(message.isAttachmentGot); + mailData.setStateValue(1); + + mailDataList.add(mailData.build()); + } + } + } + } + + proto.addAllMailList(mailDataList); + proto.setIsTruncated(mailDataList.size() <= 1000 ? false : true); // When enabled this will send a notification to the user telling them their inbox is full and they should delete old messages when opening the mailbox. + + this.setData(proto.build()); + } else { + // Make sure to send the stupid empty packet + Base64.Decoder decoder = Base64.getDecoder(); + byte[] rsp = decoder.decode("IAE="); + try { + GetAllMailRsp var = GetAllMailRsp.parseFrom(rsp); + this.setData(var.toBuilder().build()); + } catch (Exception e) {} + } + } + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetMailItemRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetMailItemRsp.java new file mode 100644 index 000000000..a2379545b --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetMailItemRsp.java @@ -0,0 +1,73 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.data.GenshinData; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.Mail; +import emu.grasscutter.game.inventory.GenshinItem; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.net.packet.GenshinPacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.EquipParamOuterClass; +import emu.grasscutter.net.proto.GetMailItemRspOuterClass.GetMailItemRsp; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class PacketGetMailItemRsp extends GenshinPacket { + + public PacketGetMailItemRsp(GenshinPlayer player, List<Integer> mailList) { + super(PacketOpcodes.GetMailItemRsp); + + List<Mail> claimedMessages = new ArrayList<>(); + List<EquipParamOuterClass.EquipParam> claimedItems = new ArrayList<>(); + + GetMailItemRsp.Builder proto = GetMailItemRsp.newBuilder(); + + for (int mailId : mailList) { + Mail message = player.getMail(mailId); + + for(Mail.MailItem mailItem : message.itemList) { + EquipParamOuterClass.EquipParam.Builder item = EquipParamOuterClass.EquipParam.newBuilder(); + int promoteLevel = 0; + if (mailItem.itemLevel > 20) { // 20/40 + promoteLevel = 1; + } else if (mailItem.itemLevel > 40) { // 40/50 + promoteLevel = 2; + } else if (mailItem.itemLevel > 50) { // 50/60 + promoteLevel = 3; + } else if (mailItem.itemLevel > 60) { // 60/70 + promoteLevel = 4; + } else if (mailItem.itemLevel > 70) { // 70/80 + promoteLevel = 5; + } else if (mailItem.itemLevel > 80) { // 80/90 + promoteLevel = 6; + } + + item.setItemId(mailItem.itemId); + item.setItemNum(mailItem.itemCount); + item.setItemLevel(mailItem.itemLevel); + item.setPromoteLevel(promoteLevel); + claimedItems.add(item.build()); + + GenshinItem genshinItem = new GenshinItem(GenshinData.getItemDataMap().get(mailItem.itemId)); + genshinItem.setCount(mailItem.itemCount); + genshinItem.setLevel(mailItem.itemLevel); + genshinItem.setPromoteLevel(promoteLevel); + player.getInventory().addItem(genshinItem); + player.sendPacket(new PacketItemAddHintNotify(genshinItem, ActionReason.MailAttachment)); + } + + message.isAttachmentGot = true; + claimedMessages.add(message); + + player.replaceMailByIndex(mailId, message); + } + + proto.addAllMailIdList(claimedMessages.stream().map(message -> player.getMailId(message)).collect(Collectors.toList())); + proto.addAllItemList(claimedItems); + + this.setData(proto.build()); + player.getSession().send(new PacketMailChangeNotify(player, claimedMessages)); // For some reason you have to also send the MailChangeNotify packet + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketMailChangeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketMailChangeNotify.java new file mode 100644 index 000000000..f7d50eb25 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketMailChangeNotify.java @@ -0,0 +1,69 @@ +package emu.grasscutter.server.packet.send; + + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.Mail; +import emu.grasscutter.net.packet.GenshinPacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.*; + +import java.util.ArrayList; +import java.util.List; + +public class PacketMailChangeNotify extends GenshinPacket { + + public PacketMailChangeNotify(GenshinPlayer player, Mail message) { + this (player, new ArrayList<Mail>(){{add(message);}}); + } + + public PacketMailChangeNotify(GenshinPlayer player, List<Mail> mailList) { + this(player, mailList, null); + } + + public PacketMailChangeNotify(GenshinPlayer player, List<Mail> mailList, List<Integer> delMailIdList) { + super(PacketOpcodes.MailChangeNotify); + + MailChangeNotifyOuterClass.MailChangeNotify.Builder proto = MailChangeNotifyOuterClass.MailChangeNotify.newBuilder(); + + if (mailList != null) { + for (Mail message : mailList) { + MailTextContentOuterClass.MailTextContent.Builder mailTextContent = MailTextContentOuterClass.MailTextContent.newBuilder(); + mailTextContent.setTitle(message.mailContent.title); + mailTextContent.setContent(message.mailContent.content); + mailTextContent.setSender(message.mailContent.sender); + + List<MailItemOuterClass.MailItem> mailItems = new ArrayList<MailItemOuterClass.MailItem>(); + + for (Mail.MailItem item : message.itemList) { + MailItemOuterClass.MailItem.Builder mailItem = MailItemOuterClass.MailItem.newBuilder(); + ItemParamOuterClass.ItemParam.Builder itemParam = ItemParamOuterClass.ItemParam.newBuilder(); + itemParam.setItemId(item.itemId); + itemParam.setCount(item.itemCount); + mailItem.setItemParam(itemParam.build()); + + mailItems.add(mailItem.build()); + } + + MailDataOuterClass.MailData.Builder mailData = MailDataOuterClass.MailData.newBuilder(); + mailData.setMailId(player.getMailId(message)); + mailData.setMailTextContent(mailTextContent.build()); + mailData.addAllItemList(mailItems); + mailData.setSendTime((int) message.sendTime); + mailData.setExpireTime((int) message.expireTime); + mailData.setImportance(message.importance); + mailData.setIsRead(message.isRead); + mailData.setIsAttachmentGot(message.isAttachmentGot); + mailData.setStateValue(message.stateValue); + + proto.addMailList(mailData.build()); + } + } + + if(delMailIdList != null) { + proto.addAllDelMailIdList(delMailIdList); + } + + this.setData(proto.build()); + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerApplyEnterMpResultNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerApplyEnterMpResultNotify.java index c55ee3b12..e164b5d0b 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerApplyEnterMpResultNotify.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketPlayerApplyEnterMpResultNotify.java @@ -4,11 +4,12 @@ import emu.grasscutter.game.GenshinPlayer; import emu.grasscutter.net.packet.GenshinPacket; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.PlayerApplyEnterMpReasonOuterClass.PlayerApplyEnterMpReason; +import emu.grasscutter.net.proto.PlayerApplyEnterMpResultNotifyOuterClass; import emu.grasscutter.net.proto.PlayerApplyEnterMpResultNotifyOuterClass.PlayerApplyEnterMpResultNotify; public class PacketPlayerApplyEnterMpResultNotify extends GenshinPacket { - public PacketPlayerApplyEnterMpResultNotify(GenshinPlayer target, boolean isAgreed, PlayerApplyEnterMpResultNotify.Reason reason) { + public PacketPlayerApplyEnterMpResultNotify(GenshinPlayer target, boolean isAgreed, PlayerApplyEnterMpResultNotifyOuterClass.PlayerApplyEnterMpResultNotify.Reason reason) { super(PacketOpcodes.PlayerApplyEnterMpResultNotify); PlayerApplyEnterMpResultNotify proto = PlayerApplyEnterMpResultNotify.newBuilder()