From d4a7aa0320902fafdcb2203e423c224d113fae6e Mon Sep 17 00:00:00 2001 From: Melledy <121644117+Melledy@users.noreply.github.com> Date: Tue, 11 Nov 2025 23:47:07 -0800 Subject: [PATCH] Implement Heartlink --- src/main/java/emu/nebula/data/GameData.java | 2 + .../nebula/data/resources/CharacterDef.java | 10 ++ .../emu/nebula/data/resources/ChatDef.java | 36 ++++ .../emu/nebula/game/character/Character.java | 22 ++- .../nebula/game/character/CharacterChat.java | 49 ++++++ .../game/character/CharacterContact.java | 154 ++++++++++++++++++ .../game/character/CharacterStorage.java | 12 ++ .../java/emu/nebula/game/player/Player.java | 5 +- .../handlers/HandlerPhoneContactsInfoReq.java | 11 +- .../HandlerPhoneContactsReportReq.java | 41 +++++ .../handlers/HandlerPhoneContactsTopReq.java | 30 ++++ 11 files changed, 366 insertions(+), 6 deletions(-) create mode 100644 src/main/java/emu/nebula/data/resources/ChatDef.java create mode 100644 src/main/java/emu/nebula/game/character/CharacterChat.java create mode 100644 src/main/java/emu/nebula/game/character/CharacterContact.java create mode 100644 src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsReportReq.java create mode 100644 src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsTopReq.java diff --git a/src/main/java/emu/nebula/data/GameData.java b/src/main/java/emu/nebula/data/GameData.java index 0af9899..100350d 100644 --- a/src/main/java/emu/nebula/data/GameData.java +++ b/src/main/java/emu/nebula/data/GameData.java @@ -28,6 +28,8 @@ public class GameData { @Getter private static DataTable TalentGroupDataTable = new DataTable<>(); @Getter private static DataTable TalentDataTable = new DataTable<>(); + @Getter private static DataTable ChatDataTable = new DataTable<>(); + // Discs @Getter private static DataTable DiscDataTable = new DataTable<>(); @Getter private static DataTable DiscStrengthenDataTable = new DataTable<>(); diff --git a/src/main/java/emu/nebula/data/resources/CharacterDef.java b/src/main/java/emu/nebula/data/resources/CharacterDef.java index 902101a..33c719e 100644 --- a/src/main/java/emu/nebula/data/resources/CharacterDef.java +++ b/src/main/java/emu/nebula/data/resources/CharacterDef.java @@ -1,7 +1,10 @@ package emu.nebula.data.resources; +import java.util.List; + import emu.nebula.data.BaseDef; import emu.nebula.data.ResourceType; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; import lombok.Getter; @Getter @@ -21,6 +24,8 @@ public class CharacterDef extends BaseDef { private int FragmentsId; private int TransformQty; + private transient List chats; + @Override public int getId() { return Id; @@ -33,4 +38,9 @@ public class CharacterDef extends BaseDef { return this.SkillsUpgradeGroup[index]; } + + @Override + public void onLoad() { + this.chats = new ObjectArrayList<>(); + } } diff --git a/src/main/java/emu/nebula/data/resources/ChatDef.java b/src/main/java/emu/nebula/data/resources/ChatDef.java new file mode 100644 index 0000000..08cf7e0 --- /dev/null +++ b/src/main/java/emu/nebula/data/resources/ChatDef.java @@ -0,0 +1,36 @@ +package emu.nebula.data.resources; + +import emu.nebula.data.BaseDef; +import emu.nebula.data.GameData; +import emu.nebula.data.ResourceType; +import emu.nebula.data.ResourceType.LoadPriority; +import lombok.Getter; + +@Getter +@ResourceType(name = "Chat.json", loadPriority = LoadPriority.LOW) +public class ChatDef extends BaseDef { + private int Id; + private int AddressBookId; + private int PreChatId; + + private int TriggerType; + private int TriggerCond; + private String TriggerCondParam; + + private int Reward1; + private int RewardQty1; + + @Override + public int getId() { + return Id; + } + + @Override + public void onLoad() { + var character = GameData.getCharacterDataTable().get(this.AddressBookId); + + if (character != null) { + character.getChats().add(this); + } + } +} diff --git a/src/main/java/emu/nebula/game/character/Character.java b/src/main/java/emu/nebula/game/character/Character.java index 9d522eb..4a95172 100644 --- a/src/main/java/emu/nebula/game/character/Character.java +++ b/src/main/java/emu/nebula/game/character/Character.java @@ -49,7 +49,7 @@ public class Character implements GameDatabaseObject { private int skin; private int[] skills; private Bitset talents; - + private CharacterContact contact; private long createTime; @Deprecated // Morphia only! @@ -66,11 +66,13 @@ public class Character implements GameDatabaseObject { this.playerUid = player.getUid(); this.charId = data.getId(); this.data = data; + this.createTime = Nebula.getCurrentTime(); + this.level = 1; this.skin = data.getDefaultSkinId(); this.skills = new int[] {1, 1, 1, 1, 1}; this.talents = new Bitset(); - this.createTime = Nebula.getCurrentTime(); + this.contact = new CharacterContact(this); } public void setPlayer(Player player) { @@ -78,8 +80,20 @@ public class Character implements GameDatabaseObject { } public void setData(CharacterDef data) { - if (this.data == null && data.getId() == this.getCharId()) { - this.data = data; + // Sanity check + if (this.data != null || data.getId() != this.getCharId()) { + return; + } + + // Set data + this.data = data; + + // Check contacts + if (this.contact == null) { + this.contact = new CharacterContact(this); + this.save(); + } else { + this.contact.setCharacter(this); } } diff --git a/src/main/java/emu/nebula/game/character/CharacterChat.java b/src/main/java/emu/nebula/game/character/CharacterChat.java new file mode 100644 index 0000000..56e1ac0 --- /dev/null +++ b/src/main/java/emu/nebula/game/character/CharacterChat.java @@ -0,0 +1,49 @@ +package emu.nebula.game.character; + +import dev.morphia.annotations.Entity; +import emu.nebula.data.resources.ChatDef; +import emu.nebula.proto.Public.Chat; +import lombok.Getter; +import us.hebi.quickbuf.RepeatedInt; + +@Getter +@Entity(useDiscriminator = false) +public class CharacterChat { + private int id; + private int process; + private boolean end; + + private int[] options; + + @Deprecated // Morphia only + public CharacterChat() { + + } + + public CharacterChat(ChatDef data) { + this.id = data.getId(); + } + + // Proto + + public Chat toProto() { + var proto = Chat.newInstance() + .setId(this.getId()) + .setProcess(this.getProcess()); + + if (this.getOptions() != null) { + proto.addAllOptions(this.getOptions()); + } + + return proto; + } + + public void report(int process, RepeatedInt options, boolean end) { + this.process = process; + this.end = end; + + if (options.length() > 0 && options.length() <= 5) { + this.options = options.toArray(); + } + } +} diff --git a/src/main/java/emu/nebula/game/character/CharacterContact.java b/src/main/java/emu/nebula/game/character/CharacterContact.java new file mode 100644 index 0000000..d09b771 --- /dev/null +++ b/src/main/java/emu/nebula/game/character/CharacterContact.java @@ -0,0 +1,154 @@ +package emu.nebula.game.character; + +import java.util.HashMap; +import java.util.Map; + +import dev.morphia.annotations.Entity; +import emu.nebula.Nebula; +import emu.nebula.data.resources.ChatDef; +import emu.nebula.game.player.PlayerChangeInfo; +import emu.nebula.net.NetMsgId; +import emu.nebula.proto.Public.Contacts; +import emu.nebula.proto.Public.UI32; +import lombok.Getter; +import us.hebi.quickbuf.RepeatedInt; + +@Getter +@Entity(useDiscriminator = false) +public class CharacterContact { + private transient Character character; + + private boolean top; + private long triggerTime; + private Map chats; + + @Deprecated // Morphia only + public CharacterContact() { + + } + + public CharacterContact(Character character) { + this.character = character; + this.chats = new HashMap<>(); + this.triggerTime = character.getCreateTime(); + + // Get starter chat + for (var chatData : character.getData().getChats()) { + // Skip chats that have a requirement + if (chatData.getPreChatId() != 0) { + continue; + } + + // Add chat for character + var chat = new CharacterChat(chatData); + this.getChats().put(chat.getId(), chat); + } + } + + public void setCharacter(Character character) { + this.character = character; + } + + public void addNextChat() { + // Find next chat data + ChatDef nextChatData = null; + + for (var chatData : getCharacter().getData().getChats()) { + // Skip chats that we have added + if (this.getChats().containsKey(chatData.getId())) { + continue; + } + + nextChatData = chatData; + break; + } + + // Skip if we cant find any new chat data + if (nextChatData == null) { + return; + } + + // Add chat for character + var chat = new CharacterChat(nextChatData); + this.getChats().put(chat.getId(), chat); + + // Send packet + this.getCharacter().getPlayer().addNextPackage( + NetMsgId.phone_chat_change_notify, + UI32.newInstance().setValue(chat.getId()) + ); + } + + public CharacterChat getChatById(int chatId) { + return this.getChats().get(chatId); + } + + public boolean hasNew() { + return this.getChats().values() + .stream() + .anyMatch(c -> !c.isEnd()); + } + + public PlayerChangeInfo report(ChatDef chatData, int process, RepeatedInt options, boolean end) { + // Get chat + var chat = this.getChatById(chatData.getId()); + if (chat == null) { + return null; + } + + // Player change info + var change = new PlayerChangeInfo(); + + // Sanity check + if (chat.isEnd()) { + return change; + } + + // Report + chat.report(process, options, end); + + // Set trigger time + this.triggerTime = Nebula.getCurrentTime(); + + // Add next chat + this.addNextChat(); + + // TODO save more efficiently + this.getCharacter().save(); + + // Add rewards + if (chat.isEnd()) { + getCharacter().getPlayer().getInventory().addItem( + chatData.getReward1(), + chatData.getRewardQty1(), + change + ); + } + + // Success + return change.setSuccess(true); + } + + public void toggleTop() { + // Toggle + this.top = !this.top; + + // TODO save more efficiently + this.getCharacter().save(); + } + + // Proto + + public Contacts toProto() { + var proto = Contacts.newInstance() + .setCharId(this.getCharacter().getCharId()) + .setTop(this.isTop()) + .setTriggerTime(this.getTriggerTime()); + + for (var chat : this.getChats().values()) { + proto.addChats(chat.toProto()); + } + + return proto; + } +} diff --git a/src/main/java/emu/nebula/game/character/CharacterStorage.java b/src/main/java/emu/nebula/game/character/CharacterStorage.java index 119e997..d8de760 100644 --- a/src/main/java/emu/nebula/game/character/CharacterStorage.java +++ b/src/main/java/emu/nebula/game/character/CharacterStorage.java @@ -70,6 +70,18 @@ public class CharacterStorage extends PlayerManager { return this.getCharacters().values(); } + public int getNewPhoneMessageCount() { + int count = 0; + + for (var character : this.getCharacterCollection()) { + if (character.getContact().hasNew()) { + count++; + } + } + + return count; + } + public HandbookInfo getCharacterHandbook() { var bitset = new Bitset(); diff --git a/src/main/java/emu/nebula/game/player/Player.java b/src/main/java/emu/nebula/game/player/Player.java index a121aa0..50ba829 100644 --- a/src/main/java/emu/nebula/game/player/Player.java +++ b/src/main/java/emu/nebula/game/player/Player.java @@ -710,9 +710,12 @@ public class Player implements GameDatabaseObject { proto.addHandbook(this.getCharacters().getCharacterHandbook()); proto.addHandbook(this.getCharacters().getDiscHandbook()); + // Phone + var phone = proto.getMutablePhone(); + phone.setNewMessage(this.getCharacters().getNewPhoneMessageCount()); + // Extra proto.getMutableAgent(); - proto.getMutablePhone(); return proto; } diff --git a/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsInfoReq.java b/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsInfoReq.java index ed34da7..08efa6f 100644 --- a/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsInfoReq.java +++ b/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsInfoReq.java @@ -2,6 +2,7 @@ package emu.nebula.server.handlers; import emu.nebula.net.NetHandler; import emu.nebula.net.NetMsgId; +import emu.nebula.proto.PhoneContactsInfo.PhoneContactsInfoResp; import emu.nebula.net.HandlerId; import emu.nebula.net.GameSession; @@ -10,7 +11,15 @@ public class HandlerPhoneContactsInfoReq extends NetHandler { @Override public byte[] handle(GameSession session, byte[] message) throws Exception { - return session.encodeMsg(NetMsgId.phone_contacts_info_succeed_ack); + // Build response + var rsp = PhoneContactsInfoResp.newInstance(); + + for (var character : session.getPlayer().getCharacters().getCharacterCollection()) { + rsp.addList(character.getContact().toProto()); + } + + // Encode and send + return session.encodeMsg(NetMsgId.phone_contacts_info_succeed_ack, rsp); } } diff --git a/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsReportReq.java b/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsReportReq.java new file mode 100644 index 0000000..a03e174 --- /dev/null +++ b/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsReportReq.java @@ -0,0 +1,41 @@ +package emu.nebula.server.handlers; + +import emu.nebula.net.NetHandler; +import emu.nebula.net.NetMsgId; +import emu.nebula.proto.PhoneContactsReport.PhoneContactsReportReq; +import emu.nebula.net.HandlerId; +import emu.nebula.data.GameData; +import emu.nebula.net.GameSession; + +@HandlerId(NetMsgId.phone_contacts_report_req) +public class HandlerPhoneContactsReportReq extends NetHandler { + + @Override + public byte[] handle(GameSession session, byte[] message) throws Exception { + // Parse request + var req = PhoneContactsReportReq.parseFrom(message); + + // Get chat data + var data = GameData.getChatDataTable().get(req.getChatId()); + if (data == null) { + return session.encodeMsg(NetMsgId.phone_contacts_report_failed_ack); + } + + // Get character + var character = session.getPlayer().getCharacters().getCharacterById(data.getAddressBookId()); + if (character == null) { + return session.encodeMsg(NetMsgId.phone_contacts_report_failed_ack); + } + + // Handle report + var change = character.getContact().report(data, req.getProcess(), req.getOptions(), req.getEnd()); + + if (change == null) { + return session.encodeMsg(NetMsgId.phone_contacts_report_failed_ack); + } + + // Encode and send + return session.encodeMsg(NetMsgId.phone_contacts_report_succeed_ack, change.toProto()); + } + +} diff --git a/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsTopReq.java b/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsTopReq.java new file mode 100644 index 0000000..9d8152f --- /dev/null +++ b/src/main/java/emu/nebula/server/handlers/HandlerPhoneContactsTopReq.java @@ -0,0 +1,30 @@ +package emu.nebula.server.handlers; + +import emu.nebula.net.NetHandler; +import emu.nebula.net.NetMsgId; +import emu.nebula.proto.Public.UI32; +import emu.nebula.net.HandlerId; +import emu.nebula.net.GameSession; + +@HandlerId(NetMsgId.phone_contacts_top_req) +public class HandlerPhoneContactsTopReq extends NetHandler { + + @Override + public byte[] handle(GameSession session, byte[] message) throws Exception { + // Parse request + var req = UI32.parseFrom(message); + + // Get character + var character = session.getPlayer().getCharacters().getCharacterById(req.getValue()); + if (character == null) { + return session.encodeMsg(NetMsgId.phone_contacts_top_failed_ack); + } + + // Toggle chat status + character.getContact().toggleTop(); + + // Encode and send + return session.encodeMsg(NetMsgId.phone_contacts_top_succeed_ack); + } + +}