diff --git a/README.md b/README.md
index 870e18d3a7..e1f5ce03bf 100644
--- a/README.md
+++ b/README.md
@@ -211,12 +211,17 @@ To launch the server, you may either:
---
### Getting into the game
-If you ran the admin sql script, there already exists an account in your database with an admin character on it. You don't need to change its GM level. Log in using these credentials:
+If you ran the admin sql script, there already exists an account in the database with an admin character on it (GM level 6).
+
+Log in using these credentials:
* Username: "admin"
* Password: "admin"
* Pin: "0000"
* Pic: "000000"
+Admin characters have "hide" mode enabled by default. This means your character will be translucent on your screen, and completely invisible to others.
+It will also prevent you from controlling mobs (making them stand still). To toggle this mode on and off, type "@hide" in the in-game chat.
+
By default, the server source is set to allow AUTO-REGISTERING. This means that, by simply typing in a "Login ID" and a "Password", you're able to create a new account.
After creating a character, experiment typing in all-chat "@commands".
diff --git a/pom.xml b/pom.xml
index 1a333333a6..c5ac497114 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,6 +32,7 @@
1.0
5.0.1
8.0.30
+ 3.35.0
5.9.0
4.7.0
@@ -59,6 +60,12 @@
mysql-connector-java
${mysql-connector-java.version}
+
+ org.jdbi
+ jdbi3-core
+ ${jdbi-version}
+
+
diff --git a/src/main/java/client/Character.java b/src/main/java/client/Character.java
index 660b02db29..1ba4b60603 100644
--- a/src/main/java/client/Character.java
+++ b/src/main/java/client/Character.java
@@ -97,7 +97,6 @@ import static java.util.concurrent.TimeUnit.*;
public class Character extends AbstractCharacterObject {
private static final Logger log = LoggerFactory.getLogger(Character.class);
- private static final ItemInformationProvider ii = ItemInformationProvider.getInstance();
private static final String LEVEL_200 = "[Congrats] %s has reached Level %d! Congratulate %s on such an amazing achievement!";
private static final String[] BLOCKED_NAMES = {"admin", "owner", "moderator", "intern", "donor", "administrator", "FREDRICK", "help", "helper", "alert", "notice", "maplestory", "fuck", "wizet", "fucking", "negro", "fuk", "fuc", "penis", "pussy", "asshole", "gay",
"nigger", "homo", "suck", "cum", "shit", "shitty", "condom", "security", "official", "rape", "nigga", "sex", "tit", "boner", "orgy", "clit", "asshole", "fatass", "bitch", "support", "gamemaster", "cock", "gaay", "gm",
@@ -717,7 +716,7 @@ public class Character extends AbstractCharacterObject {
int maxbasedamage;
Item weapon_item = getInventory(InventoryType.EQUIPPED).getItem((short) -11);
if (weapon_item != null) {
- maxbasedamage = calculateMaxBaseDamage(watk, ii.getWeaponType(weapon_item.getItemId()));
+ maxbasedamage = calculateMaxBaseDamage(watk, ItemInformationProvider.getInstance().getWeaponType(weapon_item.getItemId()));
} else {
if (job.isA(Job.PIRATE) || job.isA(Job.THUNDERBREAKER1)) {
double weapMulti = 3;
@@ -817,7 +816,7 @@ public class Character extends AbstractCharacterObject {
String medal = "";
final Item medalItem = getInventory(InventoryType.EQUIPPED).getItem((short) -49);
if (medalItem != null) {
- medal = "<" + ii.getName(medalItem.getItemId()) + "> ";
+ medal = "<" + ItemInformationProvider.getInstance().getName(medalItem.getItemId()) + "> ";
}
return medal;
}
@@ -1873,6 +1872,7 @@ public class Character extends AbstractCharacterObject {
public boolean applyConsumeOnPickup(final int itemId) {
if (itemId / 1000000 == 2) {
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
if (ii.isConsumeOnPickup(itemId)) {
if (ItemConstants.isPartyItem(itemId)) {
List partyMembers = this.getPartyMembersOnSameMap();
@@ -1943,6 +1943,7 @@ public class Character extends AbstractCharacterObject {
Item mItem = mapitem.getItem();
boolean hasSpaceInventory = true;
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
if (ItemId.isNxCard(mapitem.getItemId()) || mapitem.getMeso() > 0 || ii.isConsumeOnPickup(mapitem.getItemId()) || (hasSpaceInventory = InventoryManipulator.checkSpace(client, mapitem.getItemId(), mItem.getQuantity(), mItem.getOwner()))) {
int mapId = this.getMapId();
@@ -2061,6 +2062,7 @@ public class Character extends AbstractCharacterObject {
}
public boolean canHoldUniques(List itemids) {
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Integer itemid : itemids) {
if (ii.isPickupRestricted(itemid) && this.haveItem(itemid)) {
return false;
@@ -3740,6 +3742,7 @@ public class Character extends AbstractCharacterObject {
}
public void cancelEffect(int itemId) {
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
cancelEffect(ii.getItemEffect(itemId), false, -1);
}
@@ -6793,6 +6796,7 @@ public class Character extends AbstractCharacterObject {
return;
}
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
StatEffect mse = ii.getItemEffect(couponid);
mse.applyTo(this);
}
@@ -7825,6 +7829,7 @@ public class Character extends AbstractCharacterObject {
if (job.isA(Job.THIEF) || job.isA(Job.BOWMAN) || job.isA(Job.PIRATE) || job.isA(Job.NIGHTWALKER1) || job.isA(Job.WINDARCHER1)) {
Item weapon_item = getInventory(InventoryType.EQUIPPED).getItem((short) -11);
if (weapon_item != null) {
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
WeaponType weapon = ii.getWeaponType(weapon_item.getItemId());
boolean bow = weapon == WeaponType.BOW;
boolean crossbow = weapon == WeaponType.CROSSBOW;
@@ -8792,22 +8797,6 @@ public class Character extends AbstractCharacterObject {
return skillMacros;
}
- public void sendNote(String to, String msg, byte fame) throws SQLException {
- sendNote(to, this.getName(), msg, fame);
- }
-
- public static void sendNote(String to, String from, String msg, byte fame) throws SQLException {
- try (Connection con = DatabaseConnection.getConnection();
- PreparedStatement ps = con.prepareStatement("INSERT INTO notes (`to`, `from`, `message`, `timestamp`, `fame`) VALUES (?, ?, ?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) {
- ps.setString(1, to);
- ps.setString(2, from);
- ps.setString(3, msg);
- ps.setLong(4, Server.getInstance().getCurrentTime());
- ps.setByte(5, fame);
- ps.executeUpdate();
- }
- }
-
public static void setAriantRoomLeader(int room, String charname) {
ariantroomleader[room] = charname;
}
@@ -9219,20 +9208,6 @@ public class Character extends AbstractCharacterObject {
}
}
- public void changeName(String name) {
- FredrickProcessor.removeFredrickReminders(this.getId());
-
- this.name = name;
- try (Connection con = DatabaseConnection.getConnection();
- PreparedStatement ps = con.prepareStatement("UPDATE `characters` SET `name` = ? WHERE `id` = ?")) {
- ps.setString(1, name);
- ps.setInt(2, id);
- ps.executeUpdate();
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
-
public int getDoorSlot() {
if (doorSlot != -1) {
return doorSlot;
@@ -9331,6 +9306,7 @@ public class Character extends AbstractCharacterObject {
return (-1);
}
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
return (sellAllItemsFromPosition(ii, type, it.getPosition()));
} finally {
inv.unlockInventory();
@@ -9413,6 +9389,7 @@ public class Character extends AbstractCharacterObject {
private List getUpgradeableEquipped() {
List list = new LinkedList<>();
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Item item : getInventory(InventoryType.EQUIPPED)) {
if (ii.isUpgradeable(item.getItemId())) {
list.add((Equip) item);
@@ -9525,6 +9502,7 @@ public class Character extends AbstractCharacterObject {
private void standaloneMerge(Map statups, Client c, InventoryType type, short slot, Item item) {
short quantity;
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
if (item == null || (quantity = item.getQuantity()) < 1 || ii.isCash(item.getItemId()) || !ii.isUpgradeable(item.getItemId()) || hasMergeFlag(item)) {
return;
}
@@ -9619,7 +9597,7 @@ public class Character extends AbstractCharacterObject {
String medal = "";
Item medalItem = mapOwner.getInventory(InventoryType.EQUIPPED).getItem((short) -49);
if (medalItem != null) {
- medal = "<" + ii.getName(medalItem.getItemId()) + "> ";
+ medal = "<" + ItemInformationProvider.getInstance().getName(medalItem.getItemId()) + "> ";
}
List strLines = new LinkedList<>();
@@ -9640,21 +9618,6 @@ public class Character extends AbstractCharacterObject {
client.announceHint(msg, length);
}
- public void showNote() {
- try (Connection con = DatabaseConnection.getConnection();
- PreparedStatement ps = con.prepareStatement("SELECT * FROM notes WHERE `to` = ? AND `deleted` = 0", ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
- ps.setString(1, this.getName());
- try (ResultSet rs = ps.executeQuery()) {
- rs.last();
- int count = rs.getRow();
- rs.first();
- sendPacket(PacketCreator.showNotes(rs, count));
- }
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
-
public void silentGiveBuffs(List> buffs) {
for (Pair mbsv : buffs) {
PlayerBuffValueHolder mbsvh = mbsv.getRight();
@@ -10357,6 +10320,7 @@ public class Character extends AbstractCharacterObject {
}
Collection- eqpList = new LinkedHashSet<>();
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Item it : fullList) {
if (!ii.isCash(it.getItemId())) {
eqpList.add(it);
@@ -10372,6 +10336,7 @@ public class Character extends AbstractCharacterObject {
expGain = Integer.MAX_VALUE;
}
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Item item : getUpgradeableEquipList()) {
Equip nEquip = (Equip) item;
String itemName = ii.getName(nEquip.getItemId());
@@ -10387,6 +10352,7 @@ public class Character extends AbstractCharacterObject {
public void showAllEquipFeatures() {
String showMsg = "";
+ ItemInformationProvider ii = ItemInformationProvider.getInstance();
for (Item item : getInventory(InventoryType.EQUIPPED).list()) {
Equip nEquip = (Equip) item;
String itemName = ii.getName(nEquip.getItemId());
diff --git a/src/main/java/client/processor/npc/FredrickProcessor.java b/src/main/java/client/processor/npc/FredrickProcessor.java
index 36efe492de..5e0bd24015 100644
--- a/src/main/java/client/processor/npc/FredrickProcessor.java
+++ b/src/main/java/client/processor/npc/FredrickProcessor.java
@@ -36,12 +36,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.ItemInformationProvider;
import server.maps.HiredMerchant;
+import service.NoteService;
import tools.DatabaseConnection;
import tools.PacketCreator;
import tools.Pair;
import java.sql.*;
-import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -54,6 +54,12 @@ public class FredrickProcessor {
private static final Logger log = LoggerFactory.getLogger(FredrickProcessor.class);
private static final int[] dailyReminders = new int[]{2, 5, 10, 15, 30, 60, 90, Integer.MAX_VALUE};
+ private final NoteService noteService;
+
+ public FredrickProcessor(NoteService noteService) {
+ this.noteService = noteService;
+ }
+
private static byte canRetrieveFromFredrick(Character chr, List> items) {
if (!Inventory.checkSpotsAndOwnership(chr, items)) {
List itemids = new LinkedList<>();
@@ -127,10 +133,6 @@ public class FredrickProcessor {
}
}
- public static void removeFredrickReminders(int cid) {
- removeFredrickReminders(Collections.singletonList(new Pair<>(cid, 0)));
- }
-
private static void removeFredrickReminders(List> expiredCids) {
List expiredCnames = new LinkedList<>();
for (Pair id : expiredCids) {
@@ -153,7 +155,7 @@ public class FredrickProcessor {
}
}
- public static void runFredrickSchedule() {
+ public void runFredrickSchedule() {
try (Connection con = DatabaseConnection.getConnection()) {
List> expiredCids = new LinkedList<>();
List, Integer>> notifCids = new LinkedList<>();
@@ -241,7 +243,7 @@ public class FredrickProcessor {
ps.addBatch();
String msg = fredrickReminderMessage(cid.getRight() - 1);
- Character.sendNote(cid.getLeft().getRight(), "FREDRICK", msg, (byte) 0);
+ noteService.sendNormal(msg, "FREDRICK", cid.getLeft().getRight());
}
ps.executeBatch();
@@ -266,7 +268,7 @@ public class FredrickProcessor {
}
}
- public static void fredrickRetrieveItems(Client c) { // thanks Gustav for pointing out the dupe on Fredrick handling
+ public void fredrickRetrieveItems(Client c) { // thanks Gustav for pointing out the dupe on Fredrick handling
if (c.tryacquireClient()) {
try {
Character chr = c.getPlayer();
diff --git a/src/main/java/database/DaoException.java b/src/main/java/database/DaoException.java
new file mode 100644
index 0000000000..3190d233ce
--- /dev/null
+++ b/src/main/java/database/DaoException.java
@@ -0,0 +1,10 @@
+package database;
+
+import org.jdbi.v3.core.JdbiException;
+
+public class DaoException extends JdbiException {
+
+ public DaoException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/database/note/NoteDao.java b/src/main/java/database/note/NoteDao.java
new file mode 100644
index 0000000000..d429aa249f
--- /dev/null
+++ b/src/main/java/database/note/NoteDao.java
@@ -0,0 +1,89 @@
+package database.note;
+
+import database.DaoException;
+import model.Note;
+import org.jdbi.v3.core.Handle;
+import org.jdbi.v3.core.JdbiException;
+import tools.DatabaseConnection;
+
+import java.util.List;
+import java.util.Optional;
+
+public class NoteDao {
+
+ public void save(Note note) {
+ try (Handle handle = DatabaseConnection.getHandle()) {
+ handle.createUpdate("""
+ INSERT INTO notes (`message`, `from`, `to`, `timestamp`, `fame`, `deleted`)
+ VALUES (?, ?, ?, ?, ?, ?)""")
+ .bind(0, note.message())
+ .bind(1, note.from())
+ .bind(2, note.to())
+ .bind(3, note.timestamp())
+ .bind(4, note.fame())
+ .bind(5, 0)
+ .execute();
+ } catch (JdbiException e) {
+ throw new DaoException("Failed to save note: %s".formatted(note.toString()), e);
+ }
+ }
+
+ public List findAllByTo(String to) {
+ try (Handle handle = DatabaseConnection.getHandle()) {
+ return handle.createQuery("""
+ SELECT *
+ FROM notes
+ WHERE `deleted` = 0
+ AND `to` = ?""")
+ .bind(0, to)
+ .mapTo(Note.class)
+ .list();
+ } catch (JdbiException e) {
+ throw new DaoException("Failed to find notes sent to: %s".formatted(to), e);
+ }
+ }
+
+ public Optional delete(int id) {
+ try (Handle handle = DatabaseConnection.getHandle()) {
+ Optional note = findById(handle, id);
+ if (note.isEmpty()) {
+ return Optional.empty();
+ }
+ deleteById(handle, id);
+
+ return note;
+ } catch (JdbiException e) {
+ throw new DaoException("Failed to delete note with id: %d".formatted(id), e);
+ }
+ }
+
+ private Optional findById(Handle handle, int id) {
+ final Optional note;
+ try {
+ note = handle.createQuery("""
+ SELECT *
+ FROM notes
+ WHERE `deleted` = 0
+ AND `id` = ?""")
+ .bind(0, id)
+ .mapTo(Note.class)
+ .findOne();
+ } catch (JdbiException e) {
+ throw new DaoException("Failed find note with id %s".formatted(id), e);
+ }
+ return note;
+ }
+
+ private void deleteById(Handle handle, int id) {
+ try {
+ handle.createUpdate("""
+ UPDATE notes
+ SET `deleted` = 1
+ WHERE `id` = ?""")
+ .bind(0, id)
+ .execute();
+ } catch (JdbiException e) {
+ throw new DaoException("Failed to delete note with id %d".formatted(id), e);
+ }
+ }
+}
diff --git a/src/main/java/database/note/NoteRowMapper.java b/src/main/java/database/note/NoteRowMapper.java
new file mode 100644
index 0000000000..fd239a5632
--- /dev/null
+++ b/src/main/java/database/note/NoteRowMapper.java
@@ -0,0 +1,22 @@
+package database.note;
+
+import model.Note;
+import org.jdbi.v3.core.mapper.RowMapper;
+import org.jdbi.v3.core.statement.StatementContext;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class NoteRowMapper implements RowMapper {
+
+ @Override
+ public Note map(ResultSet rs, StatementContext ctx) throws SQLException {
+ int id = rs.getInt("id");
+ String message = rs.getString("message");
+ String from = rs.getString("from");
+ String to = rs.getString("to");
+ long timestamp = rs.getLong("timestamp");
+ int fame = rs.getInt("fame");
+ return new Note(id, message, from, to, timestamp, fame);
+ }
+}
diff --git a/src/main/java/model/Note.java b/src/main/java/model/Note.java
new file mode 100644
index 0000000000..9b85643d8d
--- /dev/null
+++ b/src/main/java/model/Note.java
@@ -0,0 +1,21 @@
+package model;
+
+import java.util.Objects;
+
+public record Note(int id, String message, String from, String to, long timestamp, int fame) {
+ private static final int PLACEHOLDER_ID = -1;
+
+ public Note {
+ Objects.requireNonNull(message);
+ Objects.requireNonNull(from);
+ Objects.requireNonNull(to);
+ }
+
+ public static Note createNormal(String message, String from, String to, long timestamp) {
+ return new Note(PLACEHOLDER_ID, message, from, to, timestamp, 0);
+ }
+
+ public static Note createGift(String message, String from, String to, long timestamp) {
+ return new Note(PLACEHOLDER_ID, message, from, to, timestamp, 1);
+ }
+}
diff --git a/src/main/java/net/ChannelDependencies.java b/src/main/java/net/ChannelDependencies.java
new file mode 100644
index 0000000000..a6660f494d
--- /dev/null
+++ b/src/main/java/net/ChannelDependencies.java
@@ -0,0 +1,14 @@
+package net;
+
+import client.processor.npc.FredrickProcessor;
+import service.NoteService;
+
+import java.util.Objects;
+
+public record ChannelDependencies(NoteService noteService, FredrickProcessor fredrickProcessor) {
+
+ public ChannelDependencies {
+ Objects.requireNonNull(noteService);
+ Objects.requireNonNull(fredrickProcessor);
+ }
+}
diff --git a/src/main/java/net/PacketProcessor.java b/src/main/java/net/PacketProcessor.java
index 4c5e2615cc..391143cb1a 100644
--- a/src/main/java/net/PacketProcessor.java
+++ b/src/main/java/net/PacketProcessor.java
@@ -37,6 +37,9 @@ import java.util.Map;
public final class PacketProcessor {
private static final Logger log = LoggerFactory.getLogger(PacketProcessor.class);
private static final Map instances = new LinkedHashMap<>();
+
+ private static ChannelDependencies channelDeps;
+
private PacketHandler[] handlers;
private PacketProcessor() {
@@ -49,11 +52,19 @@ public final class PacketProcessor {
handlers = new PacketHandler[maxRecvOp + 1];
}
+ public static void registerGameHandlerDependencies(ChannelDependencies channelDependencies) {
+ PacketProcessor.channelDeps = channelDependencies;
+ }
+
public static PacketProcessor getLoginServerProcessor() {
return getProcessor(LoginServer.WORLD_ID, LoginServer.CHANNEL_ID);
}
public static PacketProcessor getChannelServerProcessor(int world, int channel) {
+ if (channelDeps == null) {
+ throw new IllegalStateException("Unable to get channel server processor - dependencies are not registered");
+ }
+
return getProcessor(world, channel);
}
@@ -141,7 +152,7 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.ITEM_SORT, new InventoryMergeHandler());
registerHandler(RecvOpcode.ITEM_MOVE, new ItemMoveHandler());
registerHandler(RecvOpcode.MESO_DROP, new MesoDropHandler());
- registerHandler(RecvOpcode.PLAYER_LOGGEDIN, new PlayerLoggedinHandler());
+ registerHandler(RecvOpcode.PLAYER_LOGGEDIN, new PlayerLoggedinHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.CHANGE_MAP, new ChangeMapHandler());
registerHandler(RecvOpcode.MOVE_LIFE, new MoveLifeHandler());
registerHandler(RecvOpcode.CLOSE_RANGE_ATTACK, new CloseRangeDamageHandler());
@@ -149,7 +160,7 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.MAGIC_ATTACK, new MagicDamageHandler());
registerHandler(RecvOpcode.TAKE_DAMAGE, new TakeDamageHandler());
registerHandler(RecvOpcode.MOVE_PLAYER, new MovePlayerHandler());
- registerHandler(RecvOpcode.USE_CASH_ITEM, new UseCashItemHandler());
+ registerHandler(RecvOpcode.USE_CASH_ITEM, new UseCashItemHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.USE_ITEM, new UseItemHandler());
registerHandler(RecvOpcode.USE_RETURN_SCROLL, new UseItemHandler());
registerHandler(RecvOpcode.USE_UPGRADE_SCROLL, new ScrollHandler());
@@ -191,7 +202,7 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.MESSENGER, new MessengerHandler());
registerHandler(RecvOpcode.NPC_ACTION, new NPCAnimationHandler());
registerHandler(RecvOpcode.CHECK_CASH, new TouchingCashShopHandler());
- registerHandler(RecvOpcode.CASHSHOP_OPERATION, new CashOperationHandler());
+ registerHandler(RecvOpcode.CASHSHOP_OPERATION, new CashOperationHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.COUPON_CODE, new CouponCodeHandler());
registerHandler(RecvOpcode.SPAWN_PET, new SpawnPetHandler());
registerHandler(RecvOpcode.MOVE_PET, new MovePetHandler());
@@ -204,11 +215,11 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.CANCEL_DEBUFF, new CancelDebuffHandler());
registerHandler(RecvOpcode.USE_SKILL_BOOK, new SkillBookHandler());
registerHandler(RecvOpcode.SKILL_MACRO, new SkillMacroHandler());
- registerHandler(RecvOpcode.NOTE_ACTION, new NoteActionHandler());
+ registerHandler(RecvOpcode.NOTE_ACTION, new NoteActionHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.CLOSE_CHALKBOARD, new CloseChalkboardHandler());
registerHandler(RecvOpcode.USE_MOUNT_FOOD, new UseMountFoodHandler());
registerHandler(RecvOpcode.MTS_OPERATION, new MTSHandler());
- registerHandler(RecvOpcode.RING_ACTION, new RingActionHandler());
+ registerHandler(RecvOpcode.RING_ACTION, new RingActionHandler(channelDeps.noteService()));
registerHandler(RecvOpcode.SPOUSE_CHAT, new SpouseChatHandler());
registerHandler(RecvOpcode.PET_AUTO_POT, new PetAutoPotHandler());
registerHandler(RecvOpcode.PET_EXCLUDE_ITEMS, new PetExcludeItemsHandler());
@@ -262,7 +273,7 @@ public final class PacketProcessor {
registerHandler(RecvOpcode.COCONUT, new CoconutHandler());
registerHandler(RecvOpcode.ARAN_COMBO_COUNTER, new AranComboHandler());
registerHandler(RecvOpcode.CLICK_GUIDE, new ClickGuideHandler());
- registerHandler(RecvOpcode.FREDRICK_ACTION, new FredrickHandler());
+ registerHandler(RecvOpcode.FREDRICK_ACTION, new FredrickHandler(channelDeps.fredrickProcessor()));
registerHandler(RecvOpcode.MONSTER_CARNIVAL, new MonsterCarnivalHandler());
registerHandler(RecvOpcode.REMOTE_STORE, new RemoteStoreHandler());
registerHandler(RecvOpcode.WEDDING_ACTION, new WeddingHandler());
diff --git a/src/main/java/net/packet/out/SendNoteSuccessPacket.java b/src/main/java/net/packet/out/SendNoteSuccessPacket.java
new file mode 100644
index 0000000000..a171ea158f
--- /dev/null
+++ b/src/main/java/net/packet/out/SendNoteSuccessPacket.java
@@ -0,0 +1,13 @@
+package net.packet.out;
+
+import net.opcodes.SendOpcode;
+import net.packet.ByteBufOutPacket;
+
+public final class SendNoteSuccessPacket extends ByteBufOutPacket {
+
+ public SendNoteSuccessPacket() {
+ super(SendOpcode.MEMO_RESULT);
+
+ writeByte(4);
+ }
+}
diff --git a/src/main/java/net/packet/out/ShowNotesPacket.java b/src/main/java/net/packet/out/ShowNotesPacket.java
new file mode 100644
index 0000000000..f2b05160ac
--- /dev/null
+++ b/src/main/java/net/packet/out/ShowNotesPacket.java
@@ -0,0 +1,30 @@
+package net.packet.out;
+
+import model.Note;
+import net.opcodes.SendOpcode;
+import net.packet.ByteBufOutPacket;
+
+import java.util.List;
+import java.util.Objects;
+
+import static tools.PacketCreator.getTime;
+
+public final class ShowNotesPacket extends ByteBufOutPacket {
+
+ public ShowNotesPacket(List notes) {
+ super(SendOpcode.MEMO_RESULT);
+ Objects.requireNonNull(notes);
+
+ writeByte(3);
+ writeByte(notes.size());
+ notes.forEach(this::writeNote);
+ }
+
+ private void writeNote(Note note) {
+ writeInt(note.id());
+ writeString(note.from() + " "); //Stupid nexon forgot space lol
+ writeString(note.message());
+ writeLong(getTime(note.timestamp()));
+ writeByte(note.fame());
+ }
+}
diff --git a/src/main/java/net/server/Server.java b/src/main/java/net/server/Server.java
index f47f4770af..5b854bebcd 100644
--- a/src/main/java/net/server/Server.java
+++ b/src/main/java/net/server/Server.java
@@ -30,11 +30,15 @@ import client.inventory.Item;
import client.inventory.ItemFactory;
import client.inventory.manipulator.CashIdGenerator;
import client.newyear.NewYearCardRecord;
+import client.processor.npc.FredrickProcessor;
import config.YamlConfig;
import constants.game.GameConstants;
import constants.inventory.ItemConstants;
import constants.net.OpcodeConstants;
import constants.net.ServerConstants;
+import database.note.NoteDao;
+import net.ChannelDependencies;
+import net.PacketProcessor;
import net.netty.LoginServer;
import net.packet.Packet;
import net.server.channel.Channel;
@@ -55,6 +59,7 @@ import server.TimerManager;
import server.expeditions.ExpeditionBossLog;
import server.life.PlayerNPCFactory;
import server.quest.Quest;
+import service.NoteService;
import tools.DatabaseConnection;
import tools.Pair;
@@ -91,6 +96,7 @@ public class Server {
private static final Set activeFly = new HashSet<>();
private static final Map couponRates = new HashMap<>(30);
private static final List activeCoupons = new LinkedList<>();
+ private static ChannelDependencies channelDependencies;
private LoginServer loginServer;
private final List