Implement dorm

This commit is contained in:
Melledy committed 2026-10-20 03:05:50 -07:00
1 parent c585417b85
commit bd4aabd464
47 files changed
+2364 -7

No files matched your search

+2
View File
@@ -21,12 +21,14 @@ Latest nightly compiled server jar: [Nightly](https://nightly.link/Melledy/Nebul
- Commissions
- Heartlink
- Achievements
- Tower (dorm system)
- Monoliths
- Bounty Trials
- Menance Arena
- Proving Grounds
- Catacylsm Survivor (talents not fully working)
- Boss Blitz
- Shadow Siege
- Events (Only tower defense and trials. Other events may work)
### Supported regions
@@ -71,6 +71,25 @@ public class GameConstants {
public static final int[] TRACE_HUNT_TRACE_IDS = {21, 22, 23, 31, 41};
public static final int TRACE_HUNT_MAX_TRACE_PROGRESS = 10_000;
public static final int TRACE_HUNT_MAX_HUNT_PROGRESS = 10_000;
// Dorm
public static final int DORM_MAX_PRESET_NUM = 20;
public static final int DORM_DEFAULT_PRESET_NUM = 10;
public static final int DORM_MAX_ROOM_NUM = 4;
public static final int DORM_INVITATION_ROLE_NORMAL_NUM = 3;
public static final int DORM_INVITATION_ROLE_RAND_NUM = 1;
public static final int DORM_ROLE_INTERACTION_MAX_NUM = 5;
public static final int DORM_ROLE_INTERACTION_DROP_ID = 4002;
public static final int DORM_ROLE_FAVORABILITY = 300;
public static final int DORM_ROLE_AFFINITY_GIFT_MAX_NUM = 5;
public static final int DORM_ROLE_AFFINITY_GIFT_DAILY_NUM = 1;
public static final int DORM_NAME_RESET_TIME_LIMIT = 604_800;
public static final int DORM_LIKE_LIMIT = 3;
public static final int DORM_BLOCKS_CONVERT_PROP = 1;
public static final int DORM_DEFAULT_ROOM_ID = 701001;
public static final int DORM_JADE_ITEM_ID = 2;
public static final int DORM_FURNITURE_COIN_ITEM_ID = 701;
public static final int DORM_MAX_NAME_LENGTH = 20;
// Etc
public static final int REFRESH_TYPE_DAILY = 1;
@@ -146,6 +146,21 @@ public class GameData {
@Getter private static DataTable<TraceHuntLevelDef> TraceHuntLevelDataTable = new DataTable<>();
@Getter private static DataTable<TraceHuntSelfHuntExtraCostDef> TraceHuntSelfHuntExtraCostDataTable = new DataTable<>();
// ===== Dorm =====
@Getter private static DataTable<DormRoomDef> DormRoomDataTable = new DataTable<>();
@Getter private static DataTable<DormFurnitureDef> DormFurnitureDataTable = new DataTable<>();
@Getter private static DataTable<DormLayoutRewardDef> DormLayoutRewardDataTable = new DataTable<>();
@Getter private static DataTable<DormRoomLayoutDef> DormRoomLayoutDataTable = new DataTable<>();
@Getter private static DataTable<DormRewardDef> DormRewardDataTable = new DataTable<>();
@Getter private static DataTable<DormAchievementDef> DormAchievementDataTable = new DataTable<>();
@Getter private static DataTable<DormHonorTitleDef> DormHonorTitleDataTable = new DataTable<>();
@Getter private static DataTable<DormRoleDef> DormRoleDataTable = new DataTable<>();
@Getter private static DataTable<DormRoleSkinDef> DormRoleSkinDataTable = new DataTable<>();
@Getter private static DataTable<DormInteractiveDef> DormInteractiveDataTable = new DataTable<>();
@Getter private static DataTable<DormQuestDef> DormQuestDataTable = new DataTable<>();
@Getter private static DataTable<DormQuestGroupDef> DormQuestGroupDataTable = new DataTable<>();
@Getter private static DataTable<DormRoomSceneDef> DormRoomSceneDataTable = new DataTable<>();
// ===== Misc =====
@Getter private static DataTable<WorldClassDef> WorldClassDataTable = new DataTable<>();
@Getter private static DataTable<GuideGroupDef> GuideGroupDataTable = new DataTable<>();
@@ -0,0 +1,24 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormAchievement.json")
public class DormAchievementDef extends BaseDef {
private int Id;
private int CompleteCond;
private int[] CompleteParams;
private int Tid1;
private int Qty1;
private int Tid2;
private int Qty2;
private int Tid3;
private int Qty3;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,17 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormFurniture.json")
public class DormFurnitureDef extends BaseDef {
private int Id;
private int CollectionValue;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,17 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormHonorTitle.json")
public class DormHonorTitleDef extends BaseDef {
private int Id;
private int NeedExp;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,20 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormInteractive.json")
public class DormInteractiveDef extends BaseDef {
private int Id;
private int CharId;
private int FurniturnId;
private int DormInterType;
private String AnimationConfig;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,20 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormLayoutReward.json")
public class DormLayoutRewardDef extends BaseDef {
private int Id;
private int RoomType;
private int FurnitureCount;
private int RewardId1;
private int Qty1;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,22 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormQuest.json")
public class DormQuestDef extends BaseDef {
private int Id;
private int DormQuestGroup;
private String Title;
private int Reward1;
private int RewardQty1;
private int Reward2;
private int RewardQty2;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,24 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormQuestGroup.json")
public class DormQuestGroupDef extends BaseDef {
private int Id;
private int PreGroup;
private String Title;
private int Item1;
private int Qty1;
private int Item2;
private int Qty2;
private int Item3;
private int Qty3;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,56 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import emu.nebula.util.JsonUtils;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
@ResourceType(name = "DormReward.json")
public class DormRewardDef extends BaseDef {
private int Id;
private int DropId;
private String Conditions;
private transient int[][] parsedConditions;
@Override
public int getId() {
return Id;
}
@Override
public void onLoad() {
this.parsedConditions = JsonUtils.decode(this.Conditions, int[][].class);
}
/**
* Checks the requirements for this dorm reward.
* Condition type 1 = the player needs to have the room unlocked.
*/
public boolean isUnlocked(IntSet unlockedRoomIds) {
if (this.parsedConditions == null) {
return false;
}
for (int[] condition : this.parsedConditions) {
if (condition.length < 2) {
return false;
}
switch (condition[0]) {
case 1 -> {
if (!unlockedRoomIds.contains(condition[1])) {
return false;
}
}
default -> {
return false;
}
}
}
return true;
}
}
@@ -0,0 +1,18 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormRole.json")
public class DormRoleDef extends BaseDef {
private int Id;
private int RoleType;
private int[] Exclusions;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,19 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormRoleSkin.json")
public class DormRoleSkinDef extends BaseDef {
private int Id;
private int TrId;
private int TrQty;
private int UnlockChar;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,24 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormRoom.json")
public class DormRoomDef extends BaseDef {
private int Id;
private int RoomType;
private int RoomSizeType;
private int SceneRes;
private String DefaultName;
private int DefaultLayoutId;
private boolean DefaultCanUse;
private int RoleNum;
private int FloorRatio;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,21 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormRoomLayout.json")
public class DormRoomLayoutDef extends BaseDef {
private int Id;
private int RoomLayoutId;
private int FurnitureId;
private int[] Pos;
private int Rot;
private int ApplyRoomType;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,16 @@
package emu.nebula.data.resources;
import emu.nebula.data.BaseDef;
import emu.nebula.data.ResourceType;
import lombok.Getter;
@Getter
@ResourceType(name = "DormRoomScene.json")
public class DormRoomSceneDef extends BaseDef {
private int Id;
@Override
public int getId() {
return Id;
}
}
@@ -0,0 +1,49 @@
package emu.nebula.game.dorm;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.PublicDorm.FurnitureInfo;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity(useDiscriminator = false)
public class DormFurniture {
private int furnitureId;
private int x;
private int y;
private int z;
private int rotation;
@Deprecated // Morphia only
public DormFurniture() {
}
public DormFurniture(int furnitureId, int x, int y, int z, int rotation) {
this.furnitureId = furnitureId;
this.x = x;
this.y = y;
this.z = z;
this.rotation = rotation;
}
public DormFurniture(FurnitureInfo info) {
this.furnitureId = info.getFurnitureId();
this.x = info.getX();
this.y = info.getY();
this.z = info.getZ();
this.rotation = info.getRotation();
}
// Proto
public FurnitureInfo toProto() {
return FurnitureInfo.newInstance()
.setFurnitureId(this.furnitureId)
.setX(this.x)
.setY(this.y)
.setZ(this.z)
.setRotation(this.rotation);
}
}
@@ -0,0 +1,1000 @@
package emu.nebula.game.dorm;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.DormRoomDef;
import emu.nebula.data.resources.DropPkgDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.quest.QuestCondition;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormInfo.DormInfoListResp;
import emu.nebula.proto.Public;
import emu.nebula.proto.PublicDorm.AchievementComplete;
import emu.nebula.proto.PublicDorm.AchievementInfo;
import emu.nebula.proto.PublicDorm.DormAffinityGiftInfo;
import emu.nebula.proto.PublicDorm.DormHot;
import emu.nebula.proto.PublicDorm.DormInfoData;
import emu.nebula.proto.PublicDorm.DormRoomUnlock;
import emu.nebula.proto.PublicDorm.RoleInteraction;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
@Entity(value = "dorm", useDiscriminator = false)
public class DormManager extends PlayerManager implements GameDatabaseObject {
@Id
private int uid;
// Dorm details
private String dormName;
private long dormNameRestTime;
// Furniture collection
private Int2IntMap furnitureCollection;
// Saved room state
private Map<Integer, DormRoomState> rooms;
private Map<Integer, DormPreset> presets;
// Rewards / achievements
private IntSet receivedLayoutRewardIds;
private IntSet receivedRewardIds;
private Map<Integer, Long> completedAchievements;
// Social stats
private int likeNum;
private int dayVisitNum;
private int visitTotalNum;
// Daily state
private long lastResetDay;
private IntSet likedFriendUids;
private IntSet dailyVisitorUids;
private IntSet dailyInteractionRoleIds;
private Int2IntMap dailyAffinityGiftCounts;
private int dailyAffinityGiftTotal;
private transient boolean queueSave;
@Deprecated // Morphia only
public DormManager() {
}
public DormManager(Player player) {
super(player);
this.uid = player.getUid();
this.dormName = "";
this.furnitureCollection = new Int2IntOpenHashMap();
this.presets = new HashMap<>();
this.rooms = new HashMap<>();
this.receivedLayoutRewardIds = new IntOpenHashSet();
this.receivedRewardIds = new IntOpenHashSet();
this.completedAchievements = new HashMap<>();
this.likedFriendUids = new IntOpenHashSet();
this.dailyVisitorUids = new IntOpenHashSet();
this.dailyInteractionRoleIds = new IntOpenHashSet();
this.dailyAffinityGiftCounts = new Int2IntOpenHashMap();
this.lastResetDay = Utils.getResetEpochDay();
this.save();
}
public int getState() {
return this.getReceivedRewardIds().isEmpty() ? 1 : 0;
}
// Daily reset
public synchronized void resetDailies() {
long currentDay = Utils.getResetEpochDay();
if (currentDay <= this.lastResetDay) {
return;
}
this.likedFriendUids.clear();
this.dailyVisitorUids.clear();
this.dailyInteractionRoleIds.clear();
this.dailyAffinityGiftCounts.clear();
this.dailyAffinityGiftTotal = 0;
this.dayVisitNum = 0;
this.lastResetDay = currentDay;
this.queueSave = true;
}
// Rooms
/**
* Returns the ids of all rooms the player has access to. Rooms are unlocked
* by acquiring the room item, which creates the room state in {@link #rooms}.
*/
public IntSet getUnlockedRoomIds() {
var result = new IntOpenHashSet();
// Rooms that are available by default
for (var room : GameData.getDormRoomDataTable()) {
if (room.isDefaultCanUse()) {
result.add(room.getId());
}
}
// Rooms that have been unlocked by the player
if (this.rooms != null) {
result.addAll(this.rooms.keySet());
}
return result;
}
public boolean isRoomUnlocked(int roomId) {
return this.getUnlockedRoomIds().contains(roomId);
}
public synchronized DormRoomState getOrCreateRoom(int roomId) {
var room = this.rooms.get(roomId);
if (room != null) {
return room;
}
var data = GameData.getDormRoomDataTable().get(roomId);
if (data == null) {
return null;
}
room = new DormRoomState(roomId);
room.setFurnitureList(createDefaultFurniture(data));
this.rooms.put(roomId, room);
this.queueSave = true;
return room;
}
public synchronized boolean unlockRoom(int roomId, PlayerChangeInfo change) {
if (!GameData.getDormRoomDataTable().containsKey(roomId)) {
return false;
}
if (this.rooms.containsKey(roomId)) {
return false;
}
this.getOrCreateRoom(roomId);
if (change != null) {
this.getPlayer().trigger(QuestCondition.DormUnlockRoom, 1);
if (this.getPlayer().hasSession()) {
var proto = DormRoomUnlock.newInstance()
.setDormRoomId(roomId)
.setChangeInfo(change.toProto());
this.getPlayer().addNextPackage(NetMsgId.dorm_room_unlock_notify, proto);
}
}
return true;
}
// Furniture
/**
* Adds furniture to the collection
*/
public synchronized void addFurniture(int id, int amount) {
this.furnitureCollection.put(id, this.furnitureCollection.get(id) + amount);
this.queueSave = true;
}
/**
* Returns how many of the given furniture the player owns
*/
public int getFurnitureCount(int id) {
if (this.furnitureCollection == null) {
return 0;
}
return this.furnitureCollection.get(id);
}
/**
* Returns true if the player owns at least one of the given furniture
*/
public boolean hasFurniture(int id) {
return this.getFurnitureCount(id) > 0;
}
public static List<DormFurniture> createDefaultFurniture(DormRoomDef room) {
var result = new ArrayList<DormFurniture>();
if (room == null || room.getDefaultLayoutId() <= 0) {
return result;
}
var layoutReward = GameData.getDormLayoutRewardDataTable().get(room.getDefaultLayoutId());
if (layoutReward == null) {
return result;
}
int baseId = room.getDefaultLayoutId() * 100;
for (int i = 1; i <= layoutReward.getFurnitureCount(); i++) {
var layout = GameData.getDormRoomLayoutDataTable().get(baseId + i);
if (layout == null || layout.getPos() == null || layout.getPos().length < 3) {
continue;
}
int[] pos = layout.getPos();
result.add(new DormFurniture(layout.getFurnitureId(), pos[0], pos[1], pos[2], layout.getRot()));
}
return result;
}
public static boolean isValidFurnitureList(List<DormFurniture> furnitureList) {
if (furnitureList == null || furnitureList.size() > 1000) {
return false;
}
for (var furniture : furnitureList) {
if (furniture == null || furniture.getFurnitureId() <= 0) {
return false;
}
if (!GameData.getDormFurnitureDataTable().containsKey(furniture.getFurnitureId())) {
return false;
}
}
return true;
}
public synchronized Long saveFurnitureLayout(int roomId, List<DormFurniture> furnitureList, int buildId) {
if (!this.isRoomUnlocked(roomId)) {
return null;
}
var roomData = GameData.getDormRoomDataTable().get(roomId);
if (roomData == null || !isValidFurnitureList(furnitureList)) {
return null;
}
long now = Nebula.getCurrentServerTime();
if (buildId == 0) {
var room = this.getOrCreateRoom(roomId);
if (room == null) {
return null;
}
room.setFurnitureList(new ArrayList<>(furnitureList));
this.queueSave = true;
this.getPlayer().trigger(QuestCondition.DormPlaceFurniture, furnitureList.size());
this.getPlayer().trigger(QuestCondition.DormSaveRoom, 1);
return 0L;
}
if (buildId < 1 || buildId > GameConstants.DORM_DEFAULT_PRESET_NUM) {
return null;
}
var preset = this.presets.get(buildId);
if (preset == null) {
if (this.presets.size() >= GameConstants.DORM_DEFAULT_PRESET_NUM) {
return null;
}
preset = new DormPreset(buildId, roomData.getRoomType(), now);
this.presets.put(buildId, preset);
} else {
preset.setCreateTime(now);
preset.setDormType(roomData.getRoomType());
}
preset.setFurnitureList(new ArrayList<>(furnitureList));
this.queueSave = true;
return now;
}
public synchronized boolean deletePreset(int buildId) {
if (this.presets.remove(buildId) == null) {
return false;
}
this.queueSave = true;
return true;
}
public synchronized boolean renamePreset(int buildId, String name) {
// Sanity check name
if (name == null || name.length() > GameConstants.DORM_MAX_NAME_LENGTH) {
return false;
}
var preset = this.presets.get(buildId);
if (preset == null) {
return false;
}
preset.setBuildName(name);
this.queueSave = true;
return true;
}
public synchronized boolean renameRoom(int roomId, String name) {
// Sanity check name
if (name == null || name.length() > GameConstants.DORM_MAX_NAME_LENGTH) {
return false;
}
// Check rename cooldown
long now = Nebula.getCurrentServerTime();
if (roomId == 0) {
if (now < this.dormNameRestTime + GameConstants.DORM_NAME_RESET_TIME_LIMIT) {
return false;
}
this.dormName = name;
this.dormNameRestTime = now;
this.queueSave = true;
return true;
}
if (!this.isRoomUnlocked(roomId)) {
return false;
}
var room = this.getOrCreateRoom(roomId);
if (room == null || now < room.getRestNameTime() + GameConstants.DORM_NAME_RESET_TIME_LIMIT) {
return false;
}
room.setRoomName(name);
room.setRestNameTime(now);
this.queueSave = true;
return true;
}
public synchronized boolean setRoomBgm(int roomId, int discId) {
if (discId < 0 || !this.isRoomUnlocked(roomId)) {
return false;
}
var room = this.getOrCreateRoom(roomId);
if (room == null) {
return false;
}
room.setDiscId(discId);
this.queueSave = true;
return true;
}
public synchronized boolean setRoomScene(int roomId, int sceneId) {
if (sceneId < 0 || !this.isRoomUnlocked(roomId)) {
return false;
}
var roomData = GameData.getDormRoomDataTable().get(roomId);
if (sceneId != 0) {
if (!GameData.getDormRoomSceneDataTable().containsKey(sceneId)) {
return false;
}
// The default scene of the room is always available
if (roomData == null || roomData.getSceneRes() != sceneId) {
if (this.getPlayer().getInventory().getItemCount(sceneId) <= 0) {
return false;
}
}
}
var room = this.getOrCreateRoom(roomId);
if (room == null) {
return false;
}
room.setRoomScene(sceneId);
this.queueSave = true;
return true;
}
public synchronized boolean updateInvitationRoles(int roomId, List<Integer> roleIds) {
if (!this.isRoomUnlocked(roomId) || roleIds == null) {
return false;
}
var roomData = GameData.getDormRoomDataTable().get(roomId);
var room = this.getOrCreateRoom(roomId);
if (roomData == null || room == null) {
return false;
}
// Validate role ids
var requested = new IntOpenHashSet();
for (int roleId : roleIds) {
var roleData = GameData.getDormRoleDataTable().get(roleId);
if (roleData == null) {
return false;
}
// Playable characters need to be owned by the player.
if (roleData.getRoleType() == 1 && this.getPlayer().getCharacters().getCharacterById(roleId) == null) {
return false;
}
requested.add(roleId);
}
// Keep existing invitation NPCs, mirroring the client
var newRoles = new ArrayList<DormRole>();
for (var role : room.getRoleList()) {
var roleData = GameData.getDormRoleDataTable().get(role.getRoleId());
if (role.isInvitationRole() && roleData != null && roleData.getRoleType() == 2) {
newRoles.add(role);
}
}
// Add requested roles
var existing = new IntOpenHashSet();
for (var role : newRoles) {
existing.add(role.getRoleId());
}
for (int roleId : requested) {
if (existing.contains(roleId)) {
continue;
}
var role = new DormRole(roleId);
role.setVisit(true);
role.setInvitationRole(false);
newRoles.add(role);
}
if (newRoles.size() > roomData.getRoleNum()) {
return false;
}
room.setRoleList(newRoles);
// Remove the requested roles from all other rooms
for (var other : this.rooms.values()) {
if (other.getDormRoomId() == roomId) {
continue;
}
other.getRoleList().removeIf(role -> requested.contains(role.getRoleId()));
}
this.queueSave = true;
return true;
}
// Interactions
public boolean hasInteracted(int roleId) {
this.resetDailies();
return this.dailyInteractionRoleIds.contains(roleId);
}
public int getDailyInteractionCount() {
this.resetDailies();
return this.dailyInteractionRoleIds.size();
}
public synchronized void addInteractionRole(int roleId) {
if (this.dailyInteractionRoleIds.add(roleId)) {
this.queueSave = true;
}
}
public boolean isParticipatingInRoom(int roomId, int roleId) {
var room = this.rooms.get(roomId);
if (room == null) {
return false;
}
for (var role : room.getRoleList()) {
if (role.getRoleId() == roleId) {
return true;
}
}
return false;
}
public synchronized int interactRole(GameCharacter character, int roleId, PlayerChangeInfo change) {
if (character == null) {
return -1;
}
// Affinity
character.addAffinityExp(GameConstants.DORM_ROLE_FAVORABILITY, change);
// Random interaction reward
int dropItemId = emu.nebula.data.resources.DropPkgDef.getRandomDrop(GameConstants.DORM_ROLE_INTERACTION_DROP_ID);
if (dropItemId > 0) {
this.getPlayer().getInventory().addItem(dropItemId, 1, change);
}
this.addInteractionRole(roleId);
this.getPlayer().trigger(QuestCondition.DormRoleInteractionReward, 1);
return dropItemId;
}
/**
* Interacts with the roles of the given rooms.
*
* @param interactions the requested role interactions
* @return the result of the interactions, or null if no interaction was possible
*/
public synchronized DormRoleInteractionResult interactRoles(Iterable<RoleInteraction> interactions) {
var change = new PlayerChangeInfo();
var results = new ArrayList<DormRoleInteractionResult.Interaction>();
var processed = new IntOpenHashSet();
for (var interaction : interactions) {
int roomId = interaction.getDormRoomId();
int roleId = interaction.getRoleId();
// Sanity checks
if (roleId <= 0 || !processed.add(roleId)) {
continue;
}
if (!this.isRoomUnlocked(roomId) || !this.isParticipatingInRoom(roomId, roleId)) {
continue;
}
if (this.getDailyInteractionCount() >= GameConstants.DORM_ROLE_INTERACTION_MAX_NUM) {
break;
}
if (this.hasInteracted(roleId)) {
continue;
}
var character = this.getPlayer().getCharacters().getCharacterById(roleId);
if (character == null) {
continue;
}
int dropItemId = this.interactRole(character, roleId, change);
if (dropItemId < 0) {
continue;
}
results.add(new DormRoleInteractionResult.Interaction(character, dropItemId));
}
if (results.isEmpty()) {
return null;
}
return new DormRoleInteractionResult(change, results);
}
// Affinity gifts
public int getDailyGiftTotal() {
return this.dailyAffinityGiftTotal;
}
public int getDailyGiftCount(int roleId) {
return this.dailyAffinityGiftCounts.get(roleId);
}
public synchronized void addGift(int roleId, int count) {
this.dailyAffinityGiftCounts.put(roleId, this.dailyAffinityGiftCounts.get(roleId) + count);
this.dailyAffinityGiftTotal += count;
this.queueSave = true;
}
/**
* Sends affinity gifts to a character. One gift action per character per day,
* with a shared daily item limit.
*
* @return the changes of the gift, or null if the gifts could not be sent
*/
public synchronized PlayerChangeInfo sendAffinityGift(GameCharacter character, int roomId, ItemParamMap items) {
// Validate room and gifts
if (character == null || !this.isRoomUnlocked(roomId) || items == null || items.isEmpty()) {
return null;
}
int charId = character.getCharId();
// Validate gift items
int giftCount = 0;
for (var entry : items.int2IntEntrySet()) {
if (entry.getIntValue() <= 0) {
return null;
}
giftCount += entry.getIntValue();
if (giftCount > GameConstants.DORM_ROLE_AFFINITY_GIFT_MAX_NUM) {
return null;
}
}
// One gift action per character per day, with a shared daily item limit
if (this.getDailyGiftCount(charId) > 0) {
return null;
}
if (this.getDailyGiftTotal() + giftCount > GameConstants.DORM_ROLE_AFFINITY_GIFT_MAX_NUM) {
return null;
}
// Remove items and add affinity
var change = character.sendGift(items);
if (change == null) {
return null;
}
this.addGift(charId, giftCount);
this.getPlayer().trigger(QuestCondition.DormSendGift, giftCount);
return change;
}
// Blocks
/**
* Converts dorm blocks into furniture coins.
*
* @return the changes of the conversion, or null if the conversion failed
*/
public synchronized PlayerChangeInfo convertBlocks(int count) {
if (count <= 0 || count > Integer.MAX_VALUE / GameConstants.DORM_BLOCKS_CONVERT_PROP) {
return null;
}
var inventory = this.getPlayer().getInventory();
if (inventory.getItemCount(GameConstants.DORM_JADE_ITEM_ID) < count) {
return null;
}
var change = new PlayerChangeInfo();
// Remove jade and add furniture coins
inventory.removeItem(GameConstants.DORM_JADE_ITEM_ID, count, change);
inventory.addItem(
GameConstants.DORM_FURNITURE_COIN_ITEM_ID,
count * GameConstants.DORM_BLOCKS_CONVERT_PROP,
change
);
return change;
}
// Rewards
public IntSet getClaimableRewardIds() {
var result = new IntOpenHashSet();
var unlocked = this.getUnlockedRoomIds();
for (var reward : GameData.getDormRewardDataTable()) {
if (this.receivedRewardIds.contains(reward.getId())) {
continue;
}
if (reward.isUnlocked(unlocked)) {
result.add(reward.getId());
}
}
return result;
}
public boolean isLayoutRewardClaimable(int layoutId) {
var data = GameData.getDormLayoutRewardDataTable().get(layoutId);
if (data == null || data.getRewardId1() <= 0 || data.getQty1() <= 0) {
return false;
}
for (int i = 1; i <= data.getFurnitureCount(); i++) {
var layout = GameData.getDormRoomLayoutDataTable().get(layoutId * 100 + i);
if (layout == null) {
continue;
}
int required = 0;
for (int j = 1; j <= data.getFurnitureCount(); j++) {
var other = GameData.getDormRoomLayoutDataTable().get(layoutId * 100 + j);
if (other != null && other.getFurnitureId() == layout.getFurnitureId()) {
required++;
}
}
if (this.getFurnitureCount(layout.getFurnitureId()) < required) {
return false;
}
}
return true;
}
public synchronized PlayerChangeInfo receiveLayoutRewards(int[] layoutIds) {
var change = new PlayerChangeInfo();
var rewards = new ItemParamMap();
boolean received = false;
for (int layoutId : layoutIds) {
if (this.receivedLayoutRewardIds.contains(layoutId) || !this.isLayoutRewardClaimable(layoutId)) {
continue;
}
var data = GameData.getDormLayoutRewardDataTable().get(layoutId);
rewards.add(data.getRewardId1(), data.getQty1());
this.receivedLayoutRewardIds.add(layoutId);
received = true;
}
if (!received) {
return null;
}
this.getPlayer().getInventory().addItems(rewards, change);
this.getPlayer().trigger(QuestCondition.DormRewardReceive, 1);
this.queueSave = true;
return change;
}
public synchronized PlayerChangeInfo receiveRewards(int[] rewardIds) {
var change = new PlayerChangeInfo();
var unlocked = this.getUnlockedRoomIds();
boolean received = false;
for (int rewardId : rewardIds) {
if (this.receivedRewardIds.contains(rewardId)) {
continue;
}
var data = GameData.getDormRewardDataTable().get(rewardId);
if (data == null || !data.isUnlocked(unlocked)) {
continue;
}
int itemId = DropPkgDef.getRandomDrop(data.getDropId());
if (itemId <= 0) {
continue;
}
this.getPlayer().getInventory().addItem(itemId, 1, change);
this.receivedRewardIds.add(rewardId);
received = true;
}
if (!received) {
return null;
}
this.getPlayer().trigger(QuestCondition.DormRewardReceive, 1);
this.queueSave = true;
return change;
}
public int getCollectionValue() {
if (this.furnitureCollection == null) {
return 0;
}
int total = 0;
for (var entry : this.furnitureCollection.int2IntEntrySet()) {
if (entry.getIntValue() <= 0) {
continue;
}
var furniture = GameData.getDormFurnitureDataTable().get(entry.getIntKey());
if (furniture != null) {
total += furniture.getCollectionValue();
}
}
return total;
}
public synchronized AchievementComplete receiveAchievements(int[] achievementIds) {
int collectionValue = this.getCollectionValue();
var change = new PlayerChangeInfo();
var proto = AchievementComplete.newInstance();
var rewards = new ItemParamMap();
boolean completed = false;
for (int achievementId : achievementIds) {
if (this.completedAchievements.containsKey(achievementId)) {
continue;
}
var data = GameData.getDormAchievementDataTable().get(achievementId);
if (data == null || data.getCompleteCond() != 1) {
continue;
}
int required = data.getCompleteParams() != null && data.getCompleteParams().length > 0
? data.getCompleteParams()[0]
: 0;
if (collectionValue < required) {
continue;
}
long completeTime = Nebula.getCurrentServerTime();
this.completedAchievements.put(achievementId, completeTime);
if (data.getTid1() > 0 && data.getQty1() > 0) {
rewards.add(data.getTid1(), data.getQty1());
}
if (data.getTid2() > 0 && data.getQty2() > 0) {
rewards.add(data.getTid2(), data.getQty2());
}
if (data.getTid3() > 0 && data.getQty3() > 0) {
rewards.add(data.getTid3(), data.getQty3());
}
var info = AchievementInfo.newInstance()
.setAchievementId(achievementId)
.setCompleteTime(completeTime);
proto.addAchievementList(info);
completed = true;
}
if (!completed) {
return null;
}
this.getPlayer().getInventory().addItems(rewards, change);
proto.setChangeInfo(change.toProto());
this.queueSave = true;
return proto;
}
// Social
public boolean hasLikedFriend(int uid) {
return this.likedFriendUids.contains(uid);
}
public synchronized void addLikedFriend(int uid) {
this.likedFriendUids.add(uid);
}
public synchronized void addLike() {
this.likeNum++;
}
public synchronized void recordVisit(int visitorUid) {
if (this.dailyVisitorUids.add(visitorUid)) {
this.visitTotalNum++;
this.dayVisitNum = this.dailyVisitorUids.size();
}
}
// Helpers for friend dorm data
public DormManager getFriendDorm(int uid) {
var cached = Nebula.getGameContext().getPlayerModule().getPlayer(uid);
if (cached == null) {
return null;
}
if (cached.getDormManager() == null) {
cached.loadManagerFromDatabase(DormManager.class);
}
return cached.getDormManager();
}
// Proto
public synchronized DormInfoListResp toProto() {
var proto = DormInfoListResp.newInstance();
var unlocked = this.getUnlockedRoomIds();
var sortedRooms = new ArrayList<>(unlocked);
sortedRooms.sort(Integer::compareTo);
for (int roomId : sortedRooms) {
var room = this.getOrCreateRoom(roomId);
if (room != null) {
proto.addDormList(room.toProto());
}
}
var info = DormInfoData.newInstance()
.setDormName(this.dormName == null ? "" : this.dormName)
.setLikeNum(this.likeNum)
.setDayVisitNum(this.dayVisitNum)
.setVisitTotalNum(this.visitTotalNum)
.setRestNameTime(this.dormNameRestTime)
.setTaskGroup(Public.UI32V.newInstance().setValue(0).setVersion(0));
var rewardIds = new ArrayList<>(this.getClaimableRewardIds());
rewardIds.sort(Integer::compareTo);
for (int rewardId : rewardIds) {
info.addRewardIds(rewardId);
}
for (var entry : this.completedAchievements.entrySet()) {
info.addAchievementList(
AchievementInfo.newInstance()
.setAchievementId(entry.getKey())
.setCompleteTime(entry.getValue())
);
}
proto.setDormInfo(info);
for (int layoutId : this.receivedLayoutRewardIds) {
proto.addReceivedRewardIds(layoutId);
}
for (int roleId : this.dailyInteractionRoleIds) {
proto.addInteractionRoleIds(roleId);
}
for (var entry : this.dailyAffinityGiftCounts.int2IntEntrySet()) {
proto.addAffinityGiftRoleInfos(
DormAffinityGiftInfo.newInstance()
.setRoleId(entry.getIntKey())
.setSendGiftCnt(entry.getIntValue())
);
}
return proto;
}
public DormHot getDormHotProto() {
return DormHot.newInstance()
.setCollectionValue(this.getCollectionValue());
}
// Database
@Override
public void save() {
Nebula.getGameDatabase().save(this);
this.queueSave = false;
}
}
@@ -0,0 +1,49 @@
package emu.nebula.game.dorm;
import java.util.ArrayList;
import java.util.List;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.PublicDorm.FurniturePresetInfo;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity(useDiscriminator = false)
public class DormPreset {
private int buildId;
private int dormType;
private String buildName;
private long createTime;
private List<DormFurniture> furnitureList;
@Deprecated // Morphia only
public DormPreset() {
}
public DormPreset(int buildId, int dormType, long createTime) {
this.buildId = buildId;
this.dormType = dormType;
this.createTime = createTime;
this.buildName = "";
this.furnitureList = new ArrayList<>();
}
public FurniturePresetInfo toProto() {
var proto = FurniturePresetInfo.newInstance()
.setBuildId(this.buildId)
.setDormType(this.dormType)
.setBuildName(this.buildName == null ? "" : this.buildName)
.setCreateTime(this.createTime);
if (this.furnitureList != null) {
for (var furniture : this.furnitureList) {
proto.addFurnitureList(furniture.toProto());
}
}
return proto;
}
}
@@ -0,0 +1,31 @@
package emu.nebula.game.dorm;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.PublicDorm.DormRoleInfo;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity(useDiscriminator = false)
public class DormRole {
private int roleId;
private boolean isVisit;
private boolean isInvitationRole;
@Deprecated // Morphia only
public DormRole() {
}
public DormRole(int roleId) {
this.roleId = roleId;
}
public DormRoleInfo toProto() {
return DormRoleInfo.newInstance()
.setRoleId(this.roleId)
.setIsVisit(this.isVisit)
.setIsInvitationRole(this.isInvitationRole);
}
}
@@ -0,0 +1,32 @@
package emu.nebula.game.dorm;
import java.util.List;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.player.PlayerChangeInfo;
import lombok.Getter;
/**
* The result of interacting with roles in a dorm room.
*/
@Getter
public class DormRoleInteractionResult {
// The changes of the interactions
private final PlayerChangeInfo change;
// The interactions that were performed
private final List<Interaction> interactions;
public DormRoleInteractionResult(PlayerChangeInfo change, List<Interaction> interactions) {
this.change = change;
this.interactions = interactions;
}
/**
* A single role interaction.
*/
public record Interaction(GameCharacter character, int itemId) {
}
}
@@ -0,0 +1,59 @@
package emu.nebula.game.dorm;
import java.util.ArrayList;
import java.util.List;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.PublicDorm.DormRoomInfo;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Entity(useDiscriminator = false)
public class DormRoomState {
private int dormRoomId;
private List<DormFurniture> furnitureList;
private List<DormRole> roleList;
private int discId;
private String roomName;
private int roomScene;
private long restNameTime;
@Deprecated // Morphia only
public DormRoomState() {
}
public DormRoomState(int dormRoomId) {
this.dormRoomId = dormRoomId;
this.furnitureList = new ArrayList<>();
this.roleList = new ArrayList<>();
this.roomName = "";
}
// Proto
public DormRoomInfo toProto() {
var proto = DormRoomInfo.newInstance()
.setDormRoomId(this.getDormRoomId())
.setDiscId(this.getDiscId())
.setRoomName(this.getRoomName() == null ? "" : this.getRoomName())
.setRoomScene(this.getRoomScene())
.setRestNameTime(this.getRestNameTime());
if (this.getFurnitureList() != null) {
for (var furniture : this.getFurnitureList()) {
proto.addFurnitureList(furniture.toProto());
}
}
if (this.getRoleList() != null) {
for (var role : this.getRoleList()) {
proto.addRoleList(role.toProto());
}
}
return proto;
}
}
@@ -459,6 +459,43 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
change.add(proto);
}
}
case DormFurniture -> {
// Do not allow removing dorm furniture
if (amount <= 0) {
break;
}
// Dorm furniture sets/kits contain their furniture in UseArgs.
// Unpack them instead of adding the set itself to the bag.
if (data.getUseParams() != null && !data.getUseParams().isEmpty()) {
// Add items from the set
for (var entry : data.getUseParams()) {
this.addItem(entry.getIntKey(), entry.getIntValue() * amount, change);
}
// End early
break;
}
if (data.getItemSubType() == ItemSubType.DormRoom) {
// Unlock dorm rooms when the room item is acquired.
this.getPlayer().getDormManager().unlockRoom(id, change);
} else if (data.getItemSubType() == ItemSubType.DormFurniture) {
// Add furniture to the dorm furniture collection
var dormManager = this.getPlayer().getDormManager();
boolean isFirst = dormManager.getFurnitureCount(id) <= 0;
dormManager.addFurniture(id, amount);
// Add to change info (to notify the game client)
var proto = DormItem.newInstance()
.setTid(id)
.setQty(amount)
.setIsFirst(isFirst);
change.add(proto);
}
}
case Disc -> {
// Cannot remove discs
if (amount <= 0) break;
@@ -17,6 +17,7 @@ import emu.nebula.game.agent.AgentManager;
import emu.nebula.game.battlepass.BattlePassManager;
import emu.nebula.game.character.CharacterStorage;
import emu.nebula.game.dating.DatingManager;
import emu.nebula.game.dorm.DormManager;
import emu.nebula.game.formation.FormationManager;
import emu.nebula.game.friends.FriendList;
import emu.nebula.game.gacha.GachaManager;
@@ -113,6 +114,7 @@ public class Player implements GameDatabaseObject {
private transient AgentManager agentManager;
private transient TraceHuntManager traceHuntManager;
private transient ActivityManager activityManager;
private transient DormManager dormManager;
// Extra
private transient Stack<NetMsgPacket> nextPackages;
@@ -733,6 +735,9 @@ public class Player implements GameDatabaseObject {
// Reset daily trace hunt items
this.getTraceHuntManager().resetDailyQuota();
// Reset dorm dailies
this.getDormManager().resetDailies();
// Check to reset weeklies
if (resetWeekly) {
// Add weekly boss entry item
@@ -780,7 +785,7 @@ public class Player implements GameDatabaseObject {
// Login
private <T extends PlayerManager> T loadManagerFromDatabase(Class<T> cls) {
public <T extends PlayerManager> T loadManagerFromDatabase(Class<T> cls) {
var manager = Nebula.getGameDatabase().getObjectByField(cls, "_id", this.getUid());
if (manager != null) {
@@ -824,6 +829,7 @@ public class Player implements GameDatabaseObject {
this.agentManager = this.loadManagerFromDatabase(AgentManager.class);
this.traceHuntManager = this.loadManagerFromDatabase(TraceHuntManager.class);
this.activityManager = this.loadManagerFromDatabase(ActivityManager.class);
this.dormManager = this.loadManagerFromDatabase(DormManager.class);
// Database fixes
if (this.showChars == null) {
@@ -1105,6 +1111,11 @@ public class Player implements GameDatabaseObject {
if (this.getAchievementManager().isQueueSave()) {
this.getAchievementManager().save();
}
// Check if we need save dorm data
if (this.getDormManager().isQueueSave()) {
this.getDormManager().save();
}
}
/**
@@ -1146,7 +1157,8 @@ public class Player implements GameDatabaseObject {
.setDailyShopRewardStatus(this.getQuestManager().hasDailyShopReward())
.setDailyMallRewardStatus(this.getQuestManager().hasDailyMallReward())
.setMusicInfo(this.getMusic())
.setAchievements(new byte[64]);
.setAchievements(new byte[64])
.setDorm(this.getDormManager().getDormHotProto());
var acc = proto.getMutableAcc()
.setNickName(this.getName())
@@ -1262,6 +1274,7 @@ public class Player implements GameDatabaseObject {
state.getMutableScoreBoss();
state.getMutableCharAffinityRewards();
state.getMutableTraceHunt().setBossRewardCanReceive(this.getTraceHuntManager().isHuntComplete());
state.setDormStateFlag(this.getDormManager().getState());
// Star tower potential book state
var book = state.getMutableStarTowerBook();
@@ -16,6 +16,7 @@ import emu.nebula.game.agent.AgentManager;
import emu.nebula.game.battlepass.BattlePass;
import emu.nebula.game.character.GameCharacter;
import emu.nebula.game.character.GameDisc;
import emu.nebula.game.dorm.DormManager;
import emu.nebula.game.formation.FormationManager;
import emu.nebula.game.friends.Friendship;
import emu.nebula.game.gacha.GachaManager;
@@ -187,6 +188,7 @@ public class PlayerModule extends GameContextModule {
datastore.getCollection(TraceHuntManager.class).deleteOne(idFilter);
datastore.getCollection(AchievementManager.class).deleteOne(idFilter);
datastore.getCollection(ActivityManager.class).deleteOne(idFilter);
datastore.getCollection(DormManager.class).deleteOne(idFilter);
datastore.getCollection(BattlePass.class).deleteOne(idFilter);
datastore.getCollection(ScoreBossRankEntry.class).deleteOne(idFilter);
@@ -0,0 +1,28 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormAchievementRewardReceive.DormAchievementRewardReceiveReq;
@HandlerId(NetMsgId.dorm_achievement_reward_receive_req)
public class HandlerDormAchievementRewardReceiveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormAchievementRewardReceiveReq.parseFrom(message);
// Receive achievements
var rsp = session.getPlayer().getDormManager().receiveAchievements(req.getAchievementIds().toArray());
if (rsp == null) {
return session.encodeMsg(NetMsgId.dorm_achievement_reward_receive_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_achievement_reward_receive_succeed_ack, rsp);
}
}
@@ -0,0 +1,47 @@
package emu.nebula.server.handlers;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormAffinityGiftSend.DormAffinityGiftSendReq;
import emu.nebula.proto.DormAffinityGiftSend.DormAffinityGiftSendResp;
@HandlerId(NetMsgId.dorm_affinity_gift_send_req)
public class HandlerDormAffinityGiftSendReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormAffinityGiftSendReq.parseFrom(message);
var player = session.getPlayer();
var manager = player.getDormManager();
// Get character
var character = player.getCharacters().getCharacterById(req.getCharId());
if (character == null) {
return session.encodeMsg(NetMsgId.dorm_affinity_gift_send_failed_ack);
}
// Parse gift items
var items = ItemParamMap.fromTemplates(req.getItems());
// Send gifts
var change = manager.sendAffinityGift(character, req.getDormRoomId(), items);
if (change == null) {
return session.encodeMsg(NetMsgId.dorm_affinity_gift_send_failed_ack);
}
// Build response
var rsp = DormAffinityGiftSendResp.newInstance()
.setInfo(character.getAffinityProto())
.setChange(change.toProto())
.setSendGiftCnt(manager.getDailyGiftCount(character.getCharId()));
// Encode and send
return session.encodeMsg(NetMsgId.dorm_affinity_gift_send_succeed_ack, rsp);
}
}
@@ -0,0 +1,27 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.UI32;
@HandlerId(NetMsgId.dorm_blocks_convert_req)
public class HandlerDormBlocksConvertReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = UI32.parseFrom(message);
// Convert blocks
var change = session.getPlayer().getDormManager().convertBlocks(req.getValue());
if (change == null) {
return session.encodeMsg(NetMsgId.dorm_blocks_convert_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_blocks_convert_succeed_ack, change.toProto());
}
}
@@ -0,0 +1,52 @@
package emu.nebula.server.handlers;
import emu.nebula.GameConstants;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormFriendLike.DormFriendLikeReq;
@HandlerId(NetMsgId.dorm_friend_like_req)
public class HandlerDormFriendLikeReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormFriendLikeReq.parseFrom(message);
long friendId = req.getFriendId();
if (friendId <= 0 || friendId > Integer.MAX_VALUE) {
return session.encodeMsg(NetMsgId.dorm_friend_like_failed_ack);
}
int uid = (int) friendId;
var player = session.getPlayer();
var manager = player.getDormManager();
// Friends only
if (!player.getFriendList().getFriends().containsKey(uid)) {
return session.encodeMsg(NetMsgId.dorm_friend_like_failed_ack);
}
// Daily limit and duplicate check
if (manager.hasLikedFriend(uid) || manager.getLikedFriendUids().size() >= GameConstants.DORM_LIKE_LIMIT) {
return session.encodeMsg(NetMsgId.dorm_friend_like_failed_ack);
}
manager.addLikedFriend(uid);
// Increment the friend's like counter
var dorm = session.getPlayer().getDormManager().getFriendDorm(uid);
if (dorm != null) {
dorm.addLike();
dorm.save();
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_friend_like_succeed_ack);
}
}
@@ -0,0 +1,42 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormFriendList.DormFriendListResp;
import emu.nebula.proto.PublicDorm.DormFriendDetail;
@HandlerId(NetMsgId.dorm_friend_list_req)
public class HandlerDormFriendListReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
var player = session.getPlayer();
var manager = player.getDormManager();
var rsp = DormFriendListResp.newInstance();
for (var friendship : player.getFriendList().getFriends().values()) {
int uid = friendship.getFriendUid();
var dorm = session.getPlayer().getDormManager().getFriendDorm(uid);
var detail = DormFriendDetail.newInstance()
.setUid(uid)
.setLikeNum(dorm == null ? 0 : dorm.getLikeNum())
.setCollectionValue(dorm == null ? 0 : dorm.getCollectionValue())
.setIsLike(manager.hasLikedFriend(uid));
if (dorm != null && dorm.getDormName() != null) {
detail.setDormName(dorm.getDormName());
}
rsp.addFriends(detail);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_friend_list_succeed_ack, rsp);
}
}
@@ -0,0 +1,70 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormFriendVisit.DormFriendVisitReq;
import emu.nebula.proto.DormFriendVisit.DormFriendVisitResp;
import emu.nebula.proto.PublicDorm.DormFriendInfo;
@HandlerId(NetMsgId.dorm_friend_visit_req)
public class HandlerDormFriendVisitReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormFriendVisitReq.parseFrom(message);
// Validate friend
long friendId = req.getFriendId();
if (friendId <= 0 || friendId > Integer.MAX_VALUE) {
return session.encodeMsg(NetMsgId.dorm_friend_visit_failed_ack);
}
int uid = (int) friendId;
if (!session.getPlayer().getFriendList().getFriends().containsKey(uid)) {
return session.encodeMsg(NetMsgId.dorm_friend_visit_failed_ack);
}
// Load friend dorm data
var dorm = session.getPlayer().getDormManager().getFriendDorm(uid);
if (dorm == null) {
return session.encodeMsg(NetMsgId.dorm_friend_visit_failed_ack);
}
// Build response
var rsp = DormFriendVisitResp.newInstance();
var unlockedRoomIds = dorm.getUnlockedRoomIds();
var sortedRoomIds = new java.util.ArrayList<>(unlockedRoomIds);
sortedRoomIds.sort(Integer::compareTo);
for (int roomId : sortedRoomIds) {
var room = dorm.getOrCreateRoom(roomId);
if (room != null) {
rsp.addDormList(room.toProto());
}
}
var dormInfo = DormFriendInfo.newInstance();
if (dorm.getDormName() != null) {
dormInfo.setDormName(dorm.getDormName());
}
rsp.setDormInfo(dormInfo);
// Record the visit on the friend's dorm
dorm.recordVisit(session.getPlayer().getUid());
dorm.save();
// Encode and send
return session.encodeMsg(NetMsgId.dorm_friend_visit_succeed_ack, rsp);
}
}
@@ -0,0 +1,42 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormFurnitureList.DormBagListResp;
import emu.nebula.proto.Public.ItemTpl;
import emu.nebula.proto.PublicDorm.DormBagInfo;
@HandlerId(NetMsgId.dorm_bag_list_req)
public class HandlerDormFurnitureListReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Get dorm manager
var dorm = session.getPlayer().getDormManager();
// Build response
var rsp = DormBagListResp.newInstance();
// Encode dorm furniture inventory
for (var entry : dorm.getFurnitureCollection().int2IntEntrySet()) {
if (entry.getIntValue() <= 0) {
continue;
}
var item = ItemTpl.newInstance()
.setTid(entry.getIntKey())
.setQty(entry.getIntValue());
var info = DormBagInfo.newInstance()
.setItemTplInfo(item);
rsp.addDormBagList(info);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_bag_list_succeed_ack, rsp);
}
}
@@ -0,0 +1,34 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.UI32;
@HandlerId(NetMsgId.dorm_furniture_preset_delete_req)
public class HandlerDormFurniturePresetDeleteReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// The client does not send a request body for this packet in the current client version.
// If a body is present, treat field 1 as the preset/build id.
int buildId = 0;
if (message != null && message.length > 0) {
try {
buildId = UI32.parseFrom(message).getValue();
} catch (Exception ignored) {
buildId = 0;
}
}
if (buildId > 0) {
session.getPlayer().getDormManager().deletePreset(buildId);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_furniture_preset_delete_succeed_ack);
}
}
@@ -0,0 +1,41 @@
package emu.nebula.server.handlers;
import java.util.ArrayList;
import java.util.Comparator;
import emu.nebula.GameConstants;
import emu.nebula.game.dorm.DormPreset;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormFurniturePreset.DormFurniturePresetListResp;
@HandlerId(NetMsgId.dorm_furniture_preset_list_req)
public class HandlerDormFurniturePresetListReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Get player dorm
var dorm = session.getPlayer().getDormManager();
// Build response
var rsp = DormFurniturePresetListResp.newInstance()
.setNum(GameConstants.DORM_DEFAULT_PRESET_NUM);
// Get preset list
var list = new ArrayList<>(dorm.getPresets().values());
// Sort preset list
list.sort(Comparator.comparingInt(DormPreset::getBuildId));
// Encode presets to proto
for (var preset : list) {
rsp.addPresetList(preset.toProto());
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_furniture_preset_list_succeed_ack, rsp);
}
}
@@ -0,0 +1,47 @@
package emu.nebula.server.handlers;
import java.util.ArrayList;
import emu.nebula.game.dorm.DormFurniture;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormFurnitureSave.DormFurnitureSaveReq;
import emu.nebula.proto.Public.I64;
@HandlerId(NetMsgId.dorm_furniture_save_req)
public class HandlerDormFurnitureSaveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormFurnitureSaveReq.parseFrom(message);
// Build furniture list
var furnitureList = new ArrayList<DormFurniture>();
for (var info : req.getFurniturePosInfoList()) {
furnitureList.add(new DormFurniture(info));
}
// Save room or preset
var createTime = session.getPlayer().getDormManager().saveFurnitureLayout(
req.getDormRoomId(),
furnitureList,
req.getBuildId()
);
if (createTime == null) {
return session.encodeMsg(NetMsgId.dorm_furniture_save_failed_ack);
}
// The client reads the preset creation time from Value
var rsp = I64.newInstance()
.setValue(createTime);
// Encode and send
return session.encodeMsg(NetMsgId.dorm_furniture_save_succeed_ack, rsp);
}
}
@@ -0,0 +1,32 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormGetChangeData.DormChangeDataResp;
import emu.nebula.proto.PublicDorm.DormChangeDataInfo;
@HandlerId(NetMsgId.dorm_get_change_data_req)
public class HandlerDormGetChangeDataReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Get player dorm
var dorm = session.getPlayer().getDormManager();
// Build response
var info = DormChangeDataInfo.newInstance()
.setDormName(dorm.getDormName())
.setLikeNum(dorm.getLikeNum())
.setDayVisitNum(dorm.getDayVisitNum())
.setVisitTotalNum(dorm.getVisitTotalNum());
var rsp = DormChangeDataResp.newInstance()
.setDormChangeData(info);
// Encode and send
return session.encodeMsg(NetMsgId.dorm_get_change_data_succeed_ack, rsp);
}
}
@@ -0,0 +1,20 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
@HandlerId(NetMsgId.dorm_info_list_req)
public class HandlerDormInfoListReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Build response
var rsp = session.getPlayer().getDormManager().toProto();
// Encode and send
return session.encodeMsg(NetMsgId.dorm_info_list_succeed_ack, rsp);
}
}
@@ -0,0 +1,26 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.StringV;
@HandlerId(NetMsgId.dorm_info_update_name_req)
public class HandlerDormInfoUpdateNameReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = StringV.parseFrom(message);
// Version = 0 means the dorm name, otherwise the room id
if (!session.getPlayer().getDormManager().renameRoom(req.getVersion(), req.getValue())) {
return session.encodeMsg(NetMsgId.dorm_info_update_name_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_info_update_name_succeed_ack);
}
}
@@ -0,0 +1,35 @@
package emu.nebula.server.handlers;
import java.util.ArrayList;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormInvitationRole.DormInvitationRoleReq;
import emu.nebula.proto.Public;
@HandlerId(NetMsgId.dorm_invitation_role_req)
public class HandlerDormInvitationRoleReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormInvitationRoleReq.parseFrom(message);
// Get role ids
var roleIds = new ArrayList<Integer>();
for (var info : req.getInvitationRoleList()) {
roleIds.add(info.getRoleId());
}
// Update invitation roles
if (!session.getPlayer().getDormManager().updateInvitationRoles(req.getDormRoomId(), roleIds)) {
return session.encodeMsg(NetMsgId.dorm_invitation_role_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_invitation_role_succeed_ack, Public.Nil.newInstance());
}
}
@@ -0,0 +1,28 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormLayoutRewardReceive.DormLayoutRewardReceiveReq;
@HandlerId(NetMsgId.dorm_layout_reward_receive_req)
public class HandlerDormLayoutRewardReceiveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormLayoutRewardReceiveReq.parseFrom(message);
// Receive layout rewards
var change = session.getPlayer().getDormManager().receiveLayoutRewards(req.getLayoutIds().toArray());
if (change == null) {
return session.encodeMsg(NetMsgId.dorm_layout_reward_receive_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_layout_reward_receive_succeed_ack, change.toProto());
}
}
@@ -0,0 +1,17 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
@HandlerId(NetMsgId.dorm_learn_req)
public class HandlerDormLearnReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Dorm quests are not implemented yet, so acknowledge the request without changing state.
return session.encodeMsg(NetMsgId.dorm_learn_succeed_ack);
}
}
@@ -0,0 +1,28 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormRewardReceive.DormRewardReceiveReq;
@HandlerId(NetMsgId.dorm_reward_receive_req)
public class HandlerDormRewardReceiveReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormRewardReceiveReq.parseFrom(message);
// Receive rewards
var change = session.getPlayer().getDormManager().receiveRewards(req.getRewardIds().toArray());
if (change == null) {
return session.encodeMsg(NetMsgId.dorm_reward_receive_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_reward_receive_succeed_ack, change.toProto());
}
}
@@ -0,0 +1,49 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormRoleInteraction.DormRoleInteractionReq;
import emu.nebula.proto.DormRoleInteraction.DormRoleInteractionResp;
import emu.nebula.proto.Public;
import emu.nebula.proto.PublicDorm.AffinityData;
@HandlerId(NetMsgId.dorm_role_interaction_req)
public class HandlerDormRoleInteractionReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormRoleInteractionReq.parseFrom(message);
// Interact with roles
var result = session.getPlayer().getDormManager().interactRoles(req.getRoleInteractions());
if (result == null) {
return session.encodeMsg(NetMsgId.dorm_role_interaction_failed_ack);
}
// Build response
var rsp = DormRoleInteractionResp.newInstance()
.setChange(result.getChange().toProto());
for (var interaction : result.getInteractions()) {
int itemId = interaction.itemId();
var itemInfo = Public.ItemTpl.newInstance()
.setTid(Math.max(itemId, 0))
.setQty(itemId > 0 ? 1 : 0);
rsp.addInfoList(
AffinityData.newInstance()
.setAffinityInfo(interaction.character().getAffinityProto())
.setItemInfo(itemInfo)
);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_role_interaction_succeed_ack, rsp);
}
}
@@ -0,0 +1,34 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.DormRoomBgm.DormRoomBGMReq;
@HandlerId(NetMsgId.dorm_room_bgm_req)
public class HandlerDormRoomBgmReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = DormRoomBGMReq.parseFrom(message);
var manager = session.getPlayer().getDormManager();
// Validate the whole request before applying it
for (var bgm : req.getBGMList()) {
if (!manager.isRoomUnlocked(bgm.getDormRoomId()) || bgm.getDiscId() < 0) {
return session.encodeMsg(NetMsgId.dorm_room_bgm_failed_ack);
}
}
for (var bgm : req.getBGMList()) {
manager.setRoomBgm(bgm.getDormRoomId(), bgm.getDiscId());
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_room_bgm_succeed_ack);
}
}
@@ -0,0 +1,26 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.UI32V;
@HandlerId(NetMsgId.dorm_set_room_scene_req)
public class HandlerDormSetRoomSceneReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = UI32V.parseFrom(message);
// Version = room id, Value = scene item id (0 clears the scene)
if (!session.getPlayer().getDormManager().setRoomScene(req.getVersion(), req.getValue())) {
return session.encodeMsg(NetMsgId.dorm_set_room_scene_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_set_room_scene_succeed_ack);
}
}
@@ -0,0 +1,26 @@
package emu.nebula.server.handlers;
import emu.nebula.net.GameSession;
import emu.nebula.net.HandlerId;
import emu.nebula.net.NetHandler;
import emu.nebula.net.NetMsgId;
import emu.nebula.proto.Public.StringV;
@HandlerId(NetMsgId.dorm_update_preset_name_req)
public class HandlerDormUpdatePresetNameReq extends NetHandler {
@Override
public byte[] handle(GameSession session, byte[] message) throws Exception {
// Parse request
var req = StringV.parseFrom(message);
// Version = preset/build id, Value = new name
if (!session.getPlayer().getDormManager().renamePreset(req.getVersion(), req.getValue())) {
return session.encodeMsg(NetMsgId.dorm_update_preset_name_failed_ack);
}
// Encode and send
return session.encodeMsg(NetMsgId.dorm_update_preset_name_succeed_ack);
}
}
+5 -5
View File
@@ -1,7 +1,7 @@
{
"global": 141,
"kr": 147,
"jp": 143,
"tw": 149,
"cn": 140
"global": 143,
"kr": 150,
"jp": 145,
"tw": 151,
"cn": 142
}