diff --git a/config.yaml b/config.yaml
index 04f7da318f..18eaa55d96 100644
--- a/config.yaml
+++ b/config.yaml
@@ -319,6 +319,7 @@ server:
NAME_CHANGE_COOLDOWN: 2592000000 # (30*24*60*60*1000) Cooldown for name changes, default (GMS) is 30 days.
WORLD_TRANSFER_COOLDOWN: 2592000000 # (30*24*60*60*1000) Cooldown for world tranfers, default is same as name change (30 days).
INSTANT_NAME_CHANGE: false #Whether or not to wait for server restart to apply name changes. Does on reconnect otherwise (requires queries on every login).
+ REBIRTH_NPC_ID: 9010021 #ID of the NPC that should be replaced with the rebirth mechanic, if enabled.
#Dangling Items/Locks Configuration
ITEM_EXPIRE_TIME: 180000 # (3 * 60 * 1000) Time before items start disappearing. Recommended to be set up to 3 minutes.
@@ -460,3 +461,7 @@ server:
#Event End Timestamp
EVENT_END_TIMESTAMP: 1428897600000
+ #Any NPC ids that should search for a js override script (useful if they already have wz entries since otherwise they're ignored).
+ NPCS_SCRIPTABLE:
+ #9200000: Talk to Cody # Cody
+ 9001105: Rescue Gaga! # Grandpa moon bunny
diff --git a/pom.xml b/pom.xml
index 753a9d9b08..12b00548cf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,7 +51,11 @@
commons-io
commons-io
2.10.0
-
+
+
+ io.netty
+ netty-buffer
+ 4.1.65.Final
@@ -87,6 +91,20 @@
js-scriptengine
${graalvm.version}
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.7.2
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.7.2
+
+
+
diff --git a/scripts/npc/9010021.js b/scripts/npc/9010021.js
index 07c5e87e62..8899b84e00 100644
--- a/scripts/npc/9010021.js
+++ b/scripts/npc/9010021.js
@@ -21,48 +21,9 @@
*/
/* 9010021 - Wolf Spirit Ryko
@author Ronan
- */
- var status;
-
+ @author wejrox
+*/
function start() {
- status = -1;
- const YamlConfig = Java.type('config.YamlConfig');
- if (!YamlConfig.config.server.USE_REBIRTH_SYSTEM) {
- cm.sendOk("... I came from distant planes to assist the fight against the #rBlack Magician#k. Right now I search my master, have you seen him?");
- cm.dispose();
- return;
- }
- action(1, 0, 0);
-}
-
-function action(mode, type, selection) {
- if (mode == 1) {
- status++;
- } else {
- cm.dispose();
- return;
- }
- if (status == 0) {
- cm.sendNext("Come to me when you want to be reborn again. You currently have a total of #r" + cm.getChar().getReborns() + " #krebirths.");
- } else if (status == 1) {
- cm.sendSimple("What do you want me to do today: \r\n \r\n #L0##bI want to be rebirthed#l \r\n #L1##bMaybe next time#k#l");
- } else if (status == 2) {
- if (selection == 0) {
- if (cm.getChar().getLevel() == 200) {
- cm.sendYesNo("Are you sure you want to be rebirthed?");
- } else {
- cm.sendOk("You are not level 200, please come back when you hit level 200.");
- cm.dispose();
- }
- } else if (selection == 1) {
- cm.sendOk("Ok Bye")
- cm.dispose();
- }
- } else if (status == 3 && type == 1) {
- cm.getChar().executeReborn();
- cm.sendOk("You have now been reborn. That's a total of #r" + cm.getChar().getReborns() + "#k rebirths");
- cm.dispose();
- }
-
-
+ cm.sendOk("... I came from distant planes to assist the fight against the #rBlack Magician#k. Right now I search my master, have you seen him?");
+ cm.dispose();
}
\ No newline at end of file
diff --git a/scripts/npc/rebirth.js b/scripts/npc/rebirth.js
new file mode 100644
index 0000000000..72d2a5f8a6
--- /dev/null
+++ b/scripts/npc/rebirth.js
@@ -0,0 +1,67 @@
+/*
+ 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 .
+*/
+/* Rebirth NPC
+ @author Ronan
+ @author wejrox
+*/
+var status;
+
+function start() {
+ status = -1;
+ const YamlConfig = Java.type('config.YamlConfig');
+ if (!YamlConfig.config.server.USE_REBIRTH_SYSTEM) {
+ cm.sendOk("Rebirths aren't enabled on this server, how did you get here?");
+ cm.dispose();
+ return;
+ }
+ action(1, 0, 0);
+}
+
+function action(mode, type, selection) {
+ if (mode === 1) {
+ status++;
+ } else {
+ cm.dispose();
+ return;
+ }
+ if (status === 0) {
+ cm.sendNext("Come to me when you want to be reborn again. You currently have a total of #r" + cm.getChar().getReborns() + " #krebirths.");
+ } else if (status === 1) {
+ cm.sendSimple("What do you want me to do today: \r\n \r\n #L0##bI want to be reborn!#l \r\n #L1##bNothing for now...#k#l");
+ } else if (status === 2) {
+ if (selection === 0) {
+ if (cm.getChar().getLevel() === 200) {
+ cm.sendYesNo("Are you sure you want to be reborn?");
+ } else {
+ cm.sendOk("You are not level 200, please come back when you hit level 200.");
+ cm.dispose();
+ }
+ } else if (selection === 1) {
+ cm.sendOk("See you soon!")
+ cm.dispose();
+ }
+ } else if (status === 3 && type === 1) {
+ cm.getChar().executeReborn();
+ cm.sendOk("You have now been reborn. That's a total of #r" + cm.getChar().getReborns() + "#k rebirths");
+ cm.dispose();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/config/ServerConfig.java b/src/main/java/config/ServerConfig.java
index dbdd9e0a76..34beaea4df 100644
--- a/src/main/java/config/ServerConfig.java
+++ b/src/main/java/config/ServerConfig.java
@@ -1,5 +1,7 @@
package config;
+import java.util.*;
+
public class ServerConfig {
//Thread Tracker Configuration
public boolean USE_THREAD_TRACKER;
@@ -163,6 +165,7 @@ public class ServerConfig {
public long NAME_CHANGE_COOLDOWN;
public long WORLD_TRANSFER_COOLDOWN=NAME_CHANGE_COOLDOWN;//Cooldown for world tranfers, default is same as name change (30 days).
public boolean INSTANT_NAME_CHANGE;
+ public int REBIRTH_NPC_ID;
//Dangling Items/Locks Configuration
public int ITEM_EXPIRE_TIME ;
@@ -304,4 +307,6 @@ public class ServerConfig {
//Event End Timestamp
public long EVENT_END_TIMESTAMP;
+ //Custom NPC overrides. List of NPC IDs.
+ public Map NPCS_SCRIPTABLE = new HashMap<>();
}
diff --git a/src/main/java/constants/game/ScriptableNPCConstants.java b/src/main/java/constants/game/ScriptableNPCConstants.java
deleted file mode 100644
index df8a255cf9..0000000000
--- a/src/main/java/constants/game/ScriptableNPCConstants.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package constants.game;
-
-/**
- * @brief ScriptableNPCConstants
- * @author GabrielSin
- * @date 16/09/2018
- *
- * Adaptations to use Pair and Set, in order to suit a one-packet marshall.
- * Adapted by Ronan
- */
-
-import java.util.HashSet;
-import java.util.Set;
-import tools.Pair;
-
-public class ScriptableNPCConstants {
-
- public static final Set> SCRIPTABLE_NPCS = new HashSet>(){{
- //add(new Pair<>(9200000, "Cody"));
- add(new Pair<>(9001105, "Grandpa Moon Bunny"));
- }};
-
-}
-
\ No newline at end of file
diff --git a/src/main/java/net/packet/ByteBufInPacket.java b/src/main/java/net/packet/ByteBufInPacket.java
new file mode 100644
index 0000000000..0977172354
--- /dev/null
+++ b/src/main/java/net/packet/ByteBufInPacket.java
@@ -0,0 +1,81 @@
+package net.packet;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+
+import java.awt.*;
+
+public class ByteBufInPacket implements InPacket {
+ private final ByteBuf byteBuf;
+
+ public ByteBufInPacket(ByteBuf byteBuf) {
+ this.byteBuf = byteBuf;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return ByteBufUtil.getBytes(byteBuf);
+ }
+
+ @Override
+ public byte readByte() {
+ return byteBuf.readByte();
+ }
+
+ @Override
+ public short readShort() {
+ return byteBuf.readShortLE();
+ }
+
+ @Override
+ public int readInt() {
+ return byteBuf.readIntLE();
+ }
+
+ @Override
+ public long readLong() {
+ return byteBuf.readLongLE();
+ }
+
+ @Override
+ public Point readPoint() {
+ final short x = byteBuf.readShortLE();
+ final short y = byteBuf.readShortLE();
+ return new Point(x, y);
+ }
+
+ @Override
+ public String readString() {
+ short length = readShort();
+ byte[] stringBytes = new byte[length];
+ byteBuf.readBytes(stringBytes);
+ return new String(stringBytes, STRING_CHARSET);
+ }
+
+ @Override
+ public byte[] readBytes(int numberOfBytes) {
+ byte[] bytes = new byte[numberOfBytes];
+ byteBuf.readBytes(bytes);
+ return bytes;
+ }
+
+ @Override
+ public void skip(int numberOfBytes) {
+ byteBuf.skipBytes(numberOfBytes);
+ }
+
+ @Override
+ public int available() {
+ return byteBuf.readableBytes();
+ }
+
+ @Override
+ public void seek(int byteOffset) {
+ byteBuf.readerIndex(byteOffset);
+ }
+
+ @Override
+ public int getPosition() {
+ return byteBuf.readerIndex();
+ }
+}
diff --git a/src/main/java/net/packet/ByteBufOutPacket.java b/src/main/java/net/packet/ByteBufOutPacket.java
new file mode 100644
index 0000000000..5aa7a20b79
--- /dev/null
+++ b/src/main/java/net/packet/ByteBufOutPacket.java
@@ -0,0 +1,83 @@
+package net.packet;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
+import net.jcip.annotations.NotThreadSafe;
+import net.opcodes.SendOpcode;
+
+import java.awt.*;
+
+@NotThreadSafe
+public class ByteBufOutPacket implements OutPacket {
+ private final ByteBuf byteBuf;
+
+ public ByteBufOutPacket(SendOpcode op) {
+ ByteBuf byteBuf = Unpooled.buffer();
+ byteBuf.writeShortLE((short) op.getValue());
+ this.byteBuf = byteBuf;
+ }
+
+ public ByteBufOutPacket(SendOpcode op, int initialCapacity) {
+ ByteBuf byteBuf = Unpooled.buffer(initialCapacity);
+ byteBuf.writeShortLE((short) op.getValue());
+ this.byteBuf = byteBuf;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return ByteBufUtil.getBytes(byteBuf);
+ }
+
+ @Override
+ public void writeByte(byte value) {
+ byteBuf.writeByte(value);
+ }
+
+ @Override
+ public void writeByte(int value) {
+ writeByte((byte) value);
+ }
+
+ @Override
+ public void writeBytes(byte[] value) {
+ byteBuf.writeBytes(value);
+ }
+
+ @Override
+ public void writeShort(int value) {
+ byteBuf.writeShortLE(value);
+ }
+
+ @Override
+ public void writeInt(int value) {
+ byteBuf.writeIntLE(value);
+ }
+
+ @Override
+ public void writeLong(long value) {
+ byteBuf.writeLongLE(value);
+ }
+
+ @Override
+ public void writeBoolean(boolean value) {
+ byteBuf.writeByte(value ? 1 : 0);
+ }
+
+ @Override
+ public void writeString(String value) {
+ writeShort((short) value.length());
+ writeBytes(value.getBytes(STRING_CHARSET));
+ }
+
+ @Override
+ public void writePoint(Point value) {
+ writeShort((short) value.getX());
+ writeShort((short) value.getY());
+ }
+
+ @Override
+ public void skip(int numberOfBytes) {
+ writeBytes(new byte[numberOfBytes]);
+ }
+}
diff --git a/src/main/java/net/packet/InPacket.java b/src/main/java/net/packet/InPacket.java
new file mode 100644
index 0000000000..2c899c2ade
--- /dev/null
+++ b/src/main/java/net/packet/InPacket.java
@@ -0,0 +1,17 @@
+package net.packet;
+
+import java.awt.*;
+
+public interface InPacket extends Packet {
+ byte readByte();
+ short readShort();
+ int readInt();
+ long readLong();
+ Point readPoint();
+ String readString();
+ byte[] readBytes(int numberOfBytes);
+ void skip(int numberOfBytes);
+ int available();
+ void seek(int byteOffset);
+ int getPosition();
+}
diff --git a/src/main/java/net/packet/OutPacket.java b/src/main/java/net/packet/OutPacket.java
new file mode 100644
index 0000000000..57e953a6f7
--- /dev/null
+++ b/src/main/java/net/packet/OutPacket.java
@@ -0,0 +1,16 @@
+package net.packet;
+
+import java.awt.*;
+
+public interface OutPacket extends Packet {
+ void writeByte(byte value);
+ void writeByte(int value);
+ void writeBytes(byte[] value);
+ void writeShort(int value);
+ void writeInt(int value);
+ void writeLong(long value);
+ void writeBoolean(boolean value);
+ void writeString(String value);
+ void writePoint(Point value);
+ void skip(int numberOfBytes);
+}
diff --git a/src/main/java/net/packet/Packet.java b/src/main/java/net/packet/Packet.java
new file mode 100644
index 0000000000..79f257cd38
--- /dev/null
+++ b/src/main/java/net/packet/Packet.java
@@ -0,0 +1,10 @@
+package net.packet;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+public interface Packet {
+ Charset STRING_CHARSET = StandardCharsets.US_ASCII;
+
+ byte[] getBytes();
+}
diff --git a/src/main/java/net/server/channel/handlers/NPCTalkHandler.java b/src/main/java/net/server/channel/handlers/NPCTalkHandler.java
index 71f71c345c..73fec20467 100644
--- a/src/main/java/net/server/channel/handlers/NPCTalkHandler.java
+++ b/src/main/java/net/server/channel/handlers/NPCTalkHandler.java
@@ -65,6 +65,8 @@ public final class NPCTalkHandler extends AbstractMaplePacketHandler {
NPCScriptManager.getInstance().start(c, npc.getId(), "gachapon", null);
} else if (npc.getName().endsWith("Maple TV")) {
NPCScriptManager.getInstance().start(c, npc.getId(), "mapleTV", null);
+ } else if (YamlConfig.config.server.USE_REBIRTH_SYSTEM && npc.getId() == YamlConfig.config.server.REBIRTH_NPC_ID) {
+ NPCScriptManager.getInstance().start(c, npc.getId(), "rebirth", null);
} else {
boolean hasNpcScript = NPCScriptManager.getInstance().start(c, npc.getId(), oid, null);
if (!hasNpcScript) {
diff --git a/src/main/java/net/server/channel/handlers/PlayerLoggedinHandler.java b/src/main/java/net/server/channel/handlers/PlayerLoggedinHandler.java
index 4db6a00253..75c2d12958 100644
--- a/src/main/java/net/server/channel/handlers/PlayerLoggedinHandler.java
+++ b/src/main/java/net/server/channel/handlers/PlayerLoggedinHandler.java
@@ -26,7 +26,6 @@ import client.inventory.*;
import client.keybind.MapleKeyBinding;
import config.YamlConfig;
import constants.game.GameConstants;
-import constants.game.ScriptableNPCConstants;
import net.AbstractMaplePacketHandler;
import net.server.PlayerBuffValueHolder;
import net.server.Server;
@@ -55,6 +54,7 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.Map.Entry;
+import java.util.stream.Collectors;
public final class PlayerLoggedinHandler extends AbstractMaplePacketHandler {
@@ -407,9 +407,23 @@ public final class PlayerLoggedinHandler extends AbstractMaplePacketHandler {
eim.registerPlayer(player);
}
}
-
+
+ // Tell the client to use the custom scripts available for the NPCs provided, instead of the WZ entries.
if (YamlConfig.config.server.USE_NPCS_SCRIPTABLE) {
- c.announce(MaplePacketCreator.setNPCScriptable(ScriptableNPCConstants.SCRIPTABLE_NPCS));
+
+ // Create a copy to prevent always adding entries to the server's list.
+ Map npcsIds = YamlConfig.config.server.NPCS_SCRIPTABLE
+ .entrySet().stream().collect(Collectors.toMap(
+ entry -> Integer.parseInt(entry.getKey()),
+ Entry::getValue
+ ));
+
+ // Any npc be specified as the rebirth npc. Allow the npc to use custom scripts explicitly.
+ if (YamlConfig.config.server.USE_REBIRTH_SYSTEM) {
+ npcsIds.put(YamlConfig.config.server.REBIRTH_NPC_ID, "Rebirth");
+ }
+
+ c.announce(MaplePacketCreator.setNPCScriptable(npcsIds));
}
if(newcomer) player.setLoginTime(System.currentTimeMillis());
diff --git a/src/main/java/tools/MaplePacketCreator.java b/src/main/java/tools/MaplePacketCreator.java
index 72f3fe3963..e135021a86 100644
--- a/src/main/java/tools/MaplePacketCreator.java
+++ b/src/main/java/tools/MaplePacketCreator.java
@@ -8327,17 +8327,25 @@ public class MaplePacketCreator {
mplew.writeInt(transition);
return mplew.getPacket();
}
-
- public static byte[] setNPCScriptable(Set> scriptNpcDescriptions) { // thanks to GabrielSin
+
+ /**
+ * Makes the NPCs provided set as scriptable, informing the client to search for js scripts for these NPCs even
+ * if they already have entries within the wz files.
+ *
+ * @param scriptableNpcIds Ids of npcs to enable scripts for.
+ * @return a packet which makes the npc's provided scriptable.
+ */
+ public static byte[] setNPCScriptable(Map scriptableNpcIds) { // thanks to GabrielSin
MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
mplew.writeShort(SendOpcode.SET_NPC_SCRIPTABLE.getValue());
- mplew.write(scriptNpcDescriptions.size());
- for (Pair p : scriptNpcDescriptions) {
- mplew.writeInt(p.getLeft());
- mplew.writeMapleAsciiString(p.getRight());
+ mplew.write(scriptableNpcIds.size());
+ scriptableNpcIds.forEach((id, name) -> {
+ mplew.writeInt(id);
+ // The client needs a name for the npc conversation, which is displayed under etc when the npc has a quest available.
+ mplew.writeMapleAsciiString(name);
mplew.writeInt(0); // start time
mplew.writeInt(Integer.MAX_VALUE); // end time
- }
+ });
return mplew.getPacket();
}
@@ -8383,17 +8391,17 @@ public class MaplePacketCreator {
final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
mplew.writeShort(SendOpcode.TOURNAMENT_SET_PRIZE.getValue());
- //0 = "You have failed the set the prize. Please check the item number again."
- //1 = "You have successfully set the prize."
- mplew.write(bSetPrize);
-
- mplew.write(bHasPrize);
-
- if(bHasPrize != 0)
- {
- mplew.writeInt(nItemID1);
- mplew.writeInt(nItemID2);
- }
+ //0 = "You have failed the set the prize. Please check the item number again."
+ //1 = "You have successfully set the prize."
+ mplew.write(bSetPrize);
+
+ mplew.write(bHasPrize);
+
+ if(bHasPrize != 0)
+ {
+ mplew.writeInt(nItemID1);
+ mplew.writeInt(nItemID2);
+ }
return mplew.getPacket();
}
@@ -8402,11 +8410,11 @@ public class MaplePacketCreator {
final MaplePacketLittleEndianWriter mplew = new MaplePacketLittleEndianWriter();
mplew.writeShort(SendOpcode.TOURNAMENT_UEW.getValue());
- //Is this a bitflag o.o ?
- //2 = "You have reached the finals by default."
- //4 = "You have reached the semifinals by default."
- //8 or 16 = "You have reached the round of %n by default." | Encodes nState as %n ?!
- mplew.write(nState);
+ //Is this a bitflag o.o ?
+ //2 = "You have reached the finals by default."
+ //4 = "You have reached the semifinals by default."
+ //8 or 16 = "You have reached the round of %n by default." | Encodes nState as %n ?!
+ mplew.write(nState);
return mplew.getPacket();
}
diff --git a/src/test/java/net/packet/ByteBufInPacketTest.java b/src/test/java/net/packet/ByteBufInPacketTest.java
new file mode 100644
index 0000000000..80a611abd8
--- /dev/null
+++ b/src/test/java/net/packet/ByteBufInPacketTest.java
@@ -0,0 +1,192 @@
+package net.packet;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.awt.*;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ByteBufInPacketTest {
+ private ByteBuf byteBuf;
+ private InPacket inPacket;
+
+ @BeforeEach
+ void reset() {
+ this.byteBuf = Unpooled.buffer();
+ this.inPacket = new ByteBufInPacket(byteBuf);
+ }
+
+ private void givenWrittenBytes(int... bytes) {
+ for (int b : bytes) {
+ byteBuf.writeByte(b);
+ }
+ }
+
+ @Test
+ void readByte() {
+ final byte writtenByte = 123;
+ byteBuf.writeByte(writtenByte);
+
+ byte readByte = inPacket.readByte();
+
+ assertEquals(writtenByte, readByte);
+ }
+
+ @Test
+ void readShort() {
+ final short writtenShort = 12_345;
+ byteBuf.writeShortLE(writtenShort);
+
+ short readShort = inPacket.readShort();
+
+ assertEquals(writtenShort, readShort);
+ }
+
+ @Test
+ void readInt() {
+ final int writtenInt = 1_234_567_890;
+ byteBuf.writeIntLE(writtenInt);
+
+ int readInt = inPacket.readInt();
+
+ assertEquals(writtenInt, readInt);
+ }
+
+ @Test
+ void readLong() {
+ final long writtenLong = 9_223_372_036_854_775_807L;
+ byteBuf.writeLongLE(writtenLong);
+
+ long readLong = inPacket.readLong();
+
+ assertEquals(writtenLong, readLong);
+ }
+
+ @Test
+ void readPoint() {
+ final Point writtenPoint = new Point(111, 222);
+ byteBuf.writeShortLE((short) writtenPoint.getX());
+ byteBuf.writeShortLE((short) writtenPoint.getY());
+
+ Point readPoint = inPacket.readPoint();
+
+ assertEquals(writtenPoint, readPoint);
+ }
+
+ @Test
+ void readString() {
+ final String writtenString = "You have gained experience (+3200)";
+ byteBuf.writeShortLE(writtenString.length());
+ byte[] writtenStringBytes = writtenString.getBytes(Packet.STRING_CHARSET);
+ byteBuf.writeBytes(writtenStringBytes);
+
+ String readString = inPacket.readString();
+
+ assertEquals(writtenString, readString);
+ }
+
+ @Test
+ void readBytes() {
+ givenWrittenBytes(10, 11, 12, 13, 14, 15);
+
+ byte[] byteBatch1 = inPacket.readBytes(1);
+ assertEquals(1, byteBatch1.length);
+ assertEquals(10, byteBatch1[0]);
+
+ byte[] byteBatch2 = inPacket.readBytes(2);
+ assertEquals(2, byteBatch2.length);
+ assertEquals(11, byteBatch2[0]);
+ assertEquals(12, byteBatch2[1]);
+
+ byte[] byteBatch3 = inPacket.readBytes(3);
+ assertEquals(3, byteBatch3.length);
+ assertEquals(13, byteBatch3[0]);
+ assertEquals(14, byteBatch3[1]);
+ assertEquals(15, byteBatch3[2]);
+ }
+
+ @Test
+ void skip() {
+ givenWrittenBytes(20, 21, 22, 23, 24, 25);
+
+ byte firstByte = inPacket.readByte();
+ assertEquals(20, firstByte);
+
+ inPacket.skip(3);
+
+ byte fifthByte = inPacket.readByte();
+ assertEquals(24, fifthByte);
+ }
+
+ @Test
+ void available() {
+ givenWrittenBytes(30, 31, 32, 33, 34, 35);
+
+ assertEquals(6, inPacket.available());
+
+ inPacket.readByte();
+ assertEquals(5, inPacket.available());
+
+ inPacket.readInt();
+ assertEquals(1, inPacket.available());
+ }
+
+ @Test
+ void seek() {
+ givenWrittenBytes(40, 41, 42, 43, 44, 45);
+
+ inPacket.seek(2);
+ assertEquals(4, inPacket.available());
+ byte byteAtSeek = inPacket.readByte();
+ assertEquals(42, byteAtSeek);
+
+ inPacket.seek(0);
+ byte byteAtReset = inPacket.readByte();
+ assertEquals(40, byteAtReset);
+ }
+
+ @Test
+ void getPosition() {
+ givenWrittenBytes(50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60);
+
+ assertEquals(0, inPacket.getPosition());
+
+ inPacket.readByte();
+ assertEquals(1, inPacket.getPosition());
+
+ inPacket.readShort();
+ assertEquals(3, inPacket.getPosition());
+
+ inPacket.readInt();
+ assertEquals(7, inPacket.getPosition());
+
+ inPacket.seek(5);
+ assertEquals(5, inPacket.getPosition());
+ }
+
+ @Test
+ void getBytes() {
+ givenWrittenBytes(20, 19, 21, 18, 22);
+
+ byte[] bytes = inPacket.getBytes();
+
+ assertArrayEquals(new byte[]{20, 19, 21, 18, 22}, bytes);
+ }
+
+ @Test
+ void whenGetBytes_shouldBeRepeatable() {
+ givenWrittenBytes(1, 2, 3, 4, 5);
+
+ byte[] bytes = inPacket.getBytes();
+ assertEquals(5, bytes.length);
+
+ byte[] sameBytes = inPacket.getBytes();
+ assertEquals(5, sameBytes.length);
+
+ assertArrayEquals(bytes, sameBytes);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/net/packet/ByteBufOutPacketTest.java b/src/test/java/net/packet/ByteBufOutPacketTest.java
new file mode 100644
index 0000000000..3673254736
--- /dev/null
+++ b/src/test/java/net/packet/ByteBufOutPacketTest.java
@@ -0,0 +1,206 @@
+package net.packet;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import net.opcodes.SendOpcode;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.awt.*;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class ByteBufOutPacketTest {
+ private ByteBufOutPacket outPacket;
+
+ @BeforeEach
+ void reset() {
+ outPacket = new ByteBufOutPacket(SendOpcode.ADMIN_SHOP); // Any opcode will do
+ }
+
+ private static ByteBuf wrapExplicitlyWrittenBytes(OutPacket outPacket) {
+ byte[] packetBytes = outPacket.getBytes();
+ ByteBuf byteBuf = Unpooled.copiedBuffer(packetBytes);
+ byteBuf.readShortLE(); // Skip over opcode
+ return byteBuf;
+ }
+
+ @Test
+ void whenInstantiatingNew_shouldWriteOpcode() {
+ byte[] packetBytes = new ByteBufOutPacket(SendOpcode.NPC_TALK).getBytes();
+ assertEquals(2, packetBytes.length);
+ }
+
+ @Test
+ void getBytes() {
+ ByteBufOutPacket outPacket = new ByteBufOutPacket(SendOpcode.PING); // This opcode has value 0x11 = 17 in decimal
+ outPacket.writeByte(10);
+ outPacket.writeByte(20);
+ outPacket.writeByte(30);
+
+ byte[] bytes = outPacket.getBytes();
+
+ assertArrayEquals(new byte[]{(byte) 17, (byte) 0, (byte) 10, (byte) 20, (byte) 30}, bytes);
+ }
+
+ @Test
+ void writeByte() {
+ final byte writtenByte = 19;
+ outPacket.writeByte(writtenByte);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ byte readByte = wrapped.readByte();
+
+ assertEquals(writtenByte, readByte);
+ }
+
+ @Test
+ void writeByteFromInt() {
+ final int writtenInt = 123;
+ outPacket.writeByte(writtenInt);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ byte readByte = wrapped.readByte();
+
+ assertEquals(writtenInt, readByte);
+ }
+
+ @Test
+ void whenWritingByteFromInt_shouldOnlyWrite1Byte() {
+ final int writtenInt = Integer.MAX_VALUE;
+ outPacket.writeByte(writtenInt);
+
+ byte[] bytes = outPacket.getBytes();
+ assertEquals(2 + 1, bytes.length); // 2 for opcode
+ }
+
+ @Test
+ void writeBytes() {
+ byte[] writtenBytes = {101, 102, 103};
+ outPacket.writeBytes(writtenBytes);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+
+ assertEquals(101, wrapped.readByte());
+ assertEquals(102, wrapped.readByte());
+ assertEquals(103, wrapped.readByte());
+ }
+
+ @Test
+ void writeShort() {
+ final short writtenShort = 4312;
+ outPacket.writeShort(writtenShort);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ short readShort = wrapped.readShortLE();
+
+ assertEquals(writtenShort, readShort);
+ }
+
+ @Test
+ void whenWritingShortFromInt_shouldOnlyWrite2Bytes() {
+ final int writtenInt = Integer.MAX_VALUE;
+ outPacket.writeShort(writtenInt);
+
+ byte[] bytes = outPacket.getBytes();
+ assertEquals(2 + 2, bytes.length); // 2 for opcode
+ }
+
+ @Test
+ void writeShortFromInt() {
+ final int writtenInt = 34_567;
+ outPacket.writeShort(writtenInt);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ short readShort = wrapped.readShortLE();
+
+ assertEquals((short) writtenInt, readShort);
+ }
+
+ @Test
+ void writeInt() {
+ final int writtenInt = 1_010_101_010;
+ outPacket.writeInt(writtenInt);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ int readInt = wrapped.readIntLE();
+
+ assertEquals(writtenInt, readInt);
+ }
+
+ @Test
+ void writeLong() {
+ final long writtenLong = 100_200_300_400_500_600L;
+ outPacket.writeLong(writtenLong);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ long readLong = wrapped.readLongLE();
+
+ assertEquals(writtenLong, readLong);
+ }
+
+ @Test
+ void writeBoolean_true() {
+ outPacket.writeBoolean(true);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ byte readByte = wrapped.readByte();
+
+ assertEquals(1, readByte);
+ }
+
+ @Test
+ void writeBoolean_false() {
+ outPacket.writeBoolean(false);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ byte readByte = wrapped.readByte();
+
+ assertEquals(0, readByte);
+ }
+
+ @Test
+ void writeString() {
+ final String writtenString = "You've been weakened, making you unable to jump.";
+ outPacket.writeString(writtenString);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ int length = wrapped.readShortLE();
+ byte[] stringBytes = new byte[length];
+ wrapped.readBytes(stringBytes);
+ String readString = new String(stringBytes, StandardCharsets.US_ASCII);
+
+ assertEquals(writtenString, readString);
+ }
+
+ @Test
+ void writePoint() {
+ final Point writtenPoint = new Point(23, 42);
+ outPacket.writePoint(writtenPoint);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+ short readX = wrapped.readShortLE();
+ short readY = wrapped.readShortLE();
+
+ assertEquals((short) writtenPoint.getX(), readX);
+ assertEquals((short) writtenPoint.getY(), readY);
+ }
+
+ @Test
+ void whenSkipping_shouldWriteZeroes() {
+ final byte firstWrittenByte = 9;
+ final byte secondWrittenByte = 11;
+ outPacket.writeByte(firstWrittenByte);
+ outPacket.skip(2);
+ outPacket.writeByte(secondWrittenByte);
+
+ ByteBuf wrapped = wrapExplicitlyWrittenBytes(outPacket);
+
+ assertEquals(firstWrittenByte, wrapped.readByte());
+ assertEquals(0, wrapped.readByte());
+ assertEquals(0, wrapped.readByte());
+ assertEquals(secondWrittenByte, wrapped.readByte());
+ }
+}
\ No newline at end of file