diff --git a/src/main/java/emu/grasscutter/database/DatabaseHelper.java b/src/main/java/emu/grasscutter/database/DatabaseHelper.java index fffc6ab73..e6d562431 100644 --- a/src/main/java/emu/grasscutter/database/DatabaseHelper.java +++ b/src/main/java/emu/grasscutter/database/DatabaseHelper.java @@ -1,7 +1,5 @@ package emu.grasscutter.database; -import static com.mongodb.client.model.Filters.eq; - import dev.morphia.query.*; import dev.morphia.query.experimental.filters.Filters; import emu.grasscutter.*; @@ -20,24 +18,19 @@ import emu.grasscutter.game.player.Player; import emu.grasscutter.game.quest.GameMainQuest; import emu.grasscutter.game.world.SceneGroupInstance; import emu.grasscutter.utils.objects.Returnable; -import io.netty.util.concurrent.FastThreadLocalThread; +import lombok.Getter; + +import javax.annotation.Nullable; import java.util.List; import java.util.concurrent.*; import java.util.stream.Stream; -import javax.annotation.Nullable; -import lombok.Getter; + +import static com.mongodb.client.model.Filters.eq; public final class DatabaseHelper { @Getter private static final ExecutorService eventExecutor = - new ThreadPoolExecutor( - 6, - 6, - 60, - TimeUnit.SECONDS, - new LinkedBlockingDeque<>(), - FastThreadLocalThread::new, - new ThreadPoolExecutor.AbortPolicy()); + Executors.newFixedThreadPool(4); /** * Saves an object on the account datastore. diff --git a/src/main/java/emu/grasscutter/game/achievement/Achievements.java b/src/main/java/emu/grasscutter/game/achievement/Achievements.java index 929c382cb..c3739ac6b 100644 --- a/src/main/java/emu/grasscutter/game/achievement/Achievements.java +++ b/src/main/java/emu/grasscutter/game/achievement/Achievements.java @@ -12,12 +12,13 @@ import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.net.proto.AchievementOuterClass.Achievement.Status; import emu.grasscutter.server.event.player.PlayerCompleteAchievementEvent; import emu.grasscutter.server.packet.send.*; +import lombok.*; +import org.bson.types.ObjectId; + +import javax.annotation.Nullable; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntSupplier; -import javax.annotation.Nullable; -import lombok.*; -import org.bson.types.ObjectId; @Entity("achievements") @Data @@ -44,15 +45,30 @@ public class Achievements { return achievements; } + /** + * Creates a blank achievements object. + * + * @return The achievements object. + */ + public static Achievements blank() { + return Achievements.of() + .achievementList(init()) + .finishedAchievementNum(0) + .takenGoalRewardIdList(Lists.newArrayList()) + .build(); + } + + /** + * Creates and saves a blank achievements object. + * + * @param uid The UID of the player. + * @return The achievements object. + */ public static Achievements create(int uid) { - var newAchievement = - Achievements.of() - .uid(uid) - .achievementList(init()) - .finishedAchievementNum(0) - .takenGoalRewardIdList(Lists.newArrayList()) - .build(); + var newAchievement = blank(); + newAchievement.setUid(uid); newAchievement.save(); + return newAchievement; } diff --git a/src/main/java/emu/grasscutter/game/battlepass/BattlePassManager.java b/src/main/java/emu/grasscutter/game/battlepass/BattlePassManager.java index 0d48355a5..0e09acb50 100644 --- a/src/main/java/emu/grasscutter/game/battlepass/BattlePassManager.java +++ b/src/main/java/emu/grasscutter/game/battlepass/BattlePassManager.java @@ -14,11 +14,12 @@ import emu.grasscutter.net.proto.BattlePassRewardTakeOptionOuterClass.BattlePass import emu.grasscutter.net.proto.BattlePassScheduleOuterClass.BattlePassSchedule; import emu.grasscutter.net.proto.BattlePassUnlockStatusOuterClass.BattlePassUnlockStatus; import emu.grasscutter.server.packet.send.*; +import lombok.Getter; +import org.bson.types.ObjectId; + import java.time.*; import java.time.temporal.TemporalAdjusters; import java.util.*; -import lombok.Getter; -import org.bson.types.ObjectId; @Entity(value = "battlepass", useDiscriminator = false) public class BattlePassManager extends BasePlayerDataManager { @@ -40,7 +41,10 @@ public class BattlePassManager extends BasePlayerDataManager { public BattlePassManager(Player player) { super(player); + this.ownerUid = player.getUid(); + this.missions = new HashMap<>(); + this.takenRewards = new HashMap<>(); } public void setPlayer(Player player) { diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index d4b242d31..ac174e5b2 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -8,7 +8,7 @@ import emu.grasscutter.data.excels.world.WeatherData; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.*; import emu.grasscutter.game.ability.AbilityManager; -import emu.grasscutter.game.achievement.Achievements; +import emu.grasscutter.game.achievement.*; import emu.grasscutter.game.activity.ActivityManager; import emu.grasscutter.game.avatar.*; import emu.grasscutter.game.battlepass.BattlePassManager; @@ -259,6 +259,7 @@ public class Player implements PlayerHook, FieldFetch { this.clientAbilityInitFinishHandler = new InvokeHandler(PacketClientAbilityInitFinishNotify.class); this.birthday = new PlayerBirthday(); + this.achievements = Achievements.blank(); this.rewardedLevels = new HashSet<>(); this.homeRewardedLevels = new HashSet<>(); this.seenRealmList = new HashSet<>(); @@ -273,8 +274,10 @@ public class Player implements PlayerHook, FieldFetch { this.energyManager = new EnergyManager(this); this.resinManager = new ResinManager(this); this.forgingManager = new ForgingManager(this); + this.deforestationManager = new DeforestationManager(this); this.progressManager = new PlayerProgressManager(this); this.furnitureManager = new FurnitureManager(this); + this.battlePassManager = new BattlePassManager(this); this.cookingManager = new CookingManager(this); this.cookingCompoundManager = new CookingCompoundManager(this); this.satiationManager = new SatiationManager(this); @@ -297,19 +300,6 @@ public class Player implements PlayerHook, FieldFetch { this.applyProperties(); this.getFlyCloakList().add(140001); this.getNameCardList().add(210001); - - this.mapMarksManager = new MapMarksManager(this); - this.staminaManager = new StaminaManager(this); - this.sotsManager = new SotSManager(this); - this.energyManager = new EnergyManager(this); - this.resinManager = new ResinManager(this); - this.deforestationManager = new DeforestationManager(this); - this.forgingManager = new ForgingManager(this); - this.progressManager = new PlayerProgressManager(this); - this.furnitureManager = new FurnitureManager(this); - this.cookingManager = new CookingManager(this); - this.cookingCompoundManager = new CookingCompoundManager(this); - this.satiationManager = new SatiationManager(this); } @Override diff --git a/src/main/java/emu/grasscutter/game/quest/QuestManager.java b/src/main/java/emu/grasscutter/game/quest/QuestManager.java index 6611da1c7..039caf074 100644 --- a/src/main/java/emu/grasscutter/game/quest/QuestManager.java +++ b/src/main/java/emu/grasscutter/game/quest/QuestManager.java @@ -1,8 +1,5 @@ package emu.grasscutter.game.quest; -import static emu.grasscutter.GameConstants.DEBUG; -import static emu.grasscutter.config.Configuration.*; - import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; import emu.grasscutter.data.binout.*; @@ -15,12 +12,16 @@ import emu.grasscutter.net.proto.GivingRecordOuterClass.GivingRecord; import emu.grasscutter.server.packet.send.*; import io.netty.util.concurrent.FastThreadLocalThread; import it.unimi.dsi.fastutil.ints.*; +import lombok.*; + +import javax.annotation.Nonnull; import java.util.*; import java.util.concurrent.*; import java.util.function.Consumer; import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import lombok.*; + +import static emu.grasscutter.GameConstants.DEBUG; +import static emu.grasscutter.config.Configuration.*; public final class QuestManager extends BasePlayerManager { @Getter private final Player player; @@ -571,7 +572,7 @@ public final class QuestManager extends BasePlayerManager { * @param quest The ID of the quest. */ public void checkQuestAlreadyFulfilled(GameQuest quest) { - Grasscutter.getThreadPool() + eventExecutor .submit( () -> { for (var condition : quest.getQuestData().getFinishCond()) { diff --git a/src/test/java/io/grasscutter/GrasscutterTest.java b/src/test/java/io/grasscutter/GrasscutterTest.java index 7c2e7f7f9..501aef976 100644 --- a/src/test/java/io/grasscutter/GrasscutterTest.java +++ b/src/test/java/io/grasscutter/GrasscutterTest.java @@ -1,60 +1,18 @@ package io.grasscutter; -import com.mchange.util.AssertException; -import emu.grasscutter.Grasscutter; -import emu.grasscutter.config.Configuration; -import java.io.IOException; -import lombok.Getter; +import io.grasscutter.virtual.*; +import lombok.*; import okhttp3.OkHttpClient; -import okhttp3.Request; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -/** Testing entrypoint for {@link Grasscutter}. */ +import java.util.concurrent.*; + public final class GrasscutterTest { - @Getter private static final OkHttpClient httpClient = new OkHttpClient(); + @Getter + public static final OkHttpClient httpClient = new OkHttpClient(); + @Getter public static final ExecutorService executor = Executors.newSingleThreadExecutor(); - @Getter private static int httpPort = -1; - @Getter private static int gamePort = -1; - - /** - * Creates an HTTP URL. - * - * @param route The route to use. - * @return The URL. - */ - public static String http(String route) { - return "http://127.0.0.1:" + GrasscutterTest.httpPort + "/" + route; - } - - @BeforeAll - public static void entry() { - try { - // Start Grasscutter. - Grasscutter.main(new String[] {"-test"}); - } catch (Exception ignored) { - throw new AssertException("Grasscutter failed to start."); - } - - // Set the ports. - GrasscutterTest.httpPort = Configuration.SERVER.http.bindPort; - GrasscutterTest.gamePort = Configuration.SERVER.game.bindPort; - } - - @Test - @DisplayName("HTTP server check") - public void checkHttpServer() { - // Create a request. - var request = new Request.Builder().url(GrasscutterTest.http("")).build(); - - // Perform the request. - try (var response = GrasscutterTest.httpClient.newCall(request).execute()) { - // Check the response. - Assertions.assertTrue(response.isSuccessful()); - } catch (IOException exception) { - throw new AssertionError(exception); - } - } + @Getter public static VirtualAccount account; + @Setter + @Getter public static VirtualPlayer player; + @Getter public static VirtualGameSession gameSession; } diff --git a/src/test/java/io/grasscutter/TestUtils.java b/src/test/java/io/grasscutter/TestUtils.java new file mode 100644 index 000000000..529c12be5 --- /dev/null +++ b/src/test/java/io/grasscutter/TestUtils.java @@ -0,0 +1,17 @@ +package io.grasscutter; + +import emu.grasscutter.utils.Utils; +import emu.grasscutter.utils.objects.Returnable; + +public interface TestUtils { + /** + * Waits for a condition to be met. + * + * @param condition The condition. + */ + static void waitFor(Returnable condition) { + while (!condition.invoke()) { + Utils.sleep(100); + } + } +} diff --git a/src/test/java/io/grasscutter/tests/BaseServerTest.java b/src/test/java/io/grasscutter/tests/BaseServerTest.java new file mode 100644 index 000000000..d195c1c16 --- /dev/null +++ b/src/test/java/io/grasscutter/tests/BaseServerTest.java @@ -0,0 +1,61 @@ +package io.grasscutter.tests; + +import com.mchange.util.AssertException; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.config.Configuration; +import io.grasscutter.GrasscutterTest; +import io.grasscutter.virtual.*; +import lombok.*; +import okhttp3.*; +import org.junit.jupiter.api.*; + +import java.io.IOException; + +/** Testing entrypoint for {@link Grasscutter}. */ +public final class BaseServerTest { + @Getter private static int httpPort = -1; + @Getter private static int gamePort = -1; + + /** + * Creates an HTTP URL. + * + * @param route The route to use. + * @return The URL. + */ + public static String http(String route) { + return "http://127.0.0.1:" + BaseServerTest.httpPort + "/" + route; + } + + @BeforeAll + public static void entry() { + try { + // Start Grasscutter. + Grasscutter.main(new String[] {"-test"}); + } catch (Exception ignored) { + throw new AssertException("Grasscutter failed to start."); + } + + // Set the ports. + BaseServerTest.httpPort = Configuration.SERVER.http.bindPort; + BaseServerTest.gamePort = Configuration.SERVER.game.bindPort; + + // Create virtual instances. + GrasscutterTest.account = new VirtualAccount(); + GrasscutterTest.gameSession = new VirtualGameSession(); + } + + @Test + @DisplayName("HTTP server check") + public void checkHttpServer() { + // Create a request. + var request = new Request.Builder().url(BaseServerTest.http("")).build(); + + // Perform the request. + try (var response = GrasscutterTest.httpClient.newCall(request).execute()) { + // Check the response. + Assertions.assertTrue(response.isSuccessful()); + } catch (IOException exception) { + throw new AssertionError(exception); + } + } +} diff --git a/src/test/java/io/grasscutter/tests/LoginTest.java b/src/test/java/io/grasscutter/tests/LoginTest.java new file mode 100644 index 000000000..2fa1edfd4 --- /dev/null +++ b/src/test/java/io/grasscutter/tests/LoginTest.java @@ -0,0 +1,67 @@ +package io.grasscutter.tests; + +import emu.grasscutter.GameConstants; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.*; +import emu.grasscutter.server.game.session.GameSessionManager; +import io.grasscutter.*; +import kcp.highway.*; +import lombok.Getter; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; + +@TestMethodOrder(OrderAnnotation.class) +public final class LoginTest { + @Getter private static final Ukcp KCP = new Ukcp( + null, null, null, + new ChannelConfig(), null); + + @Test + @Order(1) + @DisplayName("Connect to server") + public void connectToServer() { + var session = GrasscutterTest.getGameSession(); + + // Register the session. + GameSessionManager.getSessions().put(KCP, session); + + // Try connecting to the server. + session.exchangeToken(); + Assertions.assertTrue(session.waitForPacket( + PacketOpcodes.GetPlayerTokenRsp, 5)); + } + + @Test + @Order(2) + @DisplayName("Login to server") + public void loginToServer() { + var account = GrasscutterTest.getAccount(); + var session = GrasscutterTest.getGameSession(); + + // Wait for the login response. + TestUtils.waitFor(session::useSecretKey); + + // Send the login packet. + session.receive( + PacketOpcodes.PlayerLoginReq, + PlayerLoginReqOuterClass.PlayerLoginReq.newBuilder() + .setToken(account.getToken()) + .build() + ); + // Wait for the login response. + Assertions.assertTrue(session.waitForPacket( + PacketOpcodes.PlayerLoginRsp, 5)); + + // Send the born data request. + session.receive( + PacketOpcodes.SetPlayerBornDataReq, + SetPlayerBornDataReqOuterClass.SetPlayerBornDataReq.newBuilder() + .setAvatarId(GameConstants.MAIN_CHARACTER_FEMALE) + .setNickName("Virtual Player") + .build() + ); + // Wait for the born data response. + Assertions.assertTrue(session.waitForPacket( + PacketOpcodes.SetPlayerBornDataRsp, 5)); + } +} diff --git a/src/test/java/io/grasscutter/virtual/VirtualAccount.java b/src/test/java/io/grasscutter/virtual/VirtualAccount.java new file mode 100644 index 000000000..f3b2a6177 --- /dev/null +++ b/src/test/java/io/grasscutter/virtual/VirtualAccount.java @@ -0,0 +1,23 @@ +package io.grasscutter.virtual; + +import emu.grasscutter.game.Account; + +import java.util.Locale; + +@SuppressWarnings("deprecation") +public final class VirtualAccount extends Account { + public VirtualAccount() { + super(); + + this.setId("virtual_account"); + this.setUsername("virtual_account"); + this.setPassword("virtual_account"); + + this.setReservedPlayerUid(10001); + this.setEmail("virtual_account@grasscutter.io"); + this.setLocale(Locale.US); + + this.generateSessionKey(); + this.generateLoginToken(); + } +} diff --git a/src/test/java/io/grasscutter/virtual/VirtualGameSession.java b/src/test/java/io/grasscutter/virtual/VirtualGameSession.java new file mode 100644 index 000000000..ac199db4b --- /dev/null +++ b/src/test/java/io/grasscutter/virtual/VirtualGameSession.java @@ -0,0 +1,142 @@ +package io.grasscutter.virtual; + +import com.google.protobuf.GeneratedMessageV3; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.net.packet.*; +import emu.grasscutter.net.proto.GetPlayerTokenReqOuterClass.GetPlayerTokenReq; +import emu.grasscutter.net.proto.PacketHeadOuterClass.PacketHead; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.utils.Crypto; +import io.grasscutter.GrasscutterTest; +import io.netty.buffer.Unpooled; +import org.slf4j.*; + +import java.util.*; +import java.util.concurrent.*; +import java.util.function.Function; + +public final class VirtualGameSession extends GameSession { + private static final Logger logger = LoggerFactory.getLogger("Game Session"); + + private final Map>> listeners = new HashMap<>(); + + public VirtualGameSession() { + super(Grasscutter.getGameServer()); + + this.setAccount(GrasscutterTest.getAccount()); + this.onConnected(new VirtualKcpTunnel()); + } + + /** + * Performs an exchange with the server for the player's token. + */ + public void exchangeToken() { + var account = GrasscutterTest.getAccount(); + + this.receive( + PacketOpcodes.GetPlayerTokenReq, + GetPlayerTokenReq.newBuilder() + .setAccountUid(account.getId()) + .setAccountToken(account.getToken()) + .build() + ); + } + + /** + * Registers a listener for a packet. + * + * @param packetId The packet's ID. + * @param listener The listener to register. + */ + public void addPacketListener(int packetId, Function listener) { + var listeners = this.listeners.computeIfAbsent( + packetId, k -> new HashSet<>()); + listeners.add(listener); + } + + /** + * Waits for a packet to be received. + * + * @param packetId The packet's ID. + * @param timeout The timeout in milliseconds. + */ + public boolean waitForPacket(int packetId, int timeout) { + var promise = new CompletableFuture(); + this.addPacketListener(packetId, data -> { + promise.complete(data); + return false; + }); + + try { + promise.get(timeout, TimeUnit.SECONDS); + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public synchronized void setPlayer(Player player) { + var newPlayer = new VirtualPlayer(); + + GrasscutterTest.setPlayer(newPlayer); + super.setPlayer(newPlayer); + } + + /** + * Receives a packet from the client. + * + * @param packetId The packet's ID. + * @param message The packet to receive. + */ + public void receive(int packetId, GeneratedMessageV3 message) { + // Craft a packet header. + var header = PacketHead.newBuilder() + .setSentMs(System.currentTimeMillis()) + .build(); + // Serialize the message. + var headerBytes = header.toByteArray(); + var messageBytes = message.toByteArray(); + + // Wrap the message into a packet. + var packet = Unpooled.buffer(12); + packet.writeShort(17767); // Packet header. + packet.writeShort(packetId); // Packet "opcode" or ID. + packet.writeShort(headerBytes.length); // Packet head length. + packet.writeInt(messageBytes.length); // Packet body length. + packet.writeBytes(headerBytes); // Packet head. + packet.writeBytes(messageBytes); // Packet body. + packet.writeShort(-30293); // Packet footer. + + // Serialize the packet. + var data = packet.array(); + // Encrypt the packet if specified. + Crypto.xor(data, this.useSecretKey() ? + Crypto.ENCRYPT_KEY : Crypto.DISPATCH_KEY); + + // Dispatch the message to the server. + GrasscutterTest.getExecutor() + .submit(() -> this.onMessage(data)); + } + + @Override + public void send(BasePacket packet) { + // Invoke packet handlers. + var listeners = this.listeners.get(packet.getOpcode()); + if (listeners != null) { + var copy = new HashSet<>(listeners); + for (var listener : copy) { + if (listener.apply(packet.getData())) { + listeners.remove(listener); + } + } + } + + // Log the received packet. + logger.info("Received packet {} ({}) of length {} (header is {}).", + PacketOpcodesUtils.getOpcodeName(packet.getOpcode()), packet.getOpcode(), + packet.getData() == null ? "null" : packet.getData().length, + packet.getHeader() == null ? "null" : packet.getHeader().length); + } +} diff --git a/src/test/java/io/grasscutter/virtual/VirtualKcpTunnel.java b/src/test/java/io/grasscutter/virtual/VirtualKcpTunnel.java new file mode 100644 index 000000000..a162c8228 --- /dev/null +++ b/src/test/java/io/grasscutter/virtual/VirtualKcpTunnel.java @@ -0,0 +1,21 @@ +package io.grasscutter.virtual; + +import emu.grasscutter.net.KcpTunnel; +import java.net.InetSocketAddress; + +public final class VirtualKcpTunnel implements KcpTunnel { + @Override + public InetSocketAddress getAddress() { + return new InetSocketAddress(1000); + } + + @Override + public void writeData(byte[] bytes) { + throw new UnsupportedOperationException("Cannot write to a virtual KCP tunnel"); + } + + @Override + public void close() { + System.exit(0); + } +} diff --git a/src/test/java/io/grasscutter/virtual/VirtualPlayer.java b/src/test/java/io/grasscutter/virtual/VirtualPlayer.java new file mode 100644 index 000000000..1b8ce8406 --- /dev/null +++ b/src/test/java/io/grasscutter/virtual/VirtualPlayer.java @@ -0,0 +1,13 @@ +package io.grasscutter.virtual; + +import emu.grasscutter.game.player.Player; +import io.grasscutter.GrasscutterTest; +import io.grasscutter.tests.BaseServerTest; + +public final class VirtualPlayer extends Player { + public VirtualPlayer() { + super(GrasscutterTest.getGameSession()); + + this.setAccount(GrasscutterTest.getAccount()); + } +}