Rewrite MonsterBook, touch up chr loading

Temporarily disabled loading monster cards from db
This commit is contained in:
P0nk
2023-07-25 21:27:10 +02:00
parent b3ec325e95
commit f6aa8ceba6
7 changed files with 165 additions and 259 deletions

View File

@@ -1901,8 +1901,8 @@ public class Character extends AbstractCharacterObject {
ii.getItemEffect(itemId).applyTo(this);
}
if (itemId / 10000 == 238) {
this.getMonsterBook().addCard(client, itemId);
if (ItemId.isMonsterCard(itemId)) {
monsterbook.addCard(itemId, client);
}
return true;
}
@@ -6743,7 +6743,7 @@ public class Character extends AbstractCharacterObject {
}
}
public static Character loadCharacterEntryFromDB(ResultSet rs, List<Item> equipped) {
public static Character loadCharacterViewFromDB(ResultSet rs, List<Item> equipped) {
Character ret = new Character();
try {
@@ -6794,7 +6794,7 @@ public class Character extends AbstractCharacterObject {
return ret;
}
public Character generateCharacterEntry() {
public Character createCharacterView() {
Character ret = new Character();
ret.accountid = this.getAccountID();
@@ -6854,10 +6854,16 @@ public class Character extends AbstractCharacterObject {
updateRemainingSp(remainingSp, GameConstants.getSkillBook(job.getId()));
}
public static Character loadCharFromDB(final int charid, Client client, boolean channelserver) throws SQLException {
@Deprecated
public static Character loadCharFromDB(int chrId, Client client, boolean channelServer) throws SQLException {
return loadCharFromDB(chrId, client, channelServer, new MonsterBook(Collections.emptyList()));
}
public static Character loadCharFromDB(final int chrId, Client client, boolean channelServer,
MonsterBook monsterBook) throws SQLException {
Character ret = new Character();
ret.client = client;
ret.id = charid;
ret.id = chrId;
try (Connection con = DatabaseConnection.getConnection()) {
final int mountexp;
@@ -6867,7 +6873,7 @@ public class Character extends AbstractCharacterObject {
// Character info
try (PreparedStatement ps = con.prepareStatement("SELECT * FROM characters WHERE id = ?")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
@@ -6925,8 +6931,7 @@ public class Character extends AbstractCharacterObject {
ret.allianceRank = rs.getInt("allianceRank");
ret.familyId = rs.getInt("familyId");
ret.bookCover = rs.getInt("monsterbookcover");
ret.monsterbook = new MonsterBook();
ret.monsterbook.loadCards(charid);
ret.monsterbook = monsterBook;
ret.vanquisherStage = rs.getInt("vanquisherStage");
ret.ariantPoints = rs.getInt("ariantPoints");
ret.dojoPoints = rs.getInt("dojoPoints");
@@ -6946,7 +6951,7 @@ public class Character extends AbstractCharacterObject {
ret.getInventory(InventoryType.ETC).setSlotLimit(rs.getByte("etcslots"));
short sandboxCheck = 0x0;
for (Pair<Item, InventoryType> item : ItemFactory.INVENTORY.loadItems(ret.id, !channelserver)) {
for (Pair<Item, InventoryType> item : ItemFactory.INVENTORY.loadItems(ret.id, !channelServer)) {
sandboxCheck |= item.getLeft().getFlag();
ret.getInventory(item.getRight()).addItemFromDB(item.getLeft());
@@ -6993,7 +6998,7 @@ public class Character extends AbstractCharacterObject {
// Items excluded from pet loot
try (PreparedStatement psPet = con.prepareStatement("SELECT petid FROM inventoryitems WHERE characterid = ? AND petid > -1")) {
psPet.setInt(1, charid);
psPet.setInt(1, chrId);
try (ResultSet rsPet = psPet.executeQuery()) {
while (rsPet.next()) {
@@ -7016,7 +7021,7 @@ public class Character extends AbstractCharacterObject {
ret.commitExcludedItems();
if (channelserver) {
if (channelServer) {
MapManager mapManager = client.getChannelServer().getMapFactory();
ret.map = mapManager.getMap(ret.mapid);
@@ -7054,7 +7059,7 @@ public class Character extends AbstractCharacterObject {
// Teleport rocks
try (PreparedStatement ps = con.prepareStatement("SELECT mapid,vip FROM trocklocations WHERE characterid = ? LIMIT 15")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rs = ps.executeQuery()) {
byte vip = 0;
@@ -7125,7 +7130,7 @@ public class Character extends AbstractCharacterObject {
// Blessing of the Fairy
try (PreparedStatement ps = con.prepareStatement("SELECT name, level FROM characters WHERE accountid = ? AND id != ? ORDER BY level DESC limit 1")) {
ps.setInt(1, ret.accountid);
ps.setInt(2, charid);
ps.setInt(2, chrId);
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
@@ -7135,12 +7140,12 @@ public class Character extends AbstractCharacterObject {
}
}
if (channelserver) {
if (channelServer) {
final Map<Integer, QuestStatus> loadedQuestStatus = new LinkedHashMap<>();
// Quest status
try (PreparedStatement ps = con.prepareStatement("SELECT * FROM queststatus WHERE characterid = ?")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
@@ -7167,7 +7172,7 @@ public class Character extends AbstractCharacterObject {
// Quest progress
// opportunity for improvement on questprogress/medalmaps calls to DB
try (PreparedStatement ps = con.prepareStatement("SELECT * FROM questprogress WHERE characterid = ?")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rsProgress = ps.executeQuery()) {
while (rsProgress.next()) {
QuestStatus status = loadedQuestStatus.get(rsProgress.getInt("queststatusid"));
@@ -7180,7 +7185,7 @@ public class Character extends AbstractCharacterObject {
// Medal map visit progress
try (PreparedStatement ps = con.prepareStatement("SELECT * FROM medalmaps WHERE characterid = ?")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rsMedalMaps = ps.executeQuery()) {
while (rsMedalMaps.next()) {
QuestStatus status = loadedQuestStatus.get(rsMedalMaps.getInt("queststatusid"));
@@ -7195,7 +7200,7 @@ public class Character extends AbstractCharacterObject {
// Skills
try (PreparedStatement ps = con.prepareStatement("SELECT skillid,skilllevel,masterlevel,expiration FROM skills WHERE characterid = ?")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
@@ -7265,7 +7270,7 @@ public class Character extends AbstractCharacterObject {
// Skill macros
try (PreparedStatement ps = con.prepareStatement("SELECT * FROM skillmacros WHERE characterid = ?")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
@@ -7278,7 +7283,7 @@ public class Character extends AbstractCharacterObject {
// Key config
try (PreparedStatement ps = con.prepareStatement("SELECT `key`,`type`,`action` FROM keymap WHERE characterid = ?")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
@@ -7292,7 +7297,7 @@ public class Character extends AbstractCharacterObject {
// Saved locations
try (PreparedStatement ps = con.prepareStatement("SELECT `locationtype`,`map`,`portal` FROM savedlocations WHERE characterid = ?")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
@@ -7303,7 +7308,7 @@ public class Character extends AbstractCharacterObject {
// Fame history
try (PreparedStatement ps = con.prepareStatement("SELECT `characterid_to`,`when` FROM famelog WHERE characterid = ? AND DATEDIFF(NOW(),`when`) < 30")) {
ps.setInt(1, charid);
ps.setInt(1, chrId);
try (ResultSet rs = ps.executeQuery()) {
ret.lastfametime = 0;
@@ -7315,7 +7320,7 @@ public class Character extends AbstractCharacterObject {
}
}
ret.buddylist.loadFromDb(charid);
ret.buddylist.loadFromDb(chrId);
ret.storage = wserv.getAccountStorage(ret.accountid);
/* Double-check storage incase player is first time on server

View File

@@ -1,190 +1,106 @@
/*
This file is part of the OdinMS Maple Story Server
Copyright (C) 2008 Patrick Huy <patrick.huy@frz.cc>
Matthias Butz <matze@odinms.de>
Jan Christian Meyer <vimes@odinms.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation version 3 as published by
the Free Software Foundation. You may not use, modify or distribute
this program under any other version of the GNU Affero General Public
License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package client;
import tools.DatabaseConnection;
import database.monsterbook.MonsterCard;
import net.jcip.annotations.ThreadSafe;
import tools.PacketCreator;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public final class MonsterBook {
private int specialCard = 0;
private int normalCard = 0;
private int bookLevel = 1;
private final Map<Integer, Integer> cards = new LinkedHashMap<>();
private final Lock lock = new ReentrantLock();
// TODO: add tests
@ThreadSafe
public class MonsterBook {
private final Map<Integer, MonsterCard> cards;
private int bookLevel;
public void addCard(final Client c, final int cardid) {
c.getPlayer().getMap().broadcastMessage(c.getPlayer(), PacketCreator.showForeignCardEffect(c.getPlayer().getId()), false);
public MonsterBook(List<MonsterCard> monsterCards) {
this.cards = monsterCards.stream()
.collect(Collectors.toMap(MonsterCard::cardId, Function.identity()));
}
Integer qty;
lock.lock();
try {
qty = cards.get(cardid);
public synchronized List<MonsterCard> getCards() {
return new ArrayList<>(cards.values());
}
if (qty != null) {
if (qty < 5) {
cards.put(cardid, qty + 1);
}
} else {
cards.put(cardid, 1);
qty = 0;
if (cardid / 1000 >= 2388) {
specialCard++;
} else {
normalCard++;
}
}
} finally {
lock.unlock();
public synchronized void addCard(int cardId, Client client) {
var monsterCard = cards.get(cardId);
if (monsterCard != null && monsterCard.isMaxLevel()) {
client.sendPacket(PacketCreator.addMonsterCardAlreadyFull());
return;
}
if (qty < 5) {
if (qty == 0) { // leveling system only accounts unique cards
calculateLevel();
}
c.sendPacket(PacketCreator.addCard(false, cardid, qty + 1));
c.sendPacket(PacketCreator.showGainCard());
boolean isNewCard = monsterCard == null;
final MonsterCard card;
if (isNewCard) {
card = new MonsterCard(cardId, (byte) 1);
cards.put(cardId, card);
calculateAndSetLevel();
} else {
c.sendPacket(PacketCreator.addCard(true, cardid, 5));
}
}
private void calculateLevel() {
lock.lock();
try {
int collectionExp = (normalCard + specialCard);
int level = 0, expToNextlevel = 1;
do {
level++;
expToNextlevel += level * 10;
} while (collectionExp >= expToNextlevel);
bookLevel = level; // thanks IxianMace for noticing book level differing between book UI and character info UI
} finally {
lock.unlock();
}
}
public int getBookLevel() {
lock.lock();
try {
return bookLevel;
} finally {
lock.unlock();
}
}
public Map<Integer, Integer> getCards() {
lock.lock();
try {
return Collections.unmodifiableMap(cards);
} finally {
lock.unlock();
}
}
public int getTotalCards() {
lock.lock();
try {
return specialCard + normalCard;
} finally {
lock.unlock();
}
}
public int getNormalCard() {
lock.lock();
try {
return normalCard;
} finally {
lock.unlock();
}
}
public int getSpecialCard() {
lock.lock();
try {
return specialCard;
} finally {
lock.unlock();
}
}
public void loadCards(final int charid) throws SQLException {
lock.lock();
try (Connection con = DatabaseConnection.getConnection();
PreparedStatement ps = con.prepareStatement("SELECT cardid, level FROM monsterbook WHERE charid = ? ORDER BY cardid ASC")) {
ps.setInt(1, charid);
try (ResultSet rs = ps.executeQuery()) {
int cardid;
int level;
while (rs.next()) {
cardid = rs.getInt("cardid");
level = rs.getInt("level");
if (cardid / 1000 >= 2388) {
specialCard++;
} else {
normalCard++;
}
cards.put(cardid, level);
}
}
} finally {
lock.unlock();
card = new MonsterCard(cardId, (byte) (monsterCard.level() + 1));
cards.put(cardId, card);
}
calculateLevel();
var chr = client.getPlayer();
chr.sendPacket(PacketCreator.addMonsterCard(card));
chr.sendPacket(PacketCreator.showMonsterCardEffect());
chr.getMap().broadcastMessage(chr, PacketCreator.showForeignMonsterCardEffect(chr.getId()), false);
}
public void saveCards(Connection con, int chrId) throws SQLException {
private synchronized void calculateAndSetLevel() {
int collectionExp = getTotalCards();
int level = 0;
int expToNextLevel = 1;
do {
level++;
expToNextLevel += level * 10;
} while (collectionExp >= expToNextLevel);
this.bookLevel = level;
}
public synchronized int getBookLevel() {
return bookLevel;
}
public synchronized int getNormalCards() {
return (int) cards.values().stream()
.filter(Predicate.not(MonsterCard::isSpecial))
.count();
}
public synchronized int getSpecialCards() {
return (int) cards.values().stream()
.filter(MonsterCard::isSpecial)
.count();
}
public synchronized int getTotalCards() {
return cards.size();
}
public synchronized void saveCards(Connection con, int chrId) throws SQLException {
final String query = """
INSERT INTO monsterbook (charid, cardid, level)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE level = ?;
""";
try (final PreparedStatement ps = con.prepareStatement(query)) {
for (Map.Entry<Integer, Integer> cardAndLevel : cards.entrySet()) {
final int card = cardAndLevel.getKey();
final int level = cardAndLevel.getValue();
for (MonsterCard card : cards.values()) {
// insert
ps.setInt(1, chrId);
ps.setInt(2, card);
ps.setInt(3, level);
ps.setInt(2, card.cardId());
ps.setInt(3, card.level());
// update
ps.setInt(4, level);
ps.setInt(4, card.level());
ps.addBatch();
}

View File

@@ -1,12 +1,15 @@
package database.monsterbook;
import constants.id.ItemId;
public record MonsterCard(int cardId, byte level) {
private static final int MAX_LEVEL = 5;
public MonsterCard {
if (cardId / 10_000 != 238) {
if (!ItemId.isMonsterCard(cardId)) {
throw new IllegalArgumentException("Invalid monster card id: %d".formatted(cardId));
}
if (level < 0 || level > 5) {
if (level < 0 || level > MAX_LEVEL) {
throw new IllegalArgumentException("Invalid monster card level: %d".formatted(level));
}
}
@@ -14,4 +17,8 @@ public record MonsterCard(int cardId, byte level) {
public boolean isSpecial() {
return cardId / 1000 == 2388;
}
public boolean isMaxLevel() {
return level == MAX_LEVEL;
}
}

View File

@@ -1432,7 +1432,7 @@ public class Server {
}
public void updateCharacterEntry(Character chr) {
Character chrView = chr.generateCharacterEntry();
Character chrView = chr.createCharacterView();
lgnWLock.lock();
try {
@@ -1457,7 +1457,7 @@ public class Server {
worldChars.put(chrid, world);
Character chrView = chr.generateCharacterEntry();
Character chrView = chr.createCharacterView();
World wserv = this.getWorld(chrView.getWorld());
if (wserv != null) {
@@ -1488,47 +1488,6 @@ public class Server {
}
}
public void transferWorldCharacterEntry(Character chr, Integer toWorld) { // used before setting the new worldid on the character object
lgnWLock.lock();
try {
Integer chrid = chr.getId(), accountid = chr.getAccountID(), world = worldChars.get(chr.getId());
if (world != null) {
World wserv = this.getWorld(world);
if (wserv != null) {
wserv.unregisterAccountCharacterView(accountid, chrid);
}
}
worldChars.put(chrid, toWorld);
Character chrView = chr.generateCharacterEntry();
World wserv = this.getWorld(toWorld);
if (wserv != null) {
wserv.registerAccountCharacterView(chrView.getAccountID(), chrView);
}
} finally {
lgnWLock.unlock();
}
}
/*
public void deleteAccountEntry(Integer accountid) { is this even a thing?
lgnWLock.lock();
try {
accountCharacterCount.remove(accountid);
accountChars.remove(accountid);
} finally {
lgnWLock.unlock();
}
for (World wserv : this.getWorlds()) {
wserv.clearAccountCharacterView(accountid);
wserv.unregisterAccountStorage(accountid);
}
}
*/
public SortedMap<Integer, List<Character>> loadAccountCharlist(int accountId, int visibleWorlds) {
List<World> worlds = this.getWorlds();
if (worlds.size() > visibleWorlds) {
@@ -1558,11 +1517,11 @@ public class Server {
return worldChrs;
}
private static Pair<Short, List<List<Character>>> loadAccountCharactersViewFromDb(int accId, int wlen) {
short characterCount = 0;
List<List<Character>> wchars = new ArrayList<>(wlen);
for (int i = 0; i < wlen; i++) {
wchars.add(i, new LinkedList<>());
private static Pair<Short, List<List<Character>>> loadAccountCharactersViewFromDb(int accId, int worlds) {
short chrCount = 0;
List<List<Character>> worldChrs = new ArrayList<>(worlds);
for (int i = 0; i < worlds; i++) {
worldChrs.add(i, new LinkedList<>());
}
List<Character> chars = new LinkedList<>();
@@ -1587,32 +1546,32 @@ public class Server {
ps.setInt(1, accId);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
characterCount++;
chrCount++;
int cworld = rs.getByte("world");
if (cworld >= wlen) {
if (cworld >= worlds) {
continue;
}
if (cworld > curWorld) {
wchars.add(curWorld, chars);
worldChrs.add(curWorld, chars);
curWorld = cworld;
chars = new LinkedList<>();
}
Integer cid = rs.getInt("id");
chars.add(Character.loadCharacterEntryFromDB(rs, accPlayerEquips.get(cid)));
chars.add(Character.loadCharacterViewFromDB(rs, accPlayerEquips.get(cid)));
}
}
}
wchars.add(curWorld, chars);
worldChrs.add(curWorld, chars);
} catch (SQLException sqle) {
sqle.printStackTrace();
}
return new Pair<>(characterCount, wchars);
return new Pair<>(chrCount, worldChrs);
}
public void loadAllAccountsCharactersView() {

View File

@@ -451,18 +451,6 @@ public class World {
}
}
public void clearAccountCharacterView(Integer accountId) {
accountCharsLock.lock();
try {
SortedMap<Integer, Character> accChars = accountChars.remove(accountId);
if (accChars != null) {
accChars.clear();
}
} finally {
accountCharsLock.unlock();
}
}
public void loadAccountStorage(Integer accountId) {
if (getAccountStorage(accountId) == null) {
registerAccountStorage(accountId);

View File

@@ -40,6 +40,7 @@ import constants.inventory.ItemConstants;
import constants.skills.Buccaneer;
import constants.skills.Corsair;
import constants.skills.ThunderBreaker;
import database.monsterbook.MonsterCard;
import net.encryption.InitializationVector;
import net.opcodes.SendOpcode;
import net.packet.ByteBufOutPacket;
@@ -526,12 +527,12 @@ public class PacketCreator {
private static void addMonsterBookInfo(OutPacket p, Character chr) {
p.writeInt(chr.getMonsterBookCover()); // cover
p.writeByte(0);
Map<Integer, Integer> cards = chr.getMonsterBook().getCards();
List<MonsterCard> cards = chr.getMonsterBook().getCards();
p.writeShort(cards.size());
for (Entry<Integer, Integer> all : cards.entrySet()) {
p.writeShort(all.getKey() % 10000); // Id
p.writeByte(all.getValue()); // Level
}
cards.forEach(card -> {
p.writeShort(card.cardId() % 10000);
p.writeByte(card.level());
});
}
public static Packet sendGuestTOS() {
@@ -2734,8 +2735,8 @@ public class PacketCreator {
MonsterBook book = chr.getMonsterBook();
p.writeInt(book.getBookLevel());
p.writeInt(book.getNormalCard());
p.writeInt(book.getSpecialCard());
p.writeInt(book.getNormalCards());
p.writeInt(book.getSpecialCards());
p.writeInt(book.getTotalCards());
p.writeInt(chr.getMonsterBookCover() > 0 ? ItemInformationProvider.getInstance().getCardMobId(chr.getMonsterBookCover()) : 0);
Item medal = chr.getInventory(InventoryType.EQUIPPED).getItem((short) -49);
@@ -5978,13 +5979,29 @@ public class PacketCreator {
return p;
}
public static Packet showGainCard() {
public static Packet addMonsterCard(MonsterCard monsterCard) {
OutPacket p = OutPacket.create(SendOpcode.MONSTER_BOOK_SET_CARD);
p.writeBool(true);
p.writeInt(monsterCard.cardId());
p.writeInt(monsterCard.level());
return p;
}
public static Packet addMonsterCardAlreadyFull() {
OutPacket p = OutPacket.create(SendOpcode.MONSTER_BOOK_SET_CARD);
p.writeBool(false);
return p;
}
public static Packet showMonsterCardEffect() {
OutPacket p = OutPacket.create(SendOpcode.SHOW_ITEM_GAIN_INCHAT);
p.writeByte(0x0D);
return p;
}
public static Packet showForeignCardEffect(int id) {
public static Packet showForeignMonsterCardEffect(int id) {
OutPacket p = OutPacket.create(SendOpcode.SHOW_FOREIGN_EFFECT);
p.writeInt(id);
p.writeByte(0x0D);

View File

@@ -38,6 +38,20 @@ class MonsterCardTest {
assertFalse(normalCard.isSpecial());
}
@Test
void notMaxLevel() {
var nonMaxedCard = new MonsterCard(validCardId(), (byte) 4);
assertFalse(nonMaxedCard.isMaxLevel());
}
@Test
void isMaxLevel() {
var maxedCard = new MonsterCard(validCardId(), (byte) 5);
assertTrue(maxedCard.isMaxLevel());
}
private int validCardId() {
return 2380000;
}