mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-07-03 13:33:31 +00:00
Compare commits
9 Commits
developmen
...
hyper-opti
Author | SHA1 | Date | |
---|---|---|---|
dad7821e26 | |||
539fa16160 | |||
3d2e0d0451 | |||
a90a81c705 | |||
fb215e06cd | |||
23aff95a2e | |||
3c3adea406 | |||
2efa022d83 | |||
5b5ec9b6b4 |
@ -1,8 +1,5 @@
|
||||
package emu.grasscutter;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.SERVER;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
import ch.qos.logback.classic.*;
|
||||
import emu.grasscutter.auth.*;
|
||||
import emu.grasscutter.command.*;
|
||||
@ -21,16 +18,20 @@ import emu.grasscutter.tools.Tools;
|
||||
import emu.grasscutter.utils.*;
|
||||
import emu.grasscutter.utils.lang.Language;
|
||||
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||
import java.io.*;
|
||||
import java.util.Calendar;
|
||||
import java.util.concurrent.*;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.*;
|
||||
import org.jline.reader.*;
|
||||
import org.jline.terminal.*;
|
||||
import org.reflections.Reflections;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.*;
|
||||
import java.util.Calendar;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.SERVER;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
public final class Grasscutter {
|
||||
public static final File configFile = new File("./config.json");
|
||||
public static final Reflections reflector = new Reflections("emu.grasscutter");
|
||||
@ -181,12 +182,18 @@ public final class Grasscutter {
|
||||
// Hook into shutdown event.
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown));
|
||||
|
||||
// Start database heartbeat.
|
||||
Database.startSaveThread();
|
||||
|
||||
// Open console.
|
||||
Grasscutter.startConsole();
|
||||
}
|
||||
|
||||
/** Server shutdown event. */
|
||||
private static void onShutdown() {
|
||||
// Save all data.
|
||||
Database.saveAll();
|
||||
|
||||
// Disable all plugins.
|
||||
if (pluginManager != null) pluginManager.disablePlugins();
|
||||
// Shutdown the game server.
|
||||
@ -196,14 +203,14 @@ public final class Grasscutter {
|
||||
// Wait for Grasscutter's thread pool to finish.
|
||||
var executor = Grasscutter.getThreadPool();
|
||||
executor.shutdown();
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
// Wait for database operations to finish.
|
||||
var dbExecutor = DatabaseHelper.getEventExecutor();
|
||||
dbExecutor.shutdown();
|
||||
if (!dbExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
if (!dbExecutor.awaitTermination(2, TimeUnit.MINUTES)) {
|
||||
dbExecutor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
|
85
src/main/java/emu/grasscutter/database/Database.java
Normal file
85
src/main/java/emu/grasscutter/database/Database.java
Normal file
@ -0,0 +1,85 @@
|
||||
package emu.grasscutter.database;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.utils.objects.DatabaseObject;
|
||||
import org.slf4j.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* Complicated manager of the MongoDB database.
|
||||
* Handles caching, data operations, and more.
|
||||
*/
|
||||
public interface Database {
|
||||
Logger logger = LoggerFactory.getLogger("Database");
|
||||
List<DatabaseObject<?>> objects = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* Queues an object to be saved.
|
||||
*
|
||||
* @param object The object to save.
|
||||
*/
|
||||
static void save(DatabaseObject<?> object) {
|
||||
if (object.saveImmediately()) {
|
||||
object.save();
|
||||
} else {
|
||||
objects.add(object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bulk save of all deferred objects.
|
||||
*/
|
||||
static void saveAll() {
|
||||
var size = objects.size();
|
||||
Database.saveAll(objects);
|
||||
|
||||
logger.debug("Performed auto save on {} objects.", size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bulk save of all deferred objects.
|
||||
*
|
||||
* @param objects The objects to save.
|
||||
*/
|
||||
static void saveAll(List<? extends DatabaseObject<?>> objects) {
|
||||
// Sort all objects into their respective databases.
|
||||
var gameObjects = objects.stream()
|
||||
.filter(DatabaseObject::isGameObject)
|
||||
.toList();
|
||||
var accountObjects = objects.stream()
|
||||
.filter(o -> !o.isGameObject())
|
||||
.toList();
|
||||
|
||||
// Clear the collective list.
|
||||
objects.clear();
|
||||
|
||||
// Save all objects.
|
||||
var executor = DatabaseHelper.getEventExecutor();
|
||||
if (Grasscutter.getRunMode() != Grasscutter.ServerRunMode.DISPATCH_ONLY) {
|
||||
executor.submit(() -> {
|
||||
DatabaseManager.getGameDatastore().save(gameObjects);
|
||||
});
|
||||
}
|
||||
if (Grasscutter.getRunMode() != Grasscutter.ServerRunMode.GAME_ONLY) {
|
||||
executor.submit(() -> {
|
||||
DatabaseManager.getAccountDatastore().save(accountObjects);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the auto-save thread.
|
||||
* Runs every 5 minutes.
|
||||
*/
|
||||
static void startSaveThread() {
|
||||
var timer = new Timer();
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Database.saveAll();
|
||||
}
|
||||
}, 0, 1000 * 60 * 5);
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -11,15 +11,17 @@ import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass;
|
||||
import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify;
|
||||
import emu.grasscutter.utils.JsonUtils;
|
||||
import java.util.*;
|
||||
import emu.grasscutter.utils.objects.DatabaseObject;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Entity("activities")
|
||||
@Data
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Builder(builderMethodName = "of")
|
||||
public class PlayerActivityData {
|
||||
public class PlayerActivityData implements DatabaseObject<PlayerActivityData> {
|
||||
@Id String id;
|
||||
int uid;
|
||||
int activityId;
|
||||
@ -34,8 +36,25 @@ public class PlayerActivityData {
|
||||
return DatabaseHelper.getPlayerActivityData(player.getUid(), activityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save() {
|
||||
DatabaseHelper.savePlayerActivityData(this);
|
||||
this.deferSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
*
|
||||
* @param immediate If true, this will be a {@link DatabaseObject#save()} call instead of a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save(boolean immediate) {
|
||||
if (immediate) {
|
||||
DatabaseObject.super.save();
|
||||
} else {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void addWatcherProgress(int watcherId) {
|
||||
|
@ -1,7 +1,5 @@
|
||||
package emu.grasscutter.game.avatar;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
|
||||
import dev.morphia.annotations.*;
|
||||
import emu.grasscutter.GameConstants;
|
||||
import emu.grasscutter.data.GameData;
|
||||
@ -15,7 +13,6 @@ import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData.InherentProudSkil
|
||||
import emu.grasscutter.data.excels.reliquary.*;
|
||||
import emu.grasscutter.data.excels.trial.TrialAvatarTemplateData;
|
||||
import emu.grasscutter.data.excels.weapon.*;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.entity.*;
|
||||
import emu.grasscutter.game.inventory.*;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
@ -31,15 +28,19 @@ import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGra
|
||||
import emu.grasscutter.net.proto.TrialAvatarInfoOuterClass.TrialAvatarInfo;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.helpers.ProtoHelper;
|
||||
import emu.grasscutter.utils.objects.DatabaseObject;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.*;
|
||||
import lombok.*;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import javax.annotation.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
|
||||
@Entity(value = "avatars", useDiscriminator = false)
|
||||
public class Avatar {
|
||||
public class Avatar implements DatabaseObject<Avatar> {
|
||||
@Transient @Getter private final Int2ObjectMap<GameItem> equips;
|
||||
@Transient @Getter private final Int2FloatOpenHashMap fightProperties;
|
||||
@Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides;
|
||||
@ -989,8 +990,25 @@ public class Avatar {
|
||||
return entity != null ? entity.getId() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save() {
|
||||
DatabaseHelper.saveAvatar(this);
|
||||
this.deferSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
*
|
||||
* @param immediate If true, this will be a {@link DatabaseObject#save()} call instead of a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save(boolean immediate) {
|
||||
if (immediate) {
|
||||
DatabaseObject.super.save();
|
||||
} else {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
public AvatarInfo toProto() {
|
||||
|
@ -60,7 +60,7 @@ public class AvatarStorage extends BasePlayerManager implements Iterable<Avatar>
|
||||
this.avatars.put(avatar.getAvatarId(), avatar);
|
||||
this.avatarsGuid.put(avatar.getGuid(), avatar);
|
||||
|
||||
avatar.save();
|
||||
avatar.save(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -165,7 +165,7 @@ public class AvatarStorage extends BasePlayerManager implements Iterable<Avatar>
|
||||
if ((avatar.getAvatarId() == 10000007) || (avatar.getAvatarId() == 10000005)) {
|
||||
avatar.setSkillDepot(skillDepot);
|
||||
avatar.setSkillDepotData(skillDepot);
|
||||
avatar.save();
|
||||
avatar.save(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -20,13 +20,14 @@ import emu.grasscutter.net.proto.ReliquaryOuterClass.Reliquary;
|
||||
import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo;
|
||||
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
|
||||
import emu.grasscutter.net.proto.WeaponOuterClass.Weapon;
|
||||
import emu.grasscutter.utils.objects.WeightedList;
|
||||
import java.util.*;
|
||||
import emu.grasscutter.utils.objects.*;
|
||||
import lombok.*;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Entity(value = "items", useDiscriminator = false)
|
||||
public class GameItem {
|
||||
public class GameItem implements DatabaseObject<GameItem> {
|
||||
@Id private ObjectId id;
|
||||
@Indexed private int ownerId;
|
||||
@Getter @Setter private int itemId;
|
||||
@ -261,14 +262,36 @@ public class GameItem {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save() {
|
||||
if (this.count > 0 && this.ownerId > 0) {
|
||||
DatabaseHelper.saveItem(this);
|
||||
} else if (this.getObjectId() != null) {
|
||||
this.deferSave();
|
||||
} else {
|
||||
DatabaseHelper.deleteItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
*
|
||||
* @param immediate If true, this will be a {@link DatabaseObject#save()} call instead of a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save(boolean immediate) {
|
||||
if (this.count < 0 || this.ownerId <= 0) {
|
||||
DatabaseHelper.deleteItem(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
DatabaseObject.super.save();
|
||||
} else {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
public SceneWeaponInfo createSceneWeaponInfo() {
|
||||
var weaponInfo =
|
||||
SceneWeaponInfo.newBuilder()
|
||||
|
@ -1,7 +1,5 @@
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
@ -18,10 +16,13 @@ import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import it.unimi.dsi.fastutil.longs.*;
|
||||
import java.util.*;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.val;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS;
|
||||
|
||||
public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
private final Long2ObjectMap<GameItem> store;
|
||||
private final Int2ObjectMap<InventoryTab> inventoryTypes;
|
||||
@ -178,7 +179,7 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
changedItems.add(result);
|
||||
}
|
||||
}
|
||||
if (changedItems.size() == 0) {
|
||||
if (changedItems.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (reason != null) {
|
||||
@ -298,8 +299,7 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
|
||||
// Add
|
||||
switch (type) {
|
||||
case ITEM_WEAPON:
|
||||
case ITEM_RELIQUARY:
|
||||
case ITEM_WEAPON, ITEM_RELIQUARY -> {
|
||||
if (tab.getSize() >= tab.getMaxCapacity()) {
|
||||
return null;
|
||||
}
|
||||
@ -310,23 +310,23 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
// Set ownership and save to db
|
||||
item.save();
|
||||
return item;
|
||||
case ITEM_VIRTUAL:
|
||||
}
|
||||
case ITEM_VIRTUAL -> {
|
||||
// Handle
|
||||
this.addVirtualItem(item.getItemId(), item.getCount());
|
||||
return item;
|
||||
default:
|
||||
}
|
||||
default -> {
|
||||
switch (item.getItemData().getMaterialType()) {
|
||||
case MATERIAL_AVATAR:
|
||||
case MATERIAL_FLYCLOAK:
|
||||
case MATERIAL_COSTUME:
|
||||
case MATERIAL_NAMECARD:
|
||||
case MATERIAL_AVATAR, MATERIAL_FLYCLOAK, MATERIAL_COSTUME, MATERIAL_NAMECARD -> {
|
||||
Grasscutter.getLogger()
|
||||
.warn(
|
||||
"Attempted to add a "
|
||||
+ item.getItemData().getMaterialType().name()
|
||||
+ " to inventory, but item definition lacks isUseOnGain. This indicates a Resources error.");
|
||||
return null;
|
||||
default:
|
||||
}
|
||||
default -> {
|
||||
if (tab == null) {
|
||||
return null;
|
||||
}
|
||||
@ -350,7 +350,9 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
existingItem.save();
|
||||
return existingItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
@ -55,7 +55,7 @@ import emu.grasscutter.server.game.GameSession.SessionState;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.*;
|
||||
import emu.grasscutter.utils.helpers.DateHelper;
|
||||
import emu.grasscutter.utils.objects.FieldFetch;
|
||||
import emu.grasscutter.utils.objects.*;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import lombok.*;
|
||||
|
||||
@ -66,7 +66,7 @@ import java.util.concurrent.*;
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
|
||||
@Entity(value = "players", useDiscriminator = false)
|
||||
public class Player implements PlayerHook, FieldFetch {
|
||||
public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
@Id private int id;
|
||||
@Indexed(options = @IndexOptions(unique = true))
|
||||
@Getter private String accountId;
|
||||
@ -261,6 +261,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<>();
|
||||
@ -275,8 +276,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);
|
||||
@ -300,19 +303,6 @@ public class Player implements PlayerHook, FieldFetch {
|
||||
this.applyStartingSceneTags();
|
||||
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
|
||||
@ -1340,8 +1330,25 @@ public class Player implements PlayerHook, FieldFetch {
|
||||
this.getTeamManager().setPlayer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save() {
|
||||
DatabaseHelper.savePlayer(this);
|
||||
this.deferSave();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
*
|
||||
* @param immediate If true, this will be a {@link DatabaseObject#save()} call instead of a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save(boolean immediate) {
|
||||
if (immediate) {
|
||||
DatabaseObject.super.save();
|
||||
} else {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
// Called from tokenrsp
|
||||
@ -1512,20 +1519,19 @@ public class Player implements PlayerHook, FieldFetch {
|
||||
this.getProfile().syncWithCharacter(this);
|
||||
|
||||
this.getCoopRequests().clear();
|
||||
this.getEnterHomeRequests().values().forEach(req -> this.expireEnterHomeRequest(req, true));
|
||||
this.getEnterHomeRequests().values()
|
||||
.forEach(req -> this.expireEnterHomeRequest(req, true));
|
||||
this.getEnterHomeRequests().clear();
|
||||
|
||||
// Save to db
|
||||
this.save();
|
||||
this.save(true);
|
||||
this.getTeamManager().saveAvatars();
|
||||
this.getFriendsList().save();
|
||||
|
||||
// Call quit event.
|
||||
PlayerQuitEvent event = new PlayerQuitEvent(this);
|
||||
event.call();
|
||||
new PlayerQuitEvent(this).call();
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
Grasscutter.getLogger().warn("Player (UID {}) save failure", getUid());
|
||||
Grasscutter.getLogger().warn("Player (UID {}) failed to save.", this.getUid(), e);
|
||||
} finally {
|
||||
removeFromServer();
|
||||
}
|
||||
@ -1533,9 +1539,10 @@ public class Player implements PlayerHook, FieldFetch {
|
||||
|
||||
public void removeFromServer() {
|
||||
// Remove from server.
|
||||
//Note: DON'T DELETE BY UID,BECAUSE THERE ARE MULTIPLE SAME UID PLAYERS WHEN DUPLICATED LOGIN!
|
||||
//so I decide to delete by object rather than uid
|
||||
getServer().getPlayers().values().removeIf(player1 -> player1 == this);
|
||||
// Note: DON'T DELETE BY UID, BECAUSE THERE ARE MULTIPLE SAME UID PLAYERS WHEN DUPLICATED LOGIN!
|
||||
//s o I decide to delete by object rather than uid
|
||||
this.getServer().getPlayers().values()
|
||||
.removeIf(player1 -> player1 == this);
|
||||
}
|
||||
|
||||
public int getLegendaryKey() {
|
||||
|
@ -4,6 +4,7 @@ import dev.morphia.annotations.*;
|
||||
import emu.grasscutter.*;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData;
|
||||
import emu.grasscutter.database.Database;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.entity.*;
|
||||
import emu.grasscutter.game.props.*;
|
||||
@ -405,7 +406,7 @@ public final class TeamManager extends BasePlayerDataManager {
|
||||
// Unload removed entities
|
||||
for (var entity : existingAvatars.values()) {
|
||||
this.getPlayer().getScene().removeEntity(entity);
|
||||
entity.getAvatar().save();
|
||||
entity.getAvatar().save(true);
|
||||
}
|
||||
|
||||
// Set new selected character index
|
||||
@ -963,11 +964,13 @@ public final class TeamManager extends BasePlayerDataManager {
|
||||
return respawnPoint.get().getPointData().getTranPos();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bulk save operation on all avatars.
|
||||
*/
|
||||
public void saveAvatars() {
|
||||
// Save all avatars from active team
|
||||
for (EntityAvatar entity : this.getActiveTeam()) {
|
||||
entity.getAvatar().save();
|
||||
}
|
||||
Database.saveAll(this.getActiveTeam().stream()
|
||||
.map(EntityAvatar::getAvatar)
|
||||
.toList());
|
||||
}
|
||||
|
||||
public void onPlayerLogin() { // Hack for now to fix resonances on login
|
||||
|
@ -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;
|
||||
@ -569,7 +570,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()) {
|
||||
|
@ -1,7 +1,5 @@
|
||||
package emu.grasscutter.game.world;
|
||||
|
||||
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.dungeon.DungeonData;
|
||||
import emu.grasscutter.game.entity.*;
|
||||
@ -20,10 +18,13 @@ import emu.grasscutter.server.game.GameServer;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.ConversionUtils;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import java.util.*;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
|
||||
|
||||
public class World implements Iterable<Player> {
|
||||
@Getter private final GameServer server;
|
||||
@Getter private Player host;
|
||||
@ -266,7 +267,7 @@ public class World implements Iterable<Player> {
|
||||
scene.removePlayer(player);
|
||||
|
||||
// Info packet for other players
|
||||
if (this.getPlayers().size() > 0) {
|
||||
if (!this.getPlayers().isEmpty()) {
|
||||
this.updatePlayerInfos(player);
|
||||
}
|
||||
|
||||
|
22
src/main/java/emu/grasscutter/net/KcpChannel.java
Normal file
22
src/main/java/emu/grasscutter/net/KcpChannel.java
Normal file
@ -0,0 +1,22 @@
|
||||
package emu.grasscutter.net;
|
||||
|
||||
public interface KcpChannel {
|
||||
/**
|
||||
* Event fired when the client connects.
|
||||
*
|
||||
* @param tunnel The tunnel.
|
||||
*/
|
||||
void onConnected(KcpTunnel tunnel);
|
||||
|
||||
/**
|
||||
* Event fired when the client disconnects.
|
||||
*/
|
||||
void onDisconnected();
|
||||
|
||||
/**
|
||||
* Event fired when data is received from the client.
|
||||
*
|
||||
* @param bytes The data received.
|
||||
*/
|
||||
void onMessage(byte[] bytes);
|
||||
}
|
22
src/main/java/emu/grasscutter/net/KcpTunnel.java
Normal file
22
src/main/java/emu/grasscutter/net/KcpTunnel.java
Normal file
@ -0,0 +1,22 @@
|
||||
package emu.grasscutter.net;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public interface KcpTunnel {
|
||||
/**
|
||||
* @return The address of the client.
|
||||
*/
|
||||
InetSocketAddress getAddress();
|
||||
|
||||
/**
|
||||
* Sends bytes to the client.
|
||||
*
|
||||
* @param bytes The bytes to send.
|
||||
*/
|
||||
void writeData(byte[] bytes);
|
||||
|
||||
/**
|
||||
* Closes the connection.
|
||||
*/
|
||||
void close();
|
||||
}
|
@ -1,55 +1,49 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
import emu.grasscutter.*;
|
||||
import emu.grasscutter.Grasscutter.ServerRunMode;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.game.battlepass.BattlePassSystem;
|
||||
import emu.grasscutter.game.chat.ChatSystem;
|
||||
import emu.grasscutter.game.chat.ChatSystemHandler;
|
||||
import emu.grasscutter.game.chat.*;
|
||||
import emu.grasscutter.game.combine.CombineManger;
|
||||
import emu.grasscutter.game.drop.DropSystem;
|
||||
import emu.grasscutter.game.drop.DropSystemLegacy;
|
||||
import emu.grasscutter.game.drop.*;
|
||||
import emu.grasscutter.game.dungeons.DungeonSystem;
|
||||
import emu.grasscutter.game.expedition.ExpeditionSystem;
|
||||
import emu.grasscutter.game.gacha.GachaSystem;
|
||||
import emu.grasscutter.game.home.HomeWorld;
|
||||
import emu.grasscutter.game.home.HomeWorldMPSystem;
|
||||
import emu.grasscutter.game.managers.cooking.CookingCompoundManager;
|
||||
import emu.grasscutter.game.managers.cooking.CookingManager;
|
||||
import emu.grasscutter.game.home.*;
|
||||
import emu.grasscutter.game.managers.cooking.*;
|
||||
import emu.grasscutter.game.managers.energy.EnergyManager;
|
||||
import emu.grasscutter.game.managers.stamina.StaminaManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.QuestSystem;
|
||||
import emu.grasscutter.game.shop.ShopSystem;
|
||||
import emu.grasscutter.game.systems.AnnouncementSystem;
|
||||
import emu.grasscutter.game.systems.InventorySystem;
|
||||
import emu.grasscutter.game.systems.MultiplayerSystem;
|
||||
import emu.grasscutter.game.systems.*;
|
||||
import emu.grasscutter.game.talk.TalkSystem;
|
||||
import emu.grasscutter.game.tower.TowerSystem;
|
||||
import emu.grasscutter.game.world.World;
|
||||
import emu.grasscutter.game.world.WorldDataSystem;
|
||||
import emu.grasscutter.game.world.*;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
|
||||
import emu.grasscutter.server.dispatch.DispatchClient;
|
||||
import emu.grasscutter.server.event.game.ServerTickEvent;
|
||||
import emu.grasscutter.server.event.internal.ServerStartEvent;
|
||||
import emu.grasscutter.server.event.internal.ServerStopEvent;
|
||||
import emu.grasscutter.server.event.internal.*;
|
||||
import emu.grasscutter.server.event.types.ServerEvent;
|
||||
import emu.grasscutter.server.scheduler.ServerTaskScheduler;
|
||||
import emu.grasscutter.task.TaskMap;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import kcp.highway.*;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
import emu.grasscutter.server.game.session.GameSessionManager;
|
||||
|
||||
import java.net.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import kcp.highway.*;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
@Getter
|
||||
public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
@ -60,6 +54,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
private final Set<World> worlds;
|
||||
private final Int2ObjectMap<HomeWorld> homeWorlds;
|
||||
|
||||
@Getter private boolean started = false;
|
||||
@Setter private DispatchClient dispatchClient;
|
||||
|
||||
// Server systems
|
||||
@ -140,7 +135,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
channelConfig.setUseConvChannel(true);
|
||||
channelConfig.setAckNoDelay(false);
|
||||
|
||||
this.init(GameSessionManager.getListener(), channelConfig, address);
|
||||
this.init(GameSessionManager.getInstance(), channelConfig, address);
|
||||
|
||||
EnergyManager.initialize();
|
||||
StaminaManager.initialize();
|
||||
@ -347,6 +342,8 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
.info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort()));
|
||||
ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now());
|
||||
event.call();
|
||||
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
public void onServerShutdown() {
|
||||
@ -361,10 +358,10 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
this.stop(); // Stop the server.
|
||||
|
||||
try {
|
||||
var threadPool = GameSessionManager.getLogicThread();
|
||||
var threadPool = GameSessionManager.getExecutor();
|
||||
|
||||
// Shutdown network thread.
|
||||
threadPool.shutdownGracefully();
|
||||
threadPool.shutdown();
|
||||
// Wait for the network thread to finish.
|
||||
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
Grasscutter.getLogger().error("Logic thread did not terminate!");
|
||||
|
@ -1,7 +1,5 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_INFO;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.Grasscutter.ServerDebugMode;
|
||||
import emu.grasscutter.net.packet.*;
|
||||
@ -9,6 +7,8 @@ import emu.grasscutter.server.event.game.ReceivePacketEvent;
|
||||
import emu.grasscutter.server.game.GameSession.SessionState;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_INFO;
|
||||
|
||||
public final class GameServerPacketHandler {
|
||||
private final Int2ObjectMap<PacketHandler> handlers;
|
||||
|
||||
@ -76,13 +76,11 @@ public final class GameServerPacketHandler {
|
||||
}
|
||||
|
||||
// Invoke event.
|
||||
ReceivePacketEvent event = new ReceivePacketEvent(session, opcode, payload);
|
||||
event.call();
|
||||
if (!event.isCanceled()) // If event is not canceled, continue.
|
||||
handler.handle(session, header, event.getPacketData());
|
||||
var event = new ReceivePacketEvent(session, opcode, payload);
|
||||
if (event.call()) // If event is not canceled, continue.
|
||||
handler.handle(session, header, event.getPacketData());
|
||||
} catch (Exception ex) {
|
||||
// TODO Remove this when no more needed
|
||||
ex.printStackTrace();
|
||||
Grasscutter.getLogger().warn("Unable to handle packet.", ex);
|
||||
}
|
||||
return; // Packet successfully handled
|
||||
}
|
||||
|
@ -1,24 +1,26 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.Grasscutter.ServerDebugMode;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.*;
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.server.event.game.SendPacketEvent;
|
||||
import emu.grasscutter.utils.*;
|
||||
import io.netty.buffer.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Path;
|
||||
import lombok.*;
|
||||
|
||||
public class GameSession implements GameSessionManager.KcpChannel {
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
public class GameSession implements KcpChannel {
|
||||
private final GameServer server;
|
||||
private GameSessionManager.KcpTunnel tunnel;
|
||||
private KcpTunnel tunnel;
|
||||
|
||||
@Getter @Setter private Account account;
|
||||
@Getter private Player player;
|
||||
@ -146,7 +148,7 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
if (packet.shouldEncrypt) {
|
||||
Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey);
|
||||
}
|
||||
tunnel.writeData(bytes);
|
||||
this.tunnel.writeData(bytes);
|
||||
} catch (Exception ignored) {
|
||||
Grasscutter.getLogger().debug("Unable to send packet to client.");
|
||||
}
|
||||
@ -154,13 +156,13 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected(GameSessionManager.KcpTunnel tunnel) {
|
||||
public void onConnected(KcpTunnel tunnel) {
|
||||
this.tunnel = tunnel;
|
||||
Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReceive(byte[] bytes) {
|
||||
public void onMessage(byte[] bytes) {
|
||||
// Decrypt and turn back into a packet
|
||||
Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY);
|
||||
ByteBuf packet = Unpooled.wrappedBuffer(bytes);
|
||||
@ -226,8 +228,8 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
// Handle
|
||||
getServer().getPacketHandler().handle(this, opcode, header, payload);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} catch (Exception exception) {
|
||||
Grasscutter.getLogger().warn("Unable to handle packet.", exception);
|
||||
} finally {
|
||||
// byteBuf.release(); //Needn't
|
||||
packet.release();
|
||||
@ -235,8 +237,9 @@ public class GameSession implements GameSessionManager.KcpChannel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClose() {
|
||||
public void onDisconnected() {
|
||||
setState(SessionState.INACTIVE);
|
||||
|
||||
// send disconnection pack in case of reconnection
|
||||
Grasscutter.getLogger()
|
||||
.info(translate("messages.game.disconnect", this.getAddress().toString()));
|
||||
|
@ -1,114 +0,0 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import io.netty.buffer.*;
|
||||
import io.netty.channel.DefaultEventLoop;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import kcp.highway.*;
|
||||
import lombok.Getter;
|
||||
|
||||
public class GameSessionManager {
|
||||
@Getter private static final DefaultEventLoop logicThread = new DefaultEventLoop();
|
||||
private static final ConcurrentHashMap<Ukcp, GameSession> sessions = new ConcurrentHashMap<>();
|
||||
private static final KcpListener listener =
|
||||
new KcpListener() {
|
||||
@Override
|
||||
public void onConnected(Ukcp ukcp) {
|
||||
int times = 0;
|
||||
GameServer server = Grasscutter.getGameServer();
|
||||
while (server == null) { // Waiting server to establish
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
ukcp.close();
|
||||
return;
|
||||
}
|
||||
if (times++ > 5) {
|
||||
Grasscutter.getLogger().error("Service is not available!");
|
||||
ukcp.close();
|
||||
return;
|
||||
}
|
||||
server = Grasscutter.getGameServer();
|
||||
}
|
||||
GameSession conversation = new GameSession(server);
|
||||
conversation.onConnected(
|
||||
new KcpTunnel() {
|
||||
@Override
|
||||
public InetSocketAddress getAddress() {
|
||||
return ukcp.user().getRemoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeData(byte[] bytes) {
|
||||
ByteBuf buf = Unpooled.wrappedBuffer(bytes);
|
||||
ukcp.write(buf);
|
||||
buf.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
ukcp.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSrtt() {
|
||||
return ukcp.srtt();
|
||||
}
|
||||
});
|
||||
sessions.put(ukcp, conversation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReceive(ByteBuf buf, Ukcp kcp) {
|
||||
var byteData = Utils.byteBufToArray(buf);
|
||||
logicThread.execute(
|
||||
() -> {
|
||||
try {
|
||||
var conversation = sessions.get(kcp);
|
||||
if (conversation != null) {
|
||||
conversation.handleReceive(byteData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleException(Throwable ex, Ukcp ukcp) {}
|
||||
|
||||
@Override
|
||||
public void handleClose(Ukcp ukcp) {
|
||||
GameSession conversation = sessions.get(ukcp);
|
||||
if (conversation != null) {
|
||||
conversation.handleClose();
|
||||
sessions.remove(ukcp);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public static KcpListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
public interface KcpTunnel {
|
||||
InetSocketAddress getAddress();
|
||||
|
||||
void writeData(byte[] bytes);
|
||||
|
||||
void close();
|
||||
|
||||
int getSrtt();
|
||||
}
|
||||
|
||||
interface KcpChannel {
|
||||
void onConnected(KcpTunnel tunnel);
|
||||
|
||||
void handleClose();
|
||||
|
||||
void handleReceive(byte[] bytes);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package emu.grasscutter.server.game.session;
|
||||
|
||||
import emu.grasscutter.net.KcpTunnel;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import kcp.highway.Ukcp;
|
||||
import lombok.*;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public final class GameSessionHandler implements KcpTunnel {
|
||||
@Getter private final Ukcp handle;
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getAddress() {
|
||||
return this.getHandle().user().getRemoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeData(byte[] bytes) {
|
||||
var buffer = Unpooled.wrappedBuffer(bytes);
|
||||
this.getHandle().write(buffer);
|
||||
|
||||
buffer.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.getHandle().close();
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package emu.grasscutter.server.game.session;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.server.game.*;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kcp.highway.*;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public final class GameSessionManager implements KcpListener {
|
||||
@Getter private static final GameSessionManager instance
|
||||
= new GameSessionManager();
|
||||
@Getter private static final ExecutorService executor
|
||||
= Executors.newWorkStealingPool();
|
||||
@Getter private static final Map<Ukcp, GameSession> sessions
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Waits for the game server to be ready.
|
||||
*
|
||||
* @return The game server.
|
||||
*/
|
||||
private GameServer waitForServer() {
|
||||
var server = Grasscutter.getGameServer();
|
||||
var times = 0; while (server == null || !server.isStarted()) {
|
||||
Utils.sleep(1000); // Wait 1s for the server to start.
|
||||
if (times++ > 5) {
|
||||
Grasscutter.getLogger().error("Game server has not started in a reasonable time.");
|
||||
return null;
|
||||
}
|
||||
|
||||
server = Grasscutter.getGameServer();
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected(Ukcp ukcp) {
|
||||
// Fetch the game server.
|
||||
var server = this.waitForServer();
|
||||
if (server == null) {
|
||||
ukcp.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new session.
|
||||
var session = sessions.compute(ukcp, (k, existing) -> {
|
||||
// Close an existing session.
|
||||
if (existing != null) {
|
||||
existing.close();
|
||||
}
|
||||
|
||||
return new GameSession(server);
|
||||
});
|
||||
|
||||
// Connect the session.
|
||||
session.onConnected(new GameSessionHandler(ukcp));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReceive(ByteBuf byteBuf, Ukcp ukcp) {
|
||||
// Get the session.
|
||||
var session = sessions.get(ukcp);
|
||||
if (session == null) {
|
||||
ukcp.close(); return;
|
||||
}
|
||||
|
||||
// Handle the message in a separate thread.
|
||||
var bytes = Utils.byteBufToArray(byteBuf);
|
||||
executor.submit(() -> session.onMessage(bytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleException(Throwable throwable, Ukcp ukcp) {
|
||||
Grasscutter.getLogger().error("Exception in game session.", throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClose(Ukcp ukcp) {
|
||||
var session = sessions.remove(ukcp);
|
||||
if (session != null) {
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package emu.grasscutter.utils.objects;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.Grasscutter.ServerRunMode;
|
||||
import emu.grasscutter.database.*;
|
||||
|
||||
public interface DatabaseObject<T> {
|
||||
/**
|
||||
* @return Does this object belong in the game database?
|
||||
*/
|
||||
default boolean isGameObject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Should this object be saved immediately?
|
||||
*/
|
||||
default boolean saveImmediately() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a deferred save.
|
||||
* This object will save as a group with other objects.
|
||||
*/
|
||||
default void deferSave() {
|
||||
Database.save(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to save this object to the database.
|
||||
*/
|
||||
default void save() {
|
||||
if (this.isGameObject()) {
|
||||
DatabaseManager.getGameDatastore().save(this);
|
||||
} else if (Grasscutter.getRunMode() != ServerRunMode.GAME_ONLY) {
|
||||
DatabaseManager.getAccountDatastore().save(this);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unable to store an account object while in game-only mode.");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
17
src/test/java/io/grasscutter/TestUtils.java
Normal file
17
src/test/java/io/grasscutter/TestUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
61
src/test/java/io/grasscutter/tests/BaseServerTest.java
Normal file
61
src/test/java/io/grasscutter/tests/BaseServerTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
67
src/test/java/io/grasscutter/tests/LoginTest.java
Normal file
67
src/test/java/io/grasscutter/tests/LoginTest.java
Normal 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));
|
||||
}
|
||||
}
|
23
src/test/java/io/grasscutter/virtual/VirtualAccount.java
Normal file
23
src/test/java/io/grasscutter/virtual/VirtualAccount.java
Normal 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();
|
||||
}
|
||||
}
|
142
src/test/java/io/grasscutter/virtual/VirtualGameSession.java
Normal file
142
src/test/java/io/grasscutter/virtual/VirtualGameSession.java
Normal 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);
|
||||
}
|
||||
}
|
21
src/test/java/io/grasscutter/virtual/VirtualKcpTunnel.java
Normal file
21
src/test/java/io/grasscutter/virtual/VirtualKcpTunnel.java
Normal 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);
|
||||
}
|
||||
}
|
13
src/test/java/io/grasscutter/virtual/VirtualPlayer.java
Normal file
13
src/test/java/io/grasscutter/virtual/VirtualPlayer.java
Normal 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());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user