Rework and optimize inventory

`GameItem`s and `GameResource`s are now deprecated, they are stored as regular `ItemParamMap`s in the `Inventory` class instead. This is to reduce database load when loading a player.
This commit is contained in:
Melledy
2026-04-15 03:01:31 -07:00
parent fa91ab0555
commit 9ba38d5a5e
7 changed files with 129 additions and 74 deletions
@@ -5,8 +5,6 @@ import emu.nebula.command.CommandArgs;
import emu.nebula.command.CommandHandler; import emu.nebula.command.CommandHandler;
import emu.nebula.data.GameData; import emu.nebula.data.GameData;
import emu.nebula.data.resources.ItemDef; import emu.nebula.data.resources.ItemDef;
import emu.nebula.game.inventory.GameItem;
import emu.nebula.game.inventory.GameResource;
import emu.nebula.game.inventory.ItemParamMap; import emu.nebula.game.inventory.ItemParamMap;
import emu.nebula.game.inventory.ItemType; import emu.nebula.game.inventory.ItemType;
import emu.nebula.game.player.PlayerChangeInfo; import emu.nebula.game.player.PlayerChangeInfo;
@@ -64,14 +62,14 @@ public class CleanCommand implements CommandHandler {
if (all) { if (all) {
if (doItems) { if (doItems) {
for (GameItem item : inv.getItems().values()) { for (var entry : inv.getItems().int2IntEntrySet()) {
removeMap.add(item.getItemId(), item.getCount()); removeMap.add(entry.getIntKey(), entry.getIntValue());
} }
} }
if (doResources) { if (doResources) {
for (GameResource res : inv.getResources().values()) { for (var entry : inv.getResources().int2IntEntrySet()) {
removeMap.add(res.getResourceId(), res.getCount()); removeMap.add(entry.getIntKey(), entry.getIntValue());
} }
} }
} else { } else {
@@ -212,6 +212,14 @@ public final class DatabaseManager {
update(obj, uid, field2, value2); update(obj, uid, field2, value2);
} }
public void updateUnset(Object obj, int uid, String field) {
var opt = new UpdateOptions().upsert(false);
getDatastore().find(obj.getClass())
.filter(Filters.eq("_id", uid))
.update(opt, UpdateOperators.unset(field));
}
public void updateNested(Object obj, int uid, String filter, int filterId, String field, Object item) { public void updateNested(Object obj, int uid, String filter, int filterId, String field, Object item) {
var opt = new UpdateOptions().upsert(false); var opt = new UpdateOptions().upsert(false);
@@ -12,6 +12,7 @@ import emu.nebula.proto.Public.Item;
import emu.nebula.util.Utils; import emu.nebula.util.Utils;
import lombok.Getter; import lombok.Getter;
@Deprecated
@Getter @Getter
@Entity(value = "items", useDiscriminator = false) @Entity(value = "items", useDiscriminator = false)
public class GameItem implements GameDatabaseObject { public class GameItem implements GameDatabaseObject {
@@ -12,6 +12,7 @@ import emu.nebula.proto.Public.Res;
import emu.nebula.util.Utils; import emu.nebula.util.Utils;
import lombok.Getter; import lombok.Getter;
@Deprecated
@Getter @Getter
@Entity(value = "resources", useDiscriminator = false) @Entity(value = "resources", useDiscriminator = false)
public class GameResource implements GameDatabaseObject { public class GameResource implements GameDatabaseObject {
@@ -2,6 +2,8 @@ package emu.nebula.game.inventory;
import java.util.List; import java.util.List;
import com.mongodb.client.model.Filters;
import dev.morphia.annotations.Entity; import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id; import dev.morphia.annotations.Id;
import emu.nebula.GameConstants; import emu.nebula.GameConstants;
@@ -20,12 +22,11 @@ import emu.nebula.proto.Public.Item;
import emu.nebula.proto.Public.Res; import emu.nebula.proto.Public.Res;
import emu.nebula.proto.Public.Title; import emu.nebula.proto.Public.Title;
import emu.nebula.proto.Public.UI32; import emu.nebula.proto.Public.UI32;
import emu.nebula.util.Utils;
import emu.nebula.util.ints.String2IntMap; import emu.nebula.util.ints.String2IntMap;
import emu.nebula.game.achievement.AchievementCondition; import emu.nebula.game.achievement.AchievementCondition;
import emu.nebula.game.player.Player; import emu.nebula.game.player.Player;
import emu.nebula.game.player.PlayerChangeInfo; import emu.nebula.game.player.PlayerChangeInfo;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntCollection; import it.unimi.dsi.fastutil.ints.IntCollection;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSet;
@@ -37,6 +38,10 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
@Id @Id
private int uid; private int uid;
// Items/resources
private ItemParamMap items;
private ItemParamMap resources;
// Database persistent data // Database persistent data
private IntSet extraSkins; private IntSet extraSkins;
private IntSet headIcons; private IntSet headIcons;
@@ -47,13 +52,9 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
private ItemParamMap shopBuyCount; private ItemParamMap shopBuyCount;
private String2IntMap mallBuyCount; private String2IntMap mallBuyCount;
// Items/resources @Deprecated
private transient Int2ObjectMap<GameResource> resources;
private transient Int2ObjectMap<GameItem> items;
public Inventory() { public Inventory() {
this.resources = new Int2ObjectOpenHashMap<>(); // Morphia only
this.items = new Int2ObjectOpenHashMap<>();
} }
public Inventory(Player player) { public Inventory(Player player) {
@@ -62,6 +63,9 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
this.uid = player.getUid(); this.uid = player.getUid();
// Setup // Setup
this.resources = new ItemParamMap();
this.items = new ItemParamMap();
this.extraSkins = new IntOpenHashSet(); this.extraSkins = new IntOpenHashSet();
this.headIcons = new IntOpenHashSet(); this.headIcons = new IntOpenHashSet();
this.titles = new IntOpenHashSet(); this.titles = new IntOpenHashSet();
@@ -266,15 +270,13 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
// Resources // Resources
public synchronized int getResourceCount(int id) { public synchronized int getResourceCount(int id) {
var res = this.resources.get(id); return this.resources.get(id);
return res != null ? res.getCount() : 0;
} }
// Items // Items
public synchronized int getItemCount(int id) { public synchronized int getItemCount(int id) {
var item = this.getItems().get(id); return this.getItems().get(id);
return item != null ? item.getCount() : 0;
} }
// Add/Remove items // Add/Remove items
@@ -306,35 +308,37 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
// Add item // Add item
switch (data.getItemType()) { switch (data.getItemType()) {
case Res -> { case Res -> {
var res = this.resources.get(id); int oldAmount = this.resources.get(id);
int newAmount = oldAmount;
int diff = 0; int diff = 0;
if (amount > 0) { if (amount > 0) {
// Add resource // Add resource
if (res == null) { newAmount = Utils.safeAdd(oldAmount, amount);
res = new GameResource(this.getPlayer(), id, amount); diff = newAmount - oldAmount;
this.resources.put(res.getResourceId(), res);
diff = amount;
} else {
diff = res.add(amount);
}
res.save(); // Set
this.resources.put(id, newAmount);
} else { } else {
// Remove resource // Remove resource
if (res == null) { newAmount = Utils.safeSubtract(oldAmount, Math.abs(amount));
break; diff = newAmount - oldAmount;
}
diff = res.add(amount); // Set
res.save(); if (newAmount > 0) {
this.resources.put(id, newAmount);
if (res.getCount() < 0) { } else {
this.resources.remove(id); this.resources.remove(id);
} }
} }
// Update in database
if (newAmount > 0) {
Nebula.getGameDatabase().update(this, this.getPlayerUid(), "resources." + id, newAmount);
} else {
Nebula.getGameDatabase().updateUnset(this, this.getPlayerUid(), "resources." + id);
}
if (diff != 0) { if (diff != 0) {
var proto = Res.newInstance() var proto = Res.newInstance()
.setTid(id) .setTid(id)
@@ -365,35 +369,37 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
} }
// Get item // Get item
var item = this.items.get(id); int oldAmount = this.items.get(id);
int newAmount = oldAmount;
int diff = 0; int diff = 0;
if (amount > 0) { if (amount > 0) {
// Add item // Add resource
if (item == null) { newAmount = Utils.safeAdd(oldAmount, amount);
item = new GameItem(this.getPlayer(), id, amount); diff = newAmount - oldAmount;
this.items.put(item.getItemId(), item);
diff = amount;
} else {
diff = item.add(amount);
}
item.save(); // Set
this.items.put(id, newAmount);
} else { } else {
// Remove resource // Remove resource
if (item == null) { newAmount = Utils.safeSubtract(oldAmount, Math.abs(amount));
break; diff = newAmount - oldAmount;
}
diff = item.add(amount); // Set
item.save(); if (newAmount > 0) {
this.items.put(id, newAmount);
if (item.getCount() < 0) { } else {
this.resources.remove(id); this.items.remove(id);
} }
} }
// Update in database
if (newAmount > 0) {
Nebula.getGameDatabase().update(this, this.getPlayerUid(), "items." + id, newAmount);
} else {
Nebula.getGameDatabase().updateUnset(this, this.getPlayerUid(), "items." + id);
}
if (diff != 0) { if (diff != 0) {
var proto = Item.newInstance() var proto = Item.newInstance()
.setTid(id) .setTid(id)
@@ -893,25 +899,55 @@ public class Inventory extends PlayerManager implements GameDatabaseObject {
// Database // Database
public void loadFromDatabase() { @SuppressWarnings("deprecation")
public void migrateFromDatabase() {
var db = Nebula.getGameDatabase(); var db = Nebula.getGameDatabase();
boolean save = false;
db.getObjects(GameItem.class, "playerUid", getPlayerUid()).forEach(item -> { // Check if we need to handle inventory migration
// Get data if (this.items == null) {
var data = GameData.getItemDataTable().get(item.getItemId()); this.items = new ItemParamMap();
if (data == null) return;
// Add // Get inventory items from database
this.items.put(item.getItemId(), item); 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.getCount());
});
// Delete all inventory items
db.getDatastore().getCollection(GameItem.class).deleteMany(Filters.eq("playerUid", uid));
// Set to save inventory to database
save = true;
}
db.getObjects(GameResource.class, "playerUid", getPlayerUid()).forEach(res -> { if (this.resources == null) {
// Get data this.resources = new ItemParamMap();
var data = GameData.getItemDataTable().get(res.getResourceId());
if (data == null) return;
// Add // Get inventory resources from database
this.resources.put(res.getResourceId(), res); 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.getCount());
});
// Delete all inventory resources
db.getDatastore().getCollection(GameResource.class).deleteMany(Filters.eq("playerUid", uid));
// Set to save inventory to database
save = true;
}
// Update in database
if (save) {
this.save();
}
} }
} }
@@ -43,8 +43,10 @@ import emu.nebula.proto.Public.CharShow;
import emu.nebula.proto.Public.Energy; import emu.nebula.proto.Public.Energy;
import emu.nebula.proto.Public.Friend; import emu.nebula.proto.Public.Friend;
import emu.nebula.proto.Public.HonorInfo; import emu.nebula.proto.Public.HonorInfo;
import emu.nebula.proto.Public.Item;
import emu.nebula.proto.Public.NewbieInfo; import emu.nebula.proto.Public.NewbieInfo;
import emu.nebula.proto.Public.QuestType; import emu.nebula.proto.Public.QuestType;
import emu.nebula.proto.Public.Res;
import emu.nebula.proto.Public.WorldClass; import emu.nebula.proto.Public.WorldClass;
import emu.nebula.proto.Public.WorldClassRewardState; import emu.nebula.proto.Public.WorldClassRewardState;
import emu.nebula.util.Utils; import emu.nebula.util.Utils;
@@ -789,7 +791,7 @@ public class Player implements GameDatabaseObject {
if (this.inventory == null) { if (this.inventory == null) {
this.inventory = this.loadManagerFromDatabase(Inventory.class); this.inventory = this.loadManagerFromDatabase(Inventory.class);
} }
this.getInventory().loadFromDatabase(); this.getInventory().migrateFromDatabase();
// Load referenced classes from the database // Load referenced classes from the database
this.formations = this.loadManagerFromDatabase(FormationManager.class); this.formations = this.loadManagerFromDatabase(FormationManager.class);
@@ -952,12 +954,20 @@ public class Player implements GameDatabaseObject {
proto.addDiscs(disc.toProto()); proto.addDiscs(disc.toProto());
} }
for (var item : getInventory().getItems().values()) { for (var item : getInventory().getItems().int2IntEntrySet()) {
proto.addItems(item.toProto()); var info = Item.newInstance()
.setTid(item.getIntKey())
.setQty(item.getIntValue());
proto.addItems(info);
} }
for (var res : getInventory().getResources().values()) { for (var res : getInventory().getResources().int2IntEntrySet()) {
proto.addRes(res.toProto()); var info = Res.newInstance()
.setTid(res.getIntKey())
.setQty(res.getIntValue());
proto.addRes(info);
} }
// Formations // Formations
@@ -147,6 +147,7 @@ public class PlayerModule extends GameContextModule {
* @param uid * @param uid
* @return * @return
*/ */
@SuppressWarnings("deprecation")
public synchronized boolean deletePlayer(int uid) { public synchronized boolean deletePlayer(int uid) {
// Make sure player is not online when we are deleting the player // Make sure player is not online when we are deleting the player
Player player = this.getCachedPlayerByUid(uid); Player player = this.getCachedPlayerByUid(uid);