diff --git a/src/main/java/client/Character.java b/src/main/java/client/Character.java index f5323a6152..54cb3999d1 100644 --- a/src/main/java/client/Character.java +++ b/src/main/java/client/Character.java @@ -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 equipped) { + public static Character loadCharacterViewFromDB(ResultSet rs, List 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 : ItemFactory.INVENTORY.loadItems(ret.id, !channelserver)) { + for (Pair 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 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 diff --git a/src/main/java/client/MonsterBook.java b/src/main/java/client/MonsterBook.java index 3123c9eea5..2faf95fbfb 100644 --- a/src/main/java/client/MonsterBook.java +++ b/src/main/java/client/MonsterBook.java @@ -1,190 +1,106 @@ -/* - This file is part of the OdinMS Maple Story Server - Copyright (C) 2008 Patrick Huy - Matthias Butz - Jan Christian Meyer - - 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 . -*/ 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 cards = new LinkedHashMap<>(); - private final Lock lock = new ReentrantLock(); +// TODO: add tests +@ThreadSafe +public class MonsterBook { + private final Map 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 monsterCards) { + this.cards = monsterCards.stream() + .collect(Collectors.toMap(MonsterCard::cardId, Function.identity())); + } - Integer qty; - lock.lock(); - try { - qty = cards.get(cardid); + public synchronized List 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 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 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(); } diff --git a/src/main/java/database/monsterbook/MonsterCard.java b/src/main/java/database/monsterbook/MonsterCard.java index 0f52bb8898..495432c979 100644 --- a/src/main/java/database/monsterbook/MonsterCard.java +++ b/src/main/java/database/monsterbook/MonsterCard.java @@ -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; + } } diff --git a/src/main/java/net/server/Server.java b/src/main/java/net/server/Server.java index eebae10c77..348a90f4f4 100644 --- a/src/main/java/net/server/Server.java +++ b/src/main/java/net/server/Server.java @@ -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> loadAccountCharlist(int accountId, int visibleWorlds) { List worlds = this.getWorlds(); if (worlds.size() > visibleWorlds) { @@ -1558,11 +1517,11 @@ public class Server { return worldChrs; } - private static Pair>> loadAccountCharactersViewFromDb(int accId, int wlen) { - short characterCount = 0; - List> wchars = new ArrayList<>(wlen); - for (int i = 0; i < wlen; i++) { - wchars.add(i, new LinkedList<>()); + private static Pair>> loadAccountCharactersViewFromDb(int accId, int worlds) { + short chrCount = 0; + List> worldChrs = new ArrayList<>(worlds); + for (int i = 0; i < worlds; i++) { + worldChrs.add(i, new LinkedList<>()); } List 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() { diff --git a/src/main/java/net/server/world/World.java b/src/main/java/net/server/world/World.java index 2bdf1b3ada..d16ce148b5 100644 --- a/src/main/java/net/server/world/World.java +++ b/src/main/java/net/server/world/World.java @@ -451,18 +451,6 @@ public class World { } } - public void clearAccountCharacterView(Integer accountId) { - accountCharsLock.lock(); - try { - SortedMap accChars = accountChars.remove(accountId); - if (accChars != null) { - accChars.clear(); - } - } finally { - accountCharsLock.unlock(); - } - } - public void loadAccountStorage(Integer accountId) { if (getAccountStorage(accountId) == null) { registerAccountStorage(accountId); diff --git a/src/main/java/tools/PacketCreator.java b/src/main/java/tools/PacketCreator.java index cfa759a3e4..033edab0b6 100644 --- a/src/main/java/tools/PacketCreator.java +++ b/src/main/java/tools/PacketCreator.java @@ -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 cards = chr.getMonsterBook().getCards(); + List cards = chr.getMonsterBook().getCards(); p.writeShort(cards.size()); - for (Entry 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); diff --git a/src/test/java/database/monsterbook/MonsterCardTest.java b/src/test/java/database/monsterbook/MonsterCardTest.java index 1b39726f3d..895221f471 100644 --- a/src/test/java/database/monsterbook/MonsterCardTest.java +++ b/src/test/java/database/monsterbook/MonsterCardTest.java @@ -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; }