Implement basic testing (login & HTTP status)

This commit is contained in:
KingRainbow44 2023-09-09 18:10:44 -04:00
parent 5b5ec9b6b4
commit 2efa022d83
No known key found for this signature in database
GPG Key ID: FC2CB64B00D257BE
13 changed files with 404 additions and 98 deletions

View File

@ -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.

View File

@ -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;
}

View File

@ -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) {

View File

@ -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

View File

@ -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()) {

View File

@ -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;
}

View File

@ -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<Boolean> condition) {
while (!condition.invoke()) {
Utils.sleep(100);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -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<Integer, Set<Function<byte[], Boolean>>> 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<byte[], Boolean> 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<byte[]>();
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);
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}