Initial Commit

This commit is contained in:
Melledy
2025-10-27 02:02:26 -07:00
commit f58951fe2a
378 changed files with 315914 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
package emu.nebula.game;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import emu.nebula.game.player.PlayerModule;
import emu.nebula.net.GameSession;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class GameContext {
private final Object2ObjectMap<String, GameSession> sessions;
// Modules
private final PlayerModule playerModule;
// Cleanup thread
private final Timer cleanupTimer;
public GameContext() {
this.sessions = new Object2ObjectOpenHashMap<>();
this.playerModule = new PlayerModule(this);
this.cleanupTimer = new Timer();
this.cleanupTimer.scheduleAtFixedRate(new CleanupTask(this), 0, TimeUnit.SECONDS.toMillis(60));
}
public synchronized GameSession getSessionByToken(String token) {
return sessions.get(token);
}
public synchronized void addSession(GameSession session) {
this.sessions.put(session.getToken(), session);
}
public synchronized void generateSessionToken(GameSession session) {
// Remove token
if (session.getToken() != null) {
this.sessions.remove(session.getToken());
}
// Generate token
String token = null;
do {
token = session.generateToken();
} while (this.getSessions().containsKey(token));
// Register session
this.sessions.put(session.getToken(), session);
}
// TODO add timeout to config
public synchronized void cleanupInactiveSessions() {
var it = this.getSessions().entrySet().iterator();
long timeout = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(600); // 10 minutes
while (it.hasNext()) {
var session = it.next().getValue();
if (timeout > session.getLastActiveTime()) {
// Remove from session map
it.remove();
// Clear player
session.clearPlayer(this);
}
}
}
@Getter
public static class CleanupTask extends TimerTask {
private GameContext gameContext;
public CleanupTask(GameContext gameContext) {
this.gameContext = gameContext;
}
@Override
public void run() {
this.getGameContext().cleanupInactiveSessions();
}
}
}

View File

@@ -0,0 +1,13 @@
package emu.nebula.game;
public abstract class GameContextModule {
private transient GameContext context;
public GameContextModule(GameContext player) {
this.context = player;
}
public GameContext getGameContext() {
return context;
}
}

View File

@@ -0,0 +1,167 @@
package emu.nebula.game.account;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.database.AccountDatabaseOnly;
import emu.nebula.util.Snowflake;
import lombok.Getter;
@Getter
@AccountDatabaseOnly
@Entity(value = "accounts", useDiscriminator = false)
public class Account {
@Id private String uid;
@Indexed
private String email;
private String code;
private String nickname;
private String picture;
@Indexed private String loginToken;
@Indexed private String gameToken;
private Set<String> permissions;
private int reservedPlayerUid;
private long createdAt;
@Deprecated
public Account() {
// Morphia only
}
public Account(String email, String password, int reservedUid) {
this.uid = Long.toString(Snowflake.newUid());
this.email = email;
this.nickname = "";
this.picture = "";
this.permissions = new HashSet<>();
this.reservedPlayerUid = reservedUid;
this.createdAt = System.currentTimeMillis() / 1000;
}
public boolean verifyCode(String code) {
// TODO
return true;
}
public void setNickname(String value) {
this.nickname = value;
}
// Tokens
public String generateLoginToken() {
this.loginToken = AccountHelper.createSessionKey(this.getUid());
this.save();
return this.loginToken;
}
public String generateGameToken() {
this.gameToken = AccountHelper.createSessionKey(this.getUid());
this.save();
return this.gameToken;
}
// Permissions
public Set<String> getPermissions() {
if (this.permissions == null) {
this.permissions = new HashSet<>();
this.save();
}
return this.permissions;
}
public boolean addPermission(String permission) {
if (this.getPermissions().contains(permission)) {
return false;
}
this.getPermissions().add(permission);
this.save();
return true;
}
public static boolean permissionMatchesWildcard(String wildcard, String[] permissionParts) {
String[] wildcardParts = wildcard.split("\\.");
if (permissionParts.length < wildcardParts.length) { // A longer wildcard can never match a shorter permission
return false;
}
for (int i = 0; i < wildcardParts.length; i++) {
switch (wildcardParts[i]) {
case "**": // Recursing match
return true;
case "*": // Match only one layer
if (i >= (permissionParts.length-1)) {
return true;
}
break;
default: // This layer isn't a wildcard, it needs to match exactly
if (!wildcardParts[i].equals(permissionParts[i])) {
return false;
}
}
}
// At this point the wildcard will have matched every layer, but if it is shorter then the permission then this is not a match at this point (no **).
return wildcardParts.length == permissionParts.length;
}
public boolean hasPermission(String permission) {
// Skip if permission isnt required
if (permission.isEmpty()) {
return true;
}
// Default permissions
var defaultPermissions = Nebula.getConfig().getServerOptions().getDefaultPermissions();
if (defaultPermissions.contains("*")) {
return true;
}
// Add default permissions if it doesn't exist
List<String> permissions = Stream.of(this.getPermissions(), defaultPermissions)
.flatMap(Collection::stream)
.distinct().toList();
if (permissions.contains(permission)) {
return true;
}
String[] permissionParts = permission.split("\\.");
for (String p : permissions) {
if (p.startsWith("-") && permissionMatchesWildcard(p.substring(1), permissionParts)) return false;
if (permissionMatchesWildcard(p, permissionParts)) return true;
}
return permissions.contains("*");
}
public boolean removePermission(String permission) {
boolean res = this.getPermissions().remove(permission);
if (res) this.save();
return res;
}
public void clearPermission() {
this.getPermissions().clear();
this.save();
}
// Database
public void save() {
Nebula.getAccountDatabase().save(this);
}
}

View File

@@ -0,0 +1,71 @@
package emu.nebula.game.account;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
import emu.nebula.Nebula;
/**
* Helper class for handling account related stuff
*/
public class AccountHelper {
public static Account createAccount(String email, String password, int reservedUid) {
Account account = Nebula.getAccountDatabase().getObjectByField(Account.class, "email", email);
if (account != null) {
return null;
}
account = new Account(email, password, reservedUid);
account.save();
return account;
}
public static Account getAccountByEmail(String email) {
if (email == null || email.isEmpty()) {
return null;
}
return Nebula.getAccountDatabase().getObjectByField(Account.class, "email", email);
}
public static Account getAccountByLoginToken(String token) {
if (token == null || token.isEmpty()) {
return null;
}
return Nebula.getAccountDatabase().getObjectByField(Account.class, "loginToken", token);
}
public static boolean deleteAccount(String username) {
Account account = Nebula.getAccountDatabase().getObjectByField(Account.class, "username", username);
if (account == null) {
return false;
}
// Delete the account first
return Nebula.getAccountDatabase().delete(account);
}
// Simple way to create a unique session key
public static String createSessionKey(String accountUid) {
byte[] random = new byte[64];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(random);
String temp = accountUid + "." + System.currentTimeMillis() + "." + secureRandom.toString();
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte[] bytes = md.digest(temp.getBytes());
return Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
return Base64.getEncoder().encodeToString(temp.getBytes());
}
}
}

View File

@@ -0,0 +1,283 @@
package emu.nebula.game.character;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.CharacterDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.Public.Char;
import emu.nebula.proto.Public.CharGemPreset;
import emu.nebula.proto.Public.CharGemSlot;
import emu.nebula.proto.PublicStarTower.StarTowerChar;
import emu.nebula.proto.PublicStarTower.StarTowerCharGem;
import lombok.Getter;
@Getter
@Entity(value = "characters", useDiscriminator = false)
public class Character implements GameDatabaseObject {
@Id
private ObjectId uid;
@Indexed
private int playerUid;
private transient CharacterDef data;
private transient Player player;
private int charId;
private int advance;
private int level;
private int exp;
private int skin;
private int[] skills;
private byte[] talents;
private long createTime;
@Deprecated // Morphia only!
public Character() {
}
public Character(Player player, int charId) {
this(player, GameData.getCharacterDataTable().get(charId));
}
public Character(Player player, CharacterDef data) {
this.player = player;
this.playerUid = player.getUid();
this.charId = data.getId();
this.data = data;
this.level = 1;
this.skin = data.getDefaultSkinId();
this.skills = new int[] {1, 1, 1, 1, 1};
this.talents = new byte[8];
this.createTime = Nebula.getCurrentTime();
}
public void setPlayer(Player player) {
this.player = player;
}
public void setData(CharacterDef data) {
if (this.data == null && data.getId() == this.getCharId()) {
this.data = data;
}
}
public int getMaxGainableExp() {
if (this.getLevel() >= this.getMaxLevel()) {
return 0;
}
int maxLevel = this.getMaxLevel();
int max = 0;
for (int i = this.getLevel() + 1; i <= maxLevel; i++) {
var data = GameData.getCharacterUpgradeDataTable().get(i);
if (data != null) {
max += data.getExp();
}
}
return Math.max(max - this.getExp(), 0);
}
public int getMaxExp() {
if (this.getLevel() >= this.getMaxLevel()) {
return 0;
}
var data = GameData.getCharacterUpgradeDataTable().get(this.level + 1);
return data != null ? data.getExp() : 0;
}
public int getMaxLevel() {
return 10 + (this.getAdvance() * 10);
}
public void addExp(int amount) {
// Setup
int expRequired = this.getMaxExp();
// Add exp
this.exp += amount;
// Check for level ups
while (this.exp >= expRequired && expRequired > 0) {
this.level += 1;
this.exp -= expRequired;
expRequired = this.getMaxExp();
}
// Clamp exp
if (this.getLevel() >= this.getMaxLevel()) {
this.exp = 0;
}
// Save to database
this.save();
}
// Handlers
public PlayerChangeInfo upgrade(ItemParamMap params) {
// Calculate exp gained
int exp = 0;
// Check if item is an exp item
for (var entry : params.getEntrySet()) {
var data = GameData.getCharItemExpDataTable().get(entry.getIntKey());
if (data == null) return null;
exp += data.getExpValue() * entry.getIntValue();
}
// Clamp exp gain
exp = Math.min(this.getMaxGainableExp(), exp);
// Calculate gold required
params.add(GameConstants.GOLD_ITEM_ID, (int) Math.ceil(exp * 0.15D));
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(params)) {
return null;
}
// Remove items
var changes = this.getPlayer().getInventory().removeItems(params, null);
// Add exp
this.addExp(exp);
// Success
return changes.setSuccess(true);
}
public PlayerChangeInfo advance() {
// TODO check player level to make sure they can advance this character
// Get advance data
int advanceId = (this.getData().getAdvanceGroup() * 100) + (this.advance + 1);
var data = GameData.getCharacterAdvanceDataTable().get(advanceId);
if (data == null) {
return null;
}
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(data.getMaterials())) {
return null;
}
// Remove items
var changes = this.getPlayer().getInventory().removeItems(data.getMaterials(), null);
// Add advance level
this.advance++;
// Save to database
this.save();
// Success
return changes.setSuccess(true);
}
public PlayerChangeInfo upgradeSkill(int index) {
// TODO check player level to make sure they can advance this character
// Sanity check
if (index < 0 || index >= this.skills.length) {
return null;
}
// Get advance data
int upgradeId = (this.getData().getSkillsUpgradeGroup(index) * 100) + (this.skills[index] + 1);
var data = GameData.getCharacterSkillUpgradeDataTable().get(upgradeId);
if (data == null) {
return null;
}
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(data.getMaterials())) {
return null;
}
// Remove items
var changes = this.getPlayer().getInventory().removeItems(data.getMaterials(), null);
// Add skill level
this.skills[index]++;
// Save to database
this.save();
// Success
return changes.setSuccess(true);
}
// Proto
public Char toProto() {
var proto = Char.newInstance()
.setTid(this.getCharId())
.setLevel(this.getLevel())
.setSkin(this.getSkin())
.setAdvance(this.getAdvance())
.setTalentNodes(this.getTalents())
.addAllSkillLvs(this.getSkills())
.setCreateTime(this.getCreateTime());
var gemPresets = proto.getMutableCharGemPresets()
.getMutableCharGemPresets();
for (int i = 0; i < 3; i++) {
var preset = CharGemPreset.newInstance()
.addAllSlotGem(-1, -1, -1);
gemPresets.add(preset);
}
for (int i = 1; i <= 3; i++) {
var slot = CharGemSlot.newInstance()
.setId(i);
proto.addCharGemSlots(slot);
}
proto.getMutableAffinityQuests();
return proto;
}
public StarTowerChar toStarTowerProto() {
var proto = StarTowerChar.newInstance()
.setId(this.getCharId())
.setAdvance(this.getAdvance())
.setLevel(this.getLevel())
.setTalentNodes(this.getTalents())
.addAllSkillLvs(this.getSkills());
for (int i = 1; i <= 3; i++) {
var slot = StarTowerCharGem.newInstance()
.setSlotId(i)
.addAllAttributes(new int[] {0, 0, 0, 0});
proto.addGems(slot);
}
return proto;
}
}

View File

@@ -0,0 +1,151 @@
package emu.nebula.game.character;
import java.util.Collection;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.CharacterDef;
import emu.nebula.data.resources.DiscDef;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.game.player.Player;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class CharacterStorage extends PlayerManager {
private final Int2ObjectMap<Character> characters;
private final Int2ObjectMap<GameDisc> discs;
public CharacterStorage(Player player) {
super(player);
this.characters = new Int2ObjectOpenHashMap<>();
this.discs = new Int2ObjectOpenHashMap<>();
}
// Characters
public Character getCharacterById(int id) {
if (id <= 0) {
return null;
}
return this.characters.get(id);
}
public boolean hasCharacter(int id) {
return this.characters.containsKey(id);
}
public Character addCharacter(int charId) {
// Sanity check to make sure we dont have this character already
if (this.hasCharacter(charId)) {
return null;
}
return this.addCharacter(GameData.getCharacterDataTable().get(charId));
}
private Character addCharacter(CharacterDef data) {
// Sanity check to make sure we dont have this character already
if (this.hasCharacter(data.getId())) {
return null;
}
// Create character
var character = new Character(this.getPlayer(), data);
// Save to database
character.save();
// Add to characters
this.characters.put(character.getCharId(), character);
return character;
}
public Collection<Character> getCharacterCollection() {
return this.getCharacters().values();
}
// Discs
public GameDisc getDiscById(int id) {
if (id <= 0) {
return null;
}
return this.discs.get(id);
}
public boolean hasDisc(int id) {
return this.discs.containsKey(id);
}
public GameDisc addDisc(int discId) {
// Sanity check to make sure we dont have this character already
if (this.hasDisc(discId)) {
return null;
}
return this.addDisc(GameData.getDiscDataTable().get(discId));
}
private GameDisc addDisc(DiscDef data) {
// Sanity check to make sure we dont have this character already
if (this.hasDisc(data.getId())) {
return null;
}
// Create disc
var disc = new GameDisc(this.getPlayer(), data);
// Save to database
disc.save();
// Add to discs
this.discs.put(disc.getDiscId(), disc);
return disc;
}
public Collection<GameDisc> getDiscCollection() {
return this.getDiscs().values();
}
// Database
public void loadFromDatabase() {
var db = Nebula.getGameDatabase();
db.getObjects(Character.class, "playerUid", getPlayerUid()).forEach(character -> {
// Get data
var data = GameData.getCharacterDataTable().get(character.getCharId());
// Validate
if (data == null) {
return;
}
character.setPlayer(this.getPlayer());
character.setData(data);
// Add to characters
this.characters.put(character.getCharId(), character);
});
db.getObjects(GameDisc.class, "playerUid", getPlayerUid()).forEach(disc -> {
// Get data
var data = GameData.getDiscDataTable().get(disc.getDiscId());
if (data == null) return;
disc.setPlayer(this.getPlayer());
disc.setData(data);
// Add
this.discs.put(disc.getDiscId(), disc);
});
}
}

View File

@@ -0,0 +1,216 @@
package emu.nebula.game.character;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.data.resources.DiscDef;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import emu.nebula.proto.Public.Disc;
import emu.nebula.proto.PublicStarTower.StarTowerDisc;
import lombok.Getter;
@Getter
@Entity(value = "discs", useDiscriminator = false)
public class GameDisc implements GameDatabaseObject {
@Id
private ObjectId uid;
@Indexed
private int playerUid;
private transient DiscDef data;
private transient Player player;
private int discId;
private int level;
private int exp;
private int phase;
private int star;
private long createTime;
@Deprecated // Morphia only!
public GameDisc() {
}
public GameDisc(Player player, int discId) {
this(player, GameData.getDiscDataTable().get(discId));
}
public GameDisc(Player player, DiscDef data) {
this.player = player;
this.playerUid = player.getUid();
this.data = data;
this.discId = data.getId();
this.level = 1;
this.createTime = Nebula.getCurrentTime();
}
public void setPlayer(Player player) {
this.player = player;
}
public void setData(DiscDef data) {
if (this.data == null && data.getId() == this.getDiscId()) {
this.data = data;
}
}
public int getMaxGainableExp() {
if (this.getLevel() >= this.getMaxLevel()) {
return 0;
}
int maxLevel = this.getMaxLevel();
int max = 0;
for (int i = this.getLevel() + 1; i <= maxLevel; i++) {
int dataId = (this.getData().getStrengthenGroupId() * 1000) + i;
var data = GameData.getDiscStrengthenDataTable().get(dataId);
if (data != null) {
max += data.getExp();
}
}
return Math.max(max - this.getExp(), 0);
}
public int getMaxExp() {
if (this.getLevel() >= this.getMaxLevel()) {
return 0;
}
int dataId = (this.getData().getStrengthenGroupId() * 1000) + (this.level + 1);
var data = GameData.getDiscStrengthenDataTable().get(dataId);
return data != null ? data.getExp() : 0;
}
public int getMaxLevel() {
return 10 + (this.getPhase() * 10);
}
public void addExp(int amount) {
// Setup
int expRequired = this.getMaxExp();
// Add exp
this.exp += amount;
// Check for level ups
while (this.exp >= expRequired && expRequired > 0) {
this.level += 1;
this.exp -= expRequired;
expRequired = this.getMaxExp();
}
// Clamp exp
if (this.getLevel() >= this.getMaxLevel()) {
this.exp = 0;
}
// Save to database
this.save();
}
// Handlers
public PlayerChangeInfo upgrade(ItemParamMap params) {
// Calculate exp gained
int exp = 0;
// Check if item is an exp item
for (var entry : params.getEntrySet()) {
var data = GameData.getDiscItemExpDataTable().get(entry.getIntKey());
if (data == null) return null;
exp += data.getExp() * entry.getIntValue();
}
// Clamp exp gain
exp = Math.min(this.getMaxGainableExp(), exp);
// Calculate gold required
params.add(GameConstants.GOLD_ITEM_ID, (int) Math.ceil(exp * 0.25D));
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(params)) {
return null;
}
// Create change info
var changes = new PlayerChangeInfo();
// Remove items
this.getPlayer().getInventory().removeItems(params, changes);
// Add exp
this.addExp(exp);
// Success
return changes.setSuccess(true);
}
public PlayerChangeInfo promote() {
// TODO check player level to make sure they can advance this disc
// Get promote data
int phaseId = (this.getData().getPromoteGroupId() * 1000) + (this.phase + 1);
var data = GameData.getDiscPromoteDataTable().get(phaseId);
if (data == null) {
return null;
}
// Verify that the player has the items
if (!this.getPlayer().getInventory().verifyItems(data.getMaterials())) {
return null;
}
// Remove items
var changes = this.getPlayer().getInventory().removeItems(data.getMaterials(), null);
// Add phase level
this.phase++;
// Save to database
this.save();
// Success
return changes.setSuccess(true);
}
// Proto
public Disc toProto() {
var proto = Disc.newInstance()
.setId(this.getDiscId())
.setLevel(this.getLevel())
.setExp(this.getExp())
.setPhase(this.getPhase())
.setStar(this.getStar())
.setCreateTime(this.getCreateTime());
return proto;
}
public StarTowerDisc toStarTowerProto() {
var proto = StarTowerDisc.newInstance()
.setId(this.getDiscId())
.setLevel(this.getLevel())
.setPhase(this.getPhase())
.setStar(this.getStar());
return proto;
}
}

View File

@@ -0,0 +1,57 @@
package emu.nebula.game.formation;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.Public.FormationInfo;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class Formation {
private int num;
private int[] charIds;
private int[] discIds;
@Deprecated
public Formation() {
}
public Formation(int num) {
this.num = num;
this.charIds = new int[3];
this.discIds = new int[6];
}
public Formation(FormationInfo formation) {
this.num = formation.getNumber();
this.charIds = formation.getCharIds().toArray();
this.discIds = formation.getDiscIds().toArray();
}
public int getCharIdAt(int i) {
if (i < 0 || i >= this.charIds.length) {
return -1;
}
return this.charIds[i];
}
public int getDiscIdAt(int i) {
if (i < 0 || i >= this.discIds.length) {
return -1;
}
return this.discIds[i];
}
// Proto
public FormationInfo toProto() {
var proto = FormationInfo.newInstance()
.setNumber(this.getNum())
.addAllCharIds(this.getCharIds())
.addAllDiscIds(this.getDiscIds());
return proto;
}
}

View File

@@ -0,0 +1,82 @@
package emu.nebula.game.formation;
import emu.nebula.game.player.PlayerManager;
import java.util.HashMap;
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.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.FormationInfo;
import emu.nebula.proto.Public.TowerFormation;
import lombok.Getter;
@Getter
@Entity(value = "formations", useDiscriminator = false)
public class FormationManager extends PlayerManager implements GameDatabaseObject {
@Id
private int uid;
private Map<Integer, Formation> formations;
@Deprecated // Morphia only
public FormationManager() {
}
public FormationManager(Player player) {
super(player);
this.uid = player.getUid();
this.formations = new HashMap<>();
this.save();
}
public Formation getFormationById(int num) {
return this.formations.get(num);
}
public boolean updateFormation(FormationInfo info) {
// Sanity check
if (info.getNumber() < 1 || info.getNumber() > GameConstants.MAX_FORMATIONS) {
return false;
}
// More sanity
if (info.getCharIds().length() < 1 || info.getCharIds().length() > 3) {
return false;
}
if (info.getDiscIds().length() < 3 || info.getDiscIds().length() > 6) {
return false;
}
// Validate formation to make sure we have all the chars and discs
// TODO
// Create formation
var formation = new Formation(info);
// Add to formations map
this.formations.put(formation.getNum(), formation);
// Save to db
Nebula.getGameDatabase().update(this, this.getPlayerUid(), "formations." + formation.getNum(), formation, true);
// Success
return true;
}
// Proto
public TowerFormation toProto() {
var proto = TowerFormation.newInstance();
return proto;
}
}

View File

@@ -0,0 +1,65 @@
package emu.nebula.game.inventory;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.Item;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
@Entity(value = "items", useDiscriminator = false)
public class GameItem implements GameDatabaseObject {
@Id
private ObjectId uid;
@Indexed
private int playerUid;
private int itemId;
private int count;
@Deprecated
public GameItem() {
}
public GameItem(Player player, int id, int count) {
this.playerUid = player.getUid();
this.itemId = id;
this.count = count;
}
public int add(int amount) {
int oldCount = this.count;
this.count = Utils.safeAdd(this.count, amount, Integer.MAX_VALUE, 0);
return this.count - oldCount;
}
// Database
@Override
public void save() {
if (this.getCount() <= 0) {
if (this.getUid() != null) {
Nebula.getGameDatabase().delete(this);
}
} else {
Nebula.getGameDatabase().save(this);
}
}
// Proto
public Item toProto() {
var proto = Item.newInstance()
.setTid(this.getItemId())
.setQty(this.getCount());
return proto;
}
}

View File

@@ -0,0 +1,63 @@
package emu.nebula.game.inventory;
import org.bson.types.ObjectId;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.Nebula;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.proto.Public.Res;
import emu.nebula.util.Utils;
import lombok.Getter;
@Getter
@Entity(value = "resources", useDiscriminator = false)
public class GameResource implements GameDatabaseObject {
@Id
private ObjectId uid;
@Indexed
private int playerUid;
public int resourceId;
public int count;
@Deprecated // Morphia only
public GameResource() {
}
public GameResource(Player player, int id, int count) {
this.playerUid = player.getUid();
this.resourceId = id;
this.count = count;
}
public int add(int amount) {
int oldCount = this.count;
this.count = Utils.safeAdd(this.count, amount, Integer.MAX_VALUE, 0);
return this.count - oldCount;
}
@Override
public void save() {
if (this.getCount() <= 0) {
if (this.getUid() != null) {
Nebula.getGameDatabase().delete(this);
}
} else {
Nebula.getGameDatabase().save(this);
}
}
// Proto
public Res toProto() {
var proto = Res.newInstance()
.setTid(this.getResourceId())
.setQty(this.getCount());
return proto;
}
}

View File

@@ -0,0 +1,310 @@
package emu.nebula.game.inventory;
import java.util.List;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.proto.Public.Item;
import emu.nebula.proto.Public.Res;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
@Getter
public class Inventory extends PlayerManager {
private final Int2ObjectMap<GameResource> resources;
private final Int2ObjectMap<GameItem> items;
public Inventory(Player player) {
super(player);
this.resources = new Int2ObjectOpenHashMap<>();
this.items = new Int2ObjectOpenHashMap<>();
}
// Resources
public synchronized int getResourceCount(int id) {
var res = this.resources.get(id);
return res != null ? res.getCount() : 0;
}
// Items
public synchronized int getItemCount(int id) {
var item = this.getItems().get(id);
return item != null ? item.getCount() : 0;
}
//
public synchronized PlayerChangeInfo addItem(int id, int count, PlayerChangeInfo changes) {
// Changes
if (changes == null) {
changes = new PlayerChangeInfo();
}
// Sanity
if (count == 0) {
return changes;
}
// Get game data
var data = GameData.getItemDataTable().get(id);
if (data == null) {
return changes;
}
// Set amount
int amount = count;
// Add item
switch (data.getItemType()) {
case Res -> {
var res = this.resources.get(id);
int diff = 0;
if (amount > 0) {
// Add resource
if (res == null) {
res = new GameResource(this.getPlayer(), id, amount);
this.resources.put(res.getResourceId(), res);
diff = amount;
} else {
diff = res.add(amount);
}
res.save();
} else {
// Remove resource
if (res == null) {
break;
}
diff = res.add(amount);
res.save();
if (res.getCount() < 0) {
this.resources.remove(id);
}
}
if (diff != 0) {
var change = Res.newInstance()
.setTid(id)
.setQty(diff);
changes.add(change);
}
}
case Item -> {
var item = this.items.get(id);
int diff = 0;
if (amount > 0) {
// Add resource
if (item == null) {
item = new GameItem(this.getPlayer(), id, amount);
this.items.put(item.getItemId(), item);
diff = amount;
} else {
diff = item.add(amount);
}
item.save();
} else {
// Remove resource
if (item == null) {
break;
}
diff = item.add(amount);
item.save();
if (item.getCount() < 0) {
this.resources.remove(id);
}
}
if (diff != 0) {
var change = Item.newInstance()
.setTid(id)
.setQty(diff);
changes.add(change);
}
}
case Disc -> {
if (amount <= 0) {
break;
}
var disc = getPlayer().getCharacters().addDisc(id);
if (disc != null) {
changes.add(disc.toProto());
}
}
case Char -> {
if (amount <= 0) {
break;
}
var character = getPlayer().getCharacters().addCharacter(id);
if (character != null) {
changes.add(character.toProto());
}
}
case WorldRankExp -> {
this.getPlayer().addExp(amount, changes);
}
default -> {
// Not implemented
}
}
//
return changes;
}
@Deprecated
public synchronized PlayerChangeInfo addItems(List<ItemParam> params, PlayerChangeInfo changes) {
// Changes
if (changes == null) {
changes = new PlayerChangeInfo();
}
for (ItemParam param : params) {
this.addItem(param.getId(), param.getCount(), changes);
}
return changes;
}
public synchronized PlayerChangeInfo addItems(ItemParamMap params) {
return this.addItems(params, null);
}
public synchronized PlayerChangeInfo addItems(ItemParamMap params, PlayerChangeInfo changes) {
// Changes
if (changes == null) {
changes = new PlayerChangeInfo();
}
for (var param : params.getEntrySet()) {
this.addItem(param.getIntKey(), param.getIntValue(), changes);
}
return changes;
}
public synchronized PlayerChangeInfo removeItem(int id, int count, PlayerChangeInfo changes) {
if (count > 0) {
count = -count;
}
return this.addItem(id, count, changes);
}
public synchronized PlayerChangeInfo removeItems(ItemParamMap params) {
return this.removeItems(params, null);
}
public synchronized PlayerChangeInfo removeItems(ItemParamMap params, PlayerChangeInfo changes) {
// Changes
if (changes == null) {
changes = new PlayerChangeInfo();
}
for (var param : params.getEntrySet()) {
this.removeItem(param.getIntKey(), param.getIntValue(), changes);
}
return changes;
}
/**
* Checks if the player has enough quanity of this item
*/
public synchronized boolean verifyItem(int id, int count) {
// Sanity check
if (count == 0) {
return true;
} else if (count < 0) {
// Return false if we are trying to verify negative numbers
return false;
}
// Get game data
var data = GameData.getItemDataTable().get(id);
if (data == null) {
return false;
}
boolean result = switch (data.getItemType()) {
case Res -> {
yield this.getResourceCount(id) >= count;
}
case Item -> {
yield this.getItemCount(id) >= count;
}
case Disc -> {
yield getPlayer().getCharacters().hasDisc(id);
}
case Char -> {
yield getPlayer().getCharacters().hasCharacter(id);
}
default -> {
// Not implemented
yield false;
}
};
//
return result;
}
public synchronized boolean verifyItems(ItemParamMap params) {
boolean hasItems = true;
for (var param : params.getEntrySet()) {
hasItems = this.verifyItem(param.getIntKey(), param.getIntValue());
if (!hasItems) {
return hasItems;
}
}
return hasItems;
}
// Database
public void loadFromDatabase() {
var db = Nebula.getGameDatabase();
db.getObjects(GameItem.class, "playerUid", getPlayerUid()).forEach(item -> {
// Get data
var data = GameData.getItemDataTable().get(item.getItemId());
if (data == null) return;
// Add
this.items.put(item.getItemId(), item);
});
db.getObjects(GameResource.class, "playerUid", getPlayerUid()).forEach(res -> {
// Get data
var data = GameData.getItemDataTable().get(res.getResourceId());
if (data == null) return;
// Add
this.resources.put(res.getResourceId(), res);
});
}
}

View File

@@ -0,0 +1,30 @@
package emu.nebula.game.inventory;
import dev.morphia.annotations.Entity;
import emu.nebula.proto.Public.ItemTpl;
import lombok.Getter;
@Getter
@Entity(useDiscriminator = false)
public class ItemParam {
public int id;
public int count;
@Deprecated // Morphia only
public ItemParam() {
}
public ItemParam(int id, int count) {
this.id = id;
this.count = count;
}
public ItemTpl toProto() {
var proto = ItemTpl.newInstance()
.setTid(this.getId())
.setQty(this.getCount());
return proto;
}
}

View File

@@ -0,0 +1,100 @@
package emu.nebula.game.inventory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import emu.nebula.proto.Public.ItemInfo;
import emu.nebula.proto.Public.ItemTpl;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import us.hebi.quickbuf.RepeatedMessage;
public class ItemParamMap extends Int2IntOpenHashMap {
private static final long serialVersionUID = -4186524272780523459L;
public FastEntrySet entries() {
return this.int2IntEntrySet();
}
@Override @Deprecated
public int addTo(int itemId, int count) {
return this.add(itemId, count);
}
public int add(int itemId, int count) {
if (count == 0) {
return 0;
}
return super.addTo(itemId, count);
}
/**
* Adds all item params from the other map to this one
* @param map The other item param map
*/
public void add(ItemParamMap map) {
for (var entry : map.entries()) {
this.add(entry.getIntKey(), entry.getIntValue());
}
}
/**
* Returns a new ItemParamMap with item amounts multiplied
* @param mult Value to multiply all item amounts in this map by
* @return
*/
public ItemParamMap mulitply(int multiplier) {
var params = new ItemParamMap();
for (var entry : this.int2IntEntrySet()) {
params.put(entry.getIntKey(), entry.getIntValue() * multiplier);
}
return params;
}
//
public FastEntrySet getEntrySet() {
return this.int2IntEntrySet();
}
public List<ItemParam> toList() {
List<ItemParam> list = new ArrayList<>();
for (var entry : this.int2IntEntrySet()) {
list.add(new ItemParam(entry.getIntKey(), entry.getIntValue()));
}
return list;
}
public Stream<ItemTpl> itemTemplateStream() {
return getEntrySet()
.stream()
.map(e -> ItemTpl.newInstance().setTid(e.getIntKey()).setQty(e.getIntValue()));
}
// Proto
public static ItemParamMap fromTemplates(RepeatedMessage<ItemTpl> items) {
var map = new ItemParamMap();
for (var template : items) {
map.add(template.getTid(), template.getQty());
}
return map;
}
public static ItemParamMap fromItemInfos(RepeatedMessage<ItemInfo> items) {
var map = new ItemParamMap();
for (var template : items) {
map.add(template.getTid(), template.getQty());
}
return map;
}
}

View File

@@ -0,0 +1,56 @@
package emu.nebula.game.inventory;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum ItemSubType {
Res (1),
Item (2),
Char (3),
Energy (4),
WorldRankExp (5),
CharShard (6),
Disc (8),
TalentStrengthen (9),
DiscStrengthen (12),
DiscPromote (13),
TreasureBox (17),
GearTreasureBox (18),
SubNoteSkill (19),
SkillStrengthen (24),
CharacterLimitBreak (25),
MonthlyCard (30),
EnergyItem (31),
ComCYO (32),
OutfitCYO (33),
RandomPackage (34),
Equipment (35),
FateCard (37),
EquipmentExp (38),
DiscLimitBreak (40),
Potential (41),
SpecificPotential (42),
Honor (43),
CharacterYO (44),
PlayHead (45),
CharacterSkin (46);
@Getter
private final int value;
private final static Int2ObjectMap<ItemSubType> map = new Int2ObjectOpenHashMap<>();
static {
for (ItemSubType type : ItemSubType.values()) {
map.put(type.getValue(), type);
}
}
private ItemSubType(int value) {
this.value = value;
}
public static ItemSubType getByValue(int value) {
return map.get(value);
}
}

View File

@@ -0,0 +1,39 @@
package emu.nebula.game.inventory;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum ItemType {
Res (1),
Item (2),
Char (3),
Energy (4),
WorldRankExp (5),
RogueItem (6),
Disc (7),
Equipment (8),
CharacterSkin (9),
MonthlyCard (10),
Title (11),
Honor (12),
HeadItem (13);
@Getter
private final int value;
private final static Int2ObjectMap<ItemType> map = new Int2ObjectOpenHashMap<>();
static {
for (ItemType type : ItemType.values()) {
map.put(type.getValue(), type);
}
}
private ItemType(int value) {
this.value = value;
}
public static ItemType getByValue(int value) {
return map.get(value);
}
}

View File

@@ -0,0 +1,86 @@
package emu.nebula.game.mail;
import java.util.concurrent.TimeUnit;
import dev.morphia.annotations.Entity;
import emu.nebula.Nebula;
import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.proto.Public.Mail;
import lombok.Getter;
import lombok.Setter;
@Getter
@Entity(useDiscriminator = false)
public class GameMail {
private int id;
private String author;
private String subject;
private String desc;
private ItemParamMap attachments;
@Setter private boolean read;
@Setter private boolean recv;
@Setter private boolean pin;
private long flag;
private long time;
private long expiry;
@Deprecated // Morphia only
public GameMail() {
}
public GameMail(String author, String subject, String desc) {
this.author = author;
this.subject = subject;
this.desc = desc;
this.time = Nebula.getCurrentTime();
this.expiry = this.time + TimeUnit.DAYS.toSeconds(30);
}
protected void setId(int id) {
if (this.id == 0) {
this.id = id;
}
}
public boolean canRemove() {
return (this.isRead() || this.isRecv()) && !this.isPin() && (this.hasAttachments() && this.isRecv());
}
public boolean hasAttachments() {
return this.attachments != null;
}
public void addAttachment(int itemId, int count) {
if (this.attachments == null) {
this.attachments = new ItemParamMap();
}
this.attachments.add(itemId, count);
}
public Mail toProto() {
var proto = Mail.newInstance()
.setId(this.getId())
.setAuthor(this.getAuthor())
.setSubject(this.getSubject())
.setDesc(this.getDesc())
.setTime(this.getTime())
.setRead(this.isRead())
.setRecv(this.isRecv())
.setPin(this.isPin())
.setFlag(this.getFlag())
.setDeadline(this.getExpiry());
if (this.getAttachments() != null) {
this.getAttachments().itemTemplateStream()
.forEach(proto::addAttachments);
}
return proto;
}
}

View File

@@ -0,0 +1,197 @@
package emu.nebula.game.mail;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.Nebula;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import lombok.Getter;
@Getter
@Entity(value = "mailbox", useDiscriminator = false)
public class Mailbox implements GameDatabaseObject, Iterable<GameMail> {
@Id
private int uid;
private int lastMailId;
private List<GameMail> list;
@Deprecated // Morphia only
public Mailbox() {
}
public Mailbox(Player player) {
this.uid = player.getUid();
this.list = new ArrayList<>();
this.save();
}
// TODO optimize to an O(n) algorithm like a map
public GameMail getMailById(int id) {
return this.getList().stream()
.filter(m -> m.getId() == id)
.findFirst()
.orElse(null);
}
public void sendMail(GameMail mail) {
// Set mail id
mail.setId(++this.lastMailId);
// Add to mail list
this.list.add(mail);
// Save to database
Nebula.getGameDatabase().update(this, getUid(), "lastMailId", this.getLastMailId());
Nebula.getGameDatabase().addToList(this, getUid(), "list", mail);
}
public boolean readMail(int id, long flag) {
// Get mail
var mail = this.getMailById(id);
if (mail == null) {
return false;
}
// Set read
mail.setRead(true);
// Update in database
Nebula.getGameDatabase().updateNested(this, getUid(), "list.id", id, "list.$.read", true);
// Success
return true;
}
public GameMail pinMail(int id, long flag, boolean pin) {
// Get mail
var mail = this.getMailById(id);
if (mail == null) {
return null;
}
// Set pin
mail.setPin(pin);
// Update in database
Nebula.getGameDatabase().updateNested(this, getUid(), "list.id", id, "list.$.pin", true);
// Success
return mail;
}
public PlayerChangeInfo recvMail(Player player, int id) {
// Get mails that we want to claim
List<GameMail> mails = null;
if (id == 0) {
// Claim all
mails = this.getList()
.stream()
.filter(mail -> !mail.isRecv() && mail.hasAttachments())
.toList();
} else {
// Claim one
var mail = this.getMailById(id);
if (mail != null && !mail.isRecv() && mail.hasAttachments()) {
mails = List.of(mail);
}
}
// Create change info
var changes = new PlayerChangeInfo();
// Sanity
if (mails == null || mails.isEmpty()) {
return changes;
}
// Recieved mail id list
var recvMails = new IntArrayList();
// Recv mails
for (var mail : mails) {
// Add attachments to player
player.getInventory().addItems(mail.getAttachments(), changes);
// Set claimed flag
mail.setRecv(true);
// Add to recvied mail list
recvMails.add(mail.getId());
// Update in database
Nebula.getGameDatabase().updateNested(this, getUid(), "list.id", mail.getId(), "list.$.recv", true);
}
// Set extra change data
changes.setExtraData(recvMails);
// Success
return changes.setSuccess(true);
}
public IntList removeMail(Player player, int id) {
// Get mails that we want to claim
Set<GameMail> toRemove = null;
if (id == 0) {
// Claim all
toRemove = this.getList()
.stream()
.filter(mail -> mail.canRemove())
.collect(Collectors.toSet());
} else {
// Claim one
var mail = this.getMailById(id);
if (mail != null && mail.canRemove()) {
toRemove = Set.of(mail);
}
}
// Recieved mail id list
var removed = new IntArrayList();
// Sanity check
if (toRemove == null || toRemove.isEmpty()) {
return removed;
}
// Remove
var it = this.getList().iterator();
while (it.hasNext()) {
var mail = it.next();
if (toRemove.contains(mail)) {
removed.add(mail.getId());
it.remove();
}
}
// Save
this.save();
// Success
return removed;
}
@Override
public Iterator<GameMail> iterator() {
return this.getList().iterator();
}
}

View File

@@ -0,0 +1,358 @@
package emu.nebula.game.player;
import java.util.HashSet;
import java.util.Set;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Indexed;
import emu.nebula.GameConstants;
import emu.nebula.Nebula;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.account.Account;
import emu.nebula.game.character.CharacterStorage;
import emu.nebula.game.formation.FormationManager;
import emu.nebula.game.inventory.Inventory;
import emu.nebula.game.mail.Mailbox;
import emu.nebula.game.tower.StarTowerManager;
import emu.nebula.net.GameSession;
import emu.nebula.proto.PlayerData.PlayerInfo;
import emu.nebula.proto.Public.NewbieInfo;
import emu.nebula.proto.Public.QuestType;
import emu.nebula.proto.Public.WorldClass;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Getter;
@Getter
@Entity(value = "players", useDiscriminator = false)
public class Player implements GameDatabaseObject {
@Id private int uid;
@Indexed private String accountUid;
private transient Account account;
private transient Set<GameSession> sessions;
// Details
private String name;
private boolean gender;
private int headIcon;
private int skinId;
private int titlePrefix;
private int titleSuffix;
private int level;
private int exp;
private int energy;
private IntSet boards;
private IntSet titles;
private long createTime;
// Managers
private final transient CharacterStorage characters;
private final transient Inventory inventory;
// Referenced data
private transient FormationManager formations;
private transient Mailbox mailbox;
private transient StarTowerManager starTowerManager;
@Deprecated // Morphia only
public Player() {
this.sessions = new HashSet<>();
this.characters = new CharacterStorage(this);
this.inventory = new Inventory(this);
}
public Player(Account account, String name, boolean gender) {
this();
// Set uid first
if (account.getReservedPlayerUid() > 0) {
this.uid = account.getReservedPlayerUid();
} else {
this.uid = Nebula.getGameDatabase().getNextObjectId(Player.class);
}
// Set basic info
this.accountUid = account.getUid();
this.name = name;
this.gender = gender;
this.headIcon = 101;
this.skinId = 10301;
this.titlePrefix = 1;
this.titleSuffix = 2;
this.level = 1;
this.boards = new IntOpenHashSet();
this.titles = new IntOpenHashSet();
this.createTime = Nebula.getCurrentTime();
// Add starter characters
this.getCharacters().addCharacter(103);
this.getCharacters().addCharacter(112);
this.getCharacters().addCharacter(113);
// Add starter discs
this.getCharacters().addDisc(211001);
this.getCharacters().addDisc(211005);
this.getCharacters().addDisc(211007);
this.getCharacters().addDisc(211008);
// Add titles
this.getTitles().add(this.getTitlePrefix());
this.getTitles().add(this.getTitleSuffix());
// Add board ids
this.getBoards().add(410301);
}
public Account getAccount() {
if (this.account == null) {
this.account = Nebula.getAccountDatabase().getObjectByField(Account.class, "_id", this.getAccountUid());
}
return this.account;
}
public void addSession(GameSession session) {
synchronized (this.sessions) {
this.sessions.add(session);
}
}
public void removeSession(GameSession session) {
synchronized (this.sessions) {
this.sessions.remove(session);
}
}
public boolean hasSessions() {
synchronized (this.sessions) {
return !this.sessions.isEmpty();
}
}
public boolean getGender() {
return this.gender;
}
public boolean editName(String newName) {
// Sanity check
if (newName == null || newName.isEmpty() || newName.equals(this.getName())) {
return false;
}
// Limit name length
if (newName.length() > 20) {
newName = newName.substring(0, 19);
}
// Set name
this.name = newName;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "name", this.getName());
// Success
return true;
}
public void editGender() {
// Set name
this.gender = !this.gender;
// Update in database
Nebula.getGameDatabase().update(this, this.getUid(), "gender", this.getGender());
}
public void setNewbieInfo(int groupId, int stepId) {
// TODO
}
public int getMaxExp() {
var data = GameData.getWorldClassDataTable().get(this.level + 1);
return data != null ? data.getExp() : 0;
}
public PlayerChangeInfo addExp(int amount, PlayerChangeInfo changes) {
// Check if changes is null
if (changes == null) {
changes = new PlayerChangeInfo();
}
// Sanity
if (amount <= 0) {
return changes;
}
// Setup
int oldLevel = this.getLevel();
int oldExp = this.getExp();
int expRequired = this.getMaxExp();
// Add exp
this.exp += amount;
// Check for level ups
while (this.exp >= expRequired && expRequired > 0) {
this.level += 1;
this.exp -= expRequired;
expRequired = this.getMaxExp();
}
// Save to database
Nebula.getGameDatabase().update(
this,
this.getUid(),
"level",
this.getLevel(),
"exp",
this.getExp()
);
// Calculate changes
var proto = WorldClass.newInstance()
.setAddClass(this.getLevel() - oldLevel)
.setExpChange(this.getExp() - oldExp);
changes.add(proto);
return changes;
}
public void sendMessage(String string) {
// Empty
}
// Login
public void onLoad() {
// Load from database
this.getCharacters().loadFromDatabase();
this.getInventory().loadFromDatabase();
// Load referenced classes
this.formations = Nebula.getGameDatabase().getObjectByField(FormationManager.class, "_id", this.getUid());
if (this.formations == null) {
this.formations = new FormationManager(this);
} else {
this.formations.setPlayer(this);
}
this.mailbox = Nebula.getGameDatabase().getObjectByField(Mailbox.class, "_id", this.getUid());
if (this.mailbox == null) {
this.mailbox = new Mailbox(this);
}
this.starTowerManager = Nebula.getGameDatabase().getObjectByField(StarTowerManager.class, "_id", this.getUid());
if (this.starTowerManager == null) {
this.starTowerManager = new StarTowerManager(this);
} else {
this.starTowerManager.setPlayer(this);
}
}
// Proto
public PlayerInfo toProto() {
PlayerInfo proto = PlayerInfo.newInstance();
var acc = proto.getMutableAcc()
.setNickName(this.getName())
.setGender(this.getGender())
.setId(this.getUid())
.setHeadIcon(this.getHeadIcon())
.setSkinId(this.getSkinId())
.setTitlePrefix(this.getTitlePrefix())
.setTitleSuffix(this.getTitleSuffix())
.setCreateTime(this.getCreateTime());
proto.getMutableWorldClass()
.setStage(3)
.setCur(this.getLevel())
.setLastExp(this.getExp());
proto.getMutableEnergy()
.getMutableEnergy()
.setUpdateTime(Nebula.getCurrentTime())
.setNextDuration(60)
.setPrimary(240)
.setIsPrimary(true);
// Add characters/discs/res/items
for (var character : getCharacters().getCharacterCollection()) {
proto.addChars(character.toProto());
}
for (var disc : getCharacters().getDiscCollection()) {
proto.addDiscs(disc.toProto());
}
for (var item : getInventory().getItems().values()) {
proto.addItems(item.toProto());
}
for (var res : getInventory().getResources().values()) {
proto.addRes(res.toProto());
}
// Formations
for (var f : this.getFormations().getFormations().values()) {
proto.getMutableFormation().addInfo(f.toProto());
}
// Set state
var state = proto.getMutableState()
.setStorySet(true);
state.getMutableMail();
state.getMutableBattlePass();
state.getMutableWorldClassReward();
state.getMutableFriendEnergy();
state.getMutableMallPackage();
state.getMutableAchievement();
state.getMutableTravelerDuelQuest()
.setType(QuestType.TravelerDuel);
state.getMutableTravelerDuelChallengeQuest()
.setType(QuestType.TravelerDuelChallenge);
state.getMutableStarTower();
state.getMutableStarTowerBook();
state.getMutableScoreBoss();
state.getMutableCharAffinityRewards();
// Force complete tutorials
for (var guide : GameData.getGuideGroupDataTable()) {
var info = NewbieInfo.newInstance()
.setGroupId(guide.getId())
.setStepId(-1);
acc.addNewbies(info);
}
acc.addNewbies(NewbieInfo.newInstance().setGroupId(GameConstants.INTRO_GUIDE_ID).setStepId(-1));
//
proto.addBoard(410301);
proto.setServerTs(Nebula.getCurrentTime());
// Extra
proto.setAchievements(new byte[64]);
proto.getMutableVampireSurvivorRecord()
.getMutableSeason();
proto.getMutableQuests();
proto.getMutableAgent();
proto.getMutablePhone();
proto.getMutableStory();
return proto;
}
}

View File

@@ -0,0 +1,49 @@
package emu.nebula.game.player;
import java.util.ArrayList;
import java.util.List;
import emu.nebula.GameConstants;
import emu.nebula.proto.AnyOuterClass.Any;
import emu.nebula.proto.Public.ChangeInfo;
import lombok.Getter;
import lombok.Setter;
import us.hebi.quickbuf.ProtoMessage;
@Getter
public class PlayerChangeInfo {
private boolean success;
private List<Any> list;
@Setter
private Object extraData;
public PlayerChangeInfo() {
this.list = new ArrayList<>();
}
public PlayerChangeInfo setSuccess(boolean success) {
this.success = success;
return this;
}
public void add(ProtoMessage<?> proto) {
var any = Any.newInstance()
.setTypeUrl(GameConstants.PROTO_BASE_TYPE_URL + proto.getClass().getSimpleName())
.setValue(proto.toByteArray());
this.list.add(any);
}
// Proto
public ChangeInfo toProto() {
var proto = ChangeInfo.newInstance();
for (var any : this.getList()) {
proto.addProps(any);
}
return proto;
}
}

View File

@@ -0,0 +1,27 @@
package emu.nebula.game.player;
public abstract class PlayerManager {
private transient Player player;
public PlayerManager() {
}
public PlayerManager(Player player) {
this.player = player;
}
public Player getPlayer() {
return this.player;
}
public void setPlayer(Player player) {
if (this.player == null) {
this.player = player;
}
}
public int getPlayerUid() {
return this.getPlayer().getUid();
}
}

View File

@@ -0,0 +1,116 @@
package emu.nebula.game.player;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import emu.nebula.Nebula;
import emu.nebula.game.GameContext;
import emu.nebula.game.GameContextModule;
import emu.nebula.game.account.Account;
import emu.nebula.net.GameSession;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
public class PlayerModule extends GameContextModule {
private final Int2ObjectMap<Player> cachedPlayers;
private final Object2ObjectMap<String, Player> cachedPlayersByAccount;
public PlayerModule(GameContext gameContext) {
super(gameContext);
this.cachedPlayers = new Int2ObjectOpenHashMap<>();
this.cachedPlayersByAccount = new Object2ObjectOpenHashMap<>();
}
public Int2ObjectMap<Player> getCachedPlayers() {
return cachedPlayers;
}
private void addToCache(Player player) {
this.cachedPlayers.put(player.getUid(), player);
this.cachedPlayersByAccount.put(player.getAccountUid(), player);
}
public void removeFromCache(Player player) {
this.cachedPlayers.remove(player.getUid());
this.cachedPlayersByAccount.remove(player.getAccountUid());
}
/**
* Returns a player object that has been previously cached. Returns null if the player isnt in the cache.
* @param uid User id of the player
* @return
*/
public synchronized Player getCachedPlayerByUid(int uid) {
return getCachedPlayers().get(uid);
}
/**
* Returns a player object with the given account. Returns null if the player doesnt exist.
* @param uid User id of the player
* @return
*/
public synchronized Player getPlayerByAccount(Account account) {
// Get player from cache
Player player = this.cachedPlayersByAccount.get(account.getUid());
if (player == null) {
// Retrieve player object from database if its not there
player = Nebula.getGameDatabase().getObjectByField(Player.class, "accountUid", account.getUid());
if (player != null) {
// Load player
player.onLoad();
// Put in cache
this.addToCache(player);
}
}
return player;
}
/**
* Creates a player with the specified user id.
* @param userId
* @return
*/
public synchronized Player createPlayer(GameSession session, String name, boolean gender) {
// Make sure player doesnt already exist
if (Nebula.getGameDatabase().checkIfObjectExists(Player.class, "accountUid", session.getAccount().getUid())) {
return null;
}
// Limit name length
if (name.length() > 20) {
name = name.substring(0, 19);
}
// Create player and save to db
var player = new Player(session.getAccount(), name, gender);
player.onLoad();
player.save();
// Put in cache
this.addToCache(player);
// Set player for session
session.setPlayer(player);
return player;
}
/**
* Returns a list of recent players that have logged on (for followers)
* @param player Player that requested this
*/
public synchronized List<Player> getRandomPlayerList(Player player) {
List<Player> list = getCachedPlayers().values().stream().filter(p -> p != player).collect(Collectors.toList());
Collections.shuffle(list);
return list.stream().limit(15).toList();
}
}

View File

@@ -0,0 +1,12 @@
package emu.nebula.game.story;
import dev.morphia.annotations.Entity;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.PlayerManager;
import lombok.Getter;
@Getter
@Entity(value = "story", useDiscriminator = false)
public class StoryManager extends PlayerManager implements GameDatabaseObject {
}

View File

@@ -0,0 +1,39 @@
package emu.nebula.game.tower;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
public enum CaseType {
Battle (1),
OpenDoor (2),
PotentialSelect (3),
FateCardSelect (4),
NoteSelect (5),
NpcEvent (6),
SelectSpecialPotential (7),
RecoveryHP (8),
NpcRecoveryHP (9),
Hawker (10),
StrengthenMachine (11),
DoorDanger (12),
SyncHP (13);
@Getter
private final int value;
private final static Int2ObjectMap<CaseType> map = new Int2ObjectOpenHashMap<>();
static {
for (CaseType type : CaseType.values()) {
map.put(type.getValue(), type);
}
}
private CaseType(int value) {
this.value = value;
}
public static CaseType getByValue(int value) {
return map.get(value);
}
}

View File

@@ -0,0 +1,55 @@
package emu.nebula.game.tower;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class StarTowerCase {
private int id;
@Setter(AccessLevel.NONE)
private CaseType type;
// Extra data
private int teamLevel;
private int floorId;
// Select
private int[] ids;
public StarTowerCase(CaseType type) {
this.type = type;
}
public StarTowerRoomCase toProto() {
var proto = StarTowerRoomCase.newInstance()
.setId(this.getId());
switch (this.type) {
case Battle -> {
proto.getMutableBattleCase();
}
case OpenDoor -> {
proto.getMutableDoorCase();
}
case SyncHP -> {
proto.getMutableSyncHPCase();
}
case SelectSpecialPotential -> {
proto.getMutableSelectSpecialPotentialCase();
}
case PotentialSelect -> {
proto.getMutableSelectPotentialCase();
}
default -> {
}
}
return proto;
}
}

View File

@@ -0,0 +1,256 @@
package emu.nebula.game.tower;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dev.morphia.annotations.Entity;
import emu.nebula.data.resources.StarTowerDef;
import emu.nebula.game.formation.Formation;
import emu.nebula.game.player.Player;
import emu.nebula.proto.PublicStarTower.StarTowerChar;
import emu.nebula.proto.PublicStarTower.StarTowerDisc;
import emu.nebula.proto.PublicStarTower.StarTowerInfo;
import emu.nebula.proto.PublicStarTower.StarTowerRoomCase;
import emu.nebula.proto.StarTowerApply.StarTowerApplyReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractReq;
import emu.nebula.proto.StarTowerInteract.StarTowerInteractResp;
import emu.nebula.util.Snowflake;
import emu.nebula.util.Utils;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import lombok.Getter;
import lombok.SneakyThrows;
@Getter
@Entity(useDiscriminator = false)
public class StarTowerInstance {
private transient StarTowerManager manager;
private transient StarTowerDef data;
// Tower id
private int id;
// Room
private int floor;
private int mapId;
private int mapTableId;
private String mapParam;
private int paramId;
// Team
private int formationId;
private int buildId;
private int teamLevel;
private int teamExp;
private int charHp;
private int battleTime;
private List<StarTowerChar> chars;
private List<StarTowerDisc> discs;
private int lastCaseId = 0;
private List<StarTowerCase> cases;
private Int2IntMap items;
@Deprecated // Morphia only
public StarTowerInstance() {
}
public StarTowerInstance(StarTowerManager manager, StarTowerDef data, Formation formation, StarTowerApplyReq req) {
this.manager = manager;
this.data = data;
this.id = req.getId();
this.mapId = req.getMapId();
this.mapTableId = req.getMapTableId();
this.mapParam = req.getMapParam();
this.paramId = req.getParamId();
this.formationId = req.getFormationId();
this.buildId = Snowflake.newUid();
this.teamLevel = 1;
this.floor = 1;
this.charHp = -1;
this.chars = new ArrayList<>();
this.discs = new ArrayList<>();
this.cases = new ArrayList<>();
this.items = new Int2IntOpenHashMap();
// Init formation
for (int i = 0; i < 3; i++) {
int id = formation.getCharIdAt(i);
var character = getPlayer().getCharacters().getCharacterById(id);
if (character != null) {
chars.add(character.toStarTowerProto());
} else {
chars.add(StarTowerChar.newInstance());
}
}
for (int i = 0; i < 6; i++) {
int id = formation.getDiscIdAt(i);
var disc = getPlayer().getCharacters().getDiscById(id);
if (disc != null) {
discs.add(disc.toStarTowerProto());
} else {
discs.add(StarTowerDisc.newInstance());
}
}
// Add cases
this.addCase(new StarTowerCase(CaseType.Battle));
this.addCase(new StarTowerCase(CaseType.SyncHP));
var doorCase = this.addCase(new StarTowerCase(CaseType.OpenDoor));
doorCase.setFloorId(this.getFloor() + 1);
}
public Player getPlayer() {
return this.manager.getPlayer();
}
public StarTowerCase addCase(StarTowerCase towerCase) {
return this.addCase(null, towerCase);
}
public StarTowerCase addCase(StarTowerInteractResp rsp, StarTowerCase towerCase) {
// Add to cases list
this.cases.add(towerCase);
// Increment id
towerCase.setId(++this.lastCaseId);
// Set proto
if (rsp != null) {
rsp.getMutableCases().add(towerCase.toProto());
}
return towerCase;
}
public StarTowerInteractResp handleInteract(StarTowerInteractReq req) {
var rsp = StarTowerInteractResp.newInstance()
.setId(req.getId());
if (req.hasBattleEndReq()) {
this.onBattleEnd(req, rsp);
} else if (req.hasRecoveryHPReq()) {
var proto = req.getRecoveryHPReq();
} else if (req.hasSelectReq()) {
} else if (req.hasEnterReq()) {
this.onEnterReq(req, rsp);
}
// Set data protos
rsp.getMutableData();
rsp.getMutableChange();
//rsp.getMutableNextPackage();
return rsp;
}
// Interact events
@SneakyThrows
public void onBattleEnd(StarTowerInteractReq req, StarTowerInteractResp rsp) {
var proto = req.getBattleEndReq();
if (proto.hasVictory()) {
// Add team level
this.teamLevel++;
// Add clear time
this.battleTime += proto.getVictory().getTime();
// Handle victory
rsp.getMutableBattleEndResp()
.getMutableVictory()
.setLv(this.getTeamLevel())
.setBattleTime(this.getBattleTime());
// Add potential selector TODO
} else {
// Handle defeat
}
}
public void onSelect(StarTowerInteractReq req, StarTowerInteractResp rsp) {
}
public void onEnterReq(StarTowerInteractReq req, StarTowerInteractResp rsp) {
var proto = req.getEnterReq();
// Set
this.floor = this.floor++;
this.mapId = proto.getMapId();
this.mapTableId = proto.getMapTableId();
// Clear cases TODO
this.lastCaseId = 0;
this.cases.clear();
// Add cases
var syncHpCase = this.addCase(new StarTowerCase(CaseType.SyncHP));
var doorCase = this.addCase(new StarTowerCase(CaseType.OpenDoor));
doorCase.setFloorId(this.getFloor() + 1);
// Proto
var room = rsp.getMutableEnterResp().getMutableRoom();
room.getMutableData()
.setMapId(this.getMapId())
.setMapTableId(this.getMapTableId())
.setFloor(this.getFloor());
room.addAllCases(syncHpCase.toProto(), doorCase.toProto());
}
public void onRecoveryHP(StarTowerInteractReq req, StarTowerInteractResp rsp) {
// Add case
this.addCase(rsp, new StarTowerCase(CaseType.RecoveryHP));
}
// Proto
public StarTowerInfo toProto() {
var proto = StarTowerInfo.newInstance();
proto.getMutableMeta()
.setId(this.getId())
.setCharHp(this.getCharHp())
.setTeamLevel(this.getTeamLevel())
.setNPCInteractions(1)
.setBuildId(this.getBuildId());
this.getChars().forEach(proto.getMutableMeta()::addChars);
this.getDiscs().forEach(proto.getMutableMeta()::addDiscs);
proto.getMutableRoom().getMutableData()
.setFloor(this.getFloor())
.setMapId(this.getMapId())
.setMapTableId(this.getMapTableId())
.setMapParam(this.getMapParam())
.setParamId(this.getParamId());
// Cases
for (var starTowerCase : this.getCases()) {
proto.getMutableRoom().addCases(starTowerCase.toProto());
}
// TODO
proto.getMutableBag();
return proto;
}
}

View File

@@ -0,0 +1,51 @@
package emu.nebula.game.tower;
import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import emu.nebula.data.GameData;
import emu.nebula.database.GameDatabaseObject;
import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerManager;
import emu.nebula.proto.StarTowerApply.StarTowerApplyReq;
import lombok.Getter;
@Getter
@Entity(value = "star_tower", useDiscriminator = false)
public class StarTowerManager extends PlayerManager implements GameDatabaseObject {
@Id
private int uid;
private transient StarTowerInstance instance;
@Deprecated // Morphia only
public StarTowerManager() {
}
public StarTowerManager(Player player) {
super(player);
this.uid = player.getUid();
this.save();
}
public StarTowerInstance apply(StarTowerApplyReq req) {
// Sanity checks
var data = GameData.getStarTowerDataTable().get(req.getId());
if (data == null) {
return null;
}
// Get formation
var formation = getPlayer().getFormations().getFormationById(req.getFormationId());
if (formation == null) {
return null;
}
// Create instance
this.instance = new StarTowerInstance(this, data, formation, req);
// Success
return this.instance;
}
}