9 Commits

60 changed files with 1014 additions and 740 deletions

View File

@ -68,7 +68,7 @@ class MlgmXyysd_Animation_Company_Proxy:
] ]
def request(self, flow: http.HTTPFlow) -> None: def request(self, flow: http.HTTPFlow) -> None:
if flow.request.pretty_host in self.LIST_DOMAINS: if flow.request.host in self.LIST_DOMAINS:
if USE_SSL: if USE_SSL:
flow.request.scheme = "https" flow.request.scheme = "https"
else: else:

View File

@ -1,8 +1,5 @@
package emu.grasscutter; package emu.grasscutter;
import static emu.grasscutter.config.Configuration.SERVER;
import static emu.grasscutter.utils.lang.Language.translate;
import ch.qos.logback.classic.*; import ch.qos.logback.classic.*;
import emu.grasscutter.auth.*; import emu.grasscutter.auth.*;
import emu.grasscutter.command.*; import emu.grasscutter.command.*;
@ -21,16 +18,20 @@ import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import emu.grasscutter.utils.lang.Language; import emu.grasscutter.utils.lang.Language;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
import java.io.*;
import java.util.Calendar;
import java.util.concurrent.*;
import javax.annotation.Nullable;
import lombok.*; import lombok.*;
import org.jline.reader.*; import org.jline.reader.*;
import org.jline.terminal.*; import org.jline.terminal.*;
import org.reflections.Reflections; import org.reflections.Reflections;
import org.slf4j.LoggerFactory; 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 final class Grasscutter {
public static final File configFile = new File("./config.json"); public static final File configFile = new File("./config.json");
public static final Reflections reflector = new Reflections("emu.grasscutter"); public static final Reflections reflector = new Reflections("emu.grasscutter");
@ -158,8 +159,6 @@ public final class Grasscutter {
// Generate handbooks. // Generate handbooks.
Tools.createGmHandbooks(false); Tools.createGmHandbooks(false);
// Generate gacha mappings.
Tools.generateGachaMappings();
} }
// Start servers. // Start servers.
@ -183,12 +182,18 @@ public final class Grasscutter {
// Hook into shutdown event. // Hook into shutdown event.
Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown)); Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown));
// Start database heartbeat.
Database.startSaveThread();
// Open console. // Open console.
Grasscutter.startConsole(); Grasscutter.startConsole();
} }
/** Server shutdown event. */ /** Server shutdown event. */
private static void onShutdown() { private static void onShutdown() {
// Save all data.
Database.saveAll();
// Disable all plugins. // Disable all plugins.
if (pluginManager != null) pluginManager.disablePlugins(); if (pluginManager != null) pluginManager.disablePlugins();
// Shutdown the game server. // Shutdown the game server.
@ -198,14 +203,14 @@ public final class Grasscutter {
// Wait for Grasscutter's thread pool to finish. // Wait for Grasscutter's thread pool to finish.
var executor = Grasscutter.getThreadPool(); var executor = Grasscutter.getThreadPool();
executor.shutdown(); executor.shutdown();
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) { if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
executor.shutdownNow(); executor.shutdownNow();
} }
// Wait for database operations to finish. // Wait for database operations to finish.
var dbExecutor = DatabaseHelper.getEventExecutor(); var dbExecutor = DatabaseHelper.getEventExecutor();
dbExecutor.shutdown(); dbExecutor.shutdown();
if (!dbExecutor.awaitTermination(5, TimeUnit.SECONDS)) { if (!dbExecutor.awaitTermination(2, TimeUnit.MINUTES)) {
dbExecutor.shutdownNow(); dbExecutor.shutdownNow();
} }
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {

View File

@ -1,6 +1,5 @@
package emu.grasscutter.command; package emu.grasscutter.command;
import emu.grasscutter.game.world.Position;
import java.util.*; import java.util.*;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.regex.*; import java.util.regex.*;
@ -55,78 +54,4 @@ public class CommandHelpers {
}); });
return args; return args;
} }
public static float parseRelative(String input, Float current) {
if (input.contains("~")) { // Relative
if (!input.equals("~")) { // Relative with offset
current += Float.parseFloat(input.replace("~", ""));
} // Else no offset, no modification
} else { // Absolute
current = Float.parseFloat(input);
}
return current;
}
public static Position parsePosition(
String inputX, String inputY, String inputZ, Position curPos, Position curRot) {
Position offset = new Position();
Position target = new Position(curPos);
if (inputX.contains("~")) { // Relative
if (!inputX.equals("~")) { // Relative with offset
target.addX(Float.parseFloat(inputX.replace("~", "")));
}
} else if (inputX.contains("^")) {
if (!inputX.equals("^")) {
offset.setX(Float.parseFloat(inputX.replace("^", "")));
}
} else { // Absolute
target.setX(Float.parseFloat(inputX));
}
if (inputY.contains("~")) { // Relative
if (!inputY.equals("~")) { // Relative with offset
target.addY(Float.parseFloat(inputY.replace("~", "")));
}
} else if (inputY.contains("^")) {
if (!inputY.equals("^")) {
offset.setY(Float.parseFloat(inputY.replace("^", "")));
}
} else { // Absolute
target.setY(Float.parseFloat(inputY));
}
if (inputZ.contains("~")) { // Relative
if (!inputZ.equals("~")) { // Relative with offset
target.addZ(Float.parseFloat(inputZ.replace("~", "")));
}
} else if (inputZ.contains("^")) {
if (!inputZ.equals("^")) {
offset.setZ(Float.parseFloat(inputZ.replace("^", "")));
}
} else { // Absolute
target.setZ(Float.parseFloat(inputZ));
}
if (!offset.equal3d(Position.ZERO)) {
return calculateOffset(target, curRot, offset);
} else {
return target;
}
}
public static Position calculateOffset(Position pos, Position rot, Position offset) {
// Degrees to radians
float angleZ = (float) Math.toRadians(rot.getY());
float angleX = (float) Math.toRadians(rot.getY() + 90);
// Calculate offset based on current position and rotation
return new Position(
pos.getX()
+ offset.getZ() * (float) Math.sin(angleZ)
+ offset.getX() * (float) Math.sin(angleX),
pos.getY() + offset.getY(),
pos.getZ()
+ offset.getZ() * (float) Math.cos(angleZ)
+ offset.getX() * (float) Math.cos(angleX));
}
} }

View File

@ -21,9 +21,9 @@ import lombok.Setter;
label = "spawn", label = "spawn",
aliases = {"drop", "s"}, aliases = {"drop", "s"},
usage = { usage = {
"<itemId> [x<amount>] [blk<blockId>] [grp<groupId>] [cfg<configId>] [<x> <y> <z>] [<rotX> <rotY> <rotZ>]", "<itemId> [x<amount>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
"<gadgetId> [x<amount>] [state<state>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] [<x> <y> <z>] [<rotX> <rotY> <rotZ>]", "<gadgetId> [x<amount>] [state<state>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
"<monsterId> [x<amount>] [lv<level>] [ai<aiId>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] [<x> <y> <z>] [<rotX> <rotY> <rotZ>]" "<monsterId> [x<amount>] [lv<level>] [ai<aiId>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>"
}, },
permission = "server.spawn", permission = "server.spawn",
permissionTargeted = "server.spawn.others") permissionTargeted = "server.spawn.others")
@ -53,23 +53,14 @@ public final class SpawnCommand implements CommandHandler {
sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
Position pos = new Position(targetPlayer.getPosition());
Position rot = new Position(targetPlayer.getRotation());
switch (args.size()) { switch (args.size()) {
case 7:
try {
rot.setX(CommandHelpers.parseRelative(args.get(4), rot.getX()));
rot.setY(CommandHelpers.parseRelative(args.get(5), rot.getY()));
rot.setZ(CommandHelpers.parseRelative(args.get(6), rot.getZ()));
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.execution.argument_error"));
} // Fallthrough
case 4: case 4:
try { try {
pos = CommandHelpers.parsePosition(args.get(1), args.get(2), args.get(3), pos, rot); float x, y, z;
x = Float.parseFloat(args.get(1));
y = Float.parseFloat(args.get(2));
z = Float.parseFloat(args.get(3));
param.pos = new Position(x, y, z);
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
CommandHandler.sendMessage( CommandHandler.sendMessage(
sender, translate(sender, "commands.execution.argument_error")); sender, translate(sender, "commands.execution.argument_error"));
@ -86,8 +77,6 @@ public final class SpawnCommand implements CommandHandler {
sendUsageMessage(sender); sendUsageMessage(sender);
return; return;
} }
param.pos = pos;
param.rot = rot;
MonsterData monsterData = GameData.getMonsterDataMap().get(param.id); MonsterData monsterData = GameData.getMonsterDataMap().get(param.id);
GadgetData gadgetData = GameData.getGadgetDataMap().get(param.id); GadgetData gadgetData = GameData.getGadgetDataMap().get(param.id);
@ -113,8 +102,12 @@ public final class SpawnCommand implements CommandHandler {
} }
double maxRadius = Math.sqrt(param.amount * 0.2 / Math.PI); double maxRadius = Math.sqrt(param.amount * 0.2 / Math.PI);
if (param.pos == null) {
param.pos = targetPlayer.getPosition();
}
for (int i = 0; i < param.amount; i++) { for (int i = 0; i < param.amount; i++) {
pos = GetRandomPositionInCircle(param.pos, maxRadius).addY(3); Position pos = GetRandomPositionInCircle(param.pos, maxRadius).addY(3);
GameEntity entity = null; GameEntity entity = null;
if (itemData != null) { if (itemData != null) {
entity = createItem(itemData, param, pos); entity = createItem(itemData, param, pos);
@ -135,12 +128,12 @@ public final class SpawnCommand implements CommandHandler {
} }
private EntityItem createItem(ItemData itemData, SpawnParameters param, Position pos) { private EntityItem createItem(ItemData itemData, SpawnParameters param, Position pos) {
return new EntityItem(param.scene, null, itemData, pos, param.rot, 1, true); return new EntityItem(param.scene, null, itemData, pos, 1, true);
} }
private EntityMonster createMonster( private EntityMonster createMonster(
MonsterData monsterData, SpawnParameters param, Position pos) { MonsterData monsterData, SpawnParameters param, Position pos) {
var entity = new EntityMonster(param.scene, monsterData, pos, param.rot, param.lvl); var entity = new EntityMonster(param.scene, monsterData, pos, param.lvl);
if (param.ai != -1) { if (param.ai != -1) {
entity.setAiId(param.ai); entity.setAiId(param.ai);
} }
@ -151,13 +144,16 @@ public final class SpawnCommand implements CommandHandler {
GadgetData gadgetData, SpawnParameters param, Position pos, Player targetPlayer) { GadgetData gadgetData, SpawnParameters param, Position pos, Player targetPlayer) {
EntityBaseGadget entity; EntityBaseGadget entity;
if (gadgetData.getType() == EntityType.Vehicle) { if (gadgetData.getType() == EntityType.Vehicle) {
entity = new EntityVehicle(param.scene, targetPlayer, param.id, 0, pos, param.rot); entity =
new EntityVehicle(
param.scene, targetPlayer, param.id, 0, pos, targetPlayer.getRotation());
} else { } else {
entity = new EntityGadget(param.scene, param.id, pos, param.rot); entity = new EntityGadget(param.scene, param.id, pos, targetPlayer.getRotation());
if (param.state != -1) { if (param.state != -1) {
((EntityGadget) entity).setState(param.state); ((EntityGadget) entity).setState(param.state);
} }
} }
return entity; return entity;
} }
@ -211,7 +207,6 @@ public final class SpawnCommand implements CommandHandler {
@Setter public int def = -1; @Setter public int def = -1;
@Setter public int ai = -1; @Setter public int ai = -1;
@Setter public Position pos = null; @Setter public Position pos = null;
@Setter public Position rot = null;
public Scene scene = null; public Scene scene = null;
} }
} }

View File

@ -16,10 +16,24 @@ import java.util.List;
permissionTargeted = "player.teleport.others") permissionTargeted = "player.teleport.others")
public final class TeleportCommand implements CommandHandler { public final class TeleportCommand implements CommandHandler {
private float parseRelative(
String input, Float current) { // TODO: Maybe this will be useful elsewhere later
if (input.contains("~")) { // Relative
if (!input.equals("~")) { // Relative with offset
current += Float.parseFloat(input.replace("~", ""));
} // Else no offset, no modification
} else { // Absolute
current = Float.parseFloat(input);
}
return current;
}
@Override @Override
public void execute(Player sender, Player targetPlayer, List<String> args) { public void execute(Player sender, Player targetPlayer, List<String> args) {
Position pos = new Position(targetPlayer.getPosition()); Position pos = targetPlayer.getPosition();
Position rot = new Position(targetPlayer.getRotation()); float x = pos.getX();
float y = pos.getY();
float z = pos.getZ();
int sceneId = targetPlayer.getSceneId(); int sceneId = targetPlayer.getSceneId();
switch (args.size()) { switch (args.size()) {
@ -32,7 +46,9 @@ public final class TeleportCommand implements CommandHandler {
} // Fallthrough } // Fallthrough
case 3: case 3:
try { try {
pos = CommandHelpers.parsePosition(args.get(0), args.get(1), args.get(2), pos, rot); x = this.parseRelative(args.get(0), x);
y = this.parseRelative(args.get(1), y);
z = this.parseRelative(args.get(2), z);
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
CommandHandler.sendMessage( CommandHandler.sendMessage(
sender, translate(sender, "commands.teleport.invalid_position")); sender, translate(sender, "commands.teleport.invalid_position"));
@ -43,10 +59,11 @@ public final class TeleportCommand implements CommandHandler {
return; return;
} }
Position target_pos = new Position(x, y, z);
boolean result = boolean result =
targetPlayer targetPlayer
.getWorld() .getWorld()
.transferPlayerToScene(targetPlayer, sceneId, TeleportType.COMMAND, pos); .transferPlayerToScene(targetPlayer, sceneId, TeleportType.COMMAND, target_pos);
if (!result) { if (!result) {
CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.exists_error")); CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.exists_error"));
@ -54,13 +71,7 @@ public final class TeleportCommand implements CommandHandler {
CommandHandler.sendMessage( CommandHandler.sendMessage(
sender, sender,
translate( translate(
sender, sender, "commands.teleport.success", targetPlayer.getNickname(), x, y, z, sceneId));
"commands.teleport.success",
targetPlayer.getNickname(),
pos.getX(),
pos.getY(),
pos.getZ(),
sceneId));
} }
} }
} }

View File

@ -1,6 +1,8 @@
package emu.grasscutter.data; package emu.grasscutter.data;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.http.handlers.GachaHandler;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import java.io.*; import java.io.*;
import java.nio.file.*; import java.nio.file.*;
@ -112,6 +114,8 @@ public class DataLoader {
} catch (Exception e) { } catch (Exception e) {
Grasscutter.getLogger().error("An error occurred while trying to check the data folder.", e); Grasscutter.getLogger().error("An error occurred while trying to check the data folder.", e);
} }
generateGachaMappings();
} }
private static void checkAndCopyData(String name) { private static void checkAndCopyData(String name) {
@ -127,4 +131,16 @@ public class DataLoader {
FileUtils.copyResource("/defaults/data/" + name, filePath.toString()); FileUtils.copyResource("/defaults/data/" + name, filePath.toString());
} }
} }
private static void generateGachaMappings() {
var path = GachaHandler.getGachaMappingsPath();
if (!Files.exists(path)) {
try {
Grasscutter.getLogger().debug("Creating default '" + path + "' data");
Tools.createGachaMappings(path);
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to create gacha mappings. \n" + exception);
}
}
}
} }

View File

@ -214,14 +214,6 @@ public final class GameData {
private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap = private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap =
new Int2ObjectOpenHashMap<>(); new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CoopChapterData> coopChapterDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CoopPointData> coopPointDataMap =
new Int2ObjectOpenHashMap<>();
@Getter @Getter
private static final Int2ObjectMap<CompoundData> compoundDataMap = new Int2ObjectOpenHashMap<>(); private static final Int2ObjectMap<CompoundData> compoundDataMap = new Int2ObjectOpenHashMap<>();

View File

@ -1,46 +0,0 @@
package emu.grasscutter.data.excels;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.data.*;
import java.util.List;
import lombok.*;
import lombok.experimental.FieldDefaults;
@ResourceType(name = "CoopChapterExcelConfigData.json")
@Getter
@Setter // TODO: remove setters next API break
@FieldDefaults(level = AccessLevel.PRIVATE)
public class CoopChapterData extends GameResource {
@Getter(onMethod_ = @Override)
int id;
int avatarId;
// int chapterNameTextMapHash;
// int coopPageTitleTextMapHash;
// int chapterSortId;
// int avatarSortId;
// String chapterIcon;
List<CoopCondition> unlockCond;
// int [] unlockCondTips;
// int openMaterialId;
// int openMaterialNum;
// String beginTimeStr;
// int confidenceValue;
// String pointGraphPath;
// Double graphXRatio;
// Double graphYRatio;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
private static class CoopCondition {
@SerializedName(
value = "_condType",
alternate = {"condType"})
String type = "COOP_COND_NONE";
@SerializedName(
value = "_args",
alternate = {"args"})
int[] args;
}
}

View File

@ -1,24 +0,0 @@
package emu.grasscutter.data.excels;
import emu.grasscutter.data.*;
import lombok.*;
import lombok.experimental.FieldDefaults;
@ResourceType(name = "CoopPointExcelConfigData.json")
@Getter
@Setter // TODO: remove setters next API break
@FieldDefaults(level = AccessLevel.PRIVATE)
public class CoopPointData extends GameResource {
@Getter(onMethod_ = @Override)
int id;
int chapterId;
String type;
int acceptQuest;
int[] postPointList;
// int pointNameTextMapHash;
// int pointDecTextMapHash;
int pointPosId;
// long photoMaleHash;
// long photoFemaleHash;
}

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

View File

@ -1,7 +1,5 @@
package emu.grasscutter.database; package emu.grasscutter.database;
import static com.mongodb.client.model.Filters.eq;
import dev.morphia.query.*; import dev.morphia.query.*;
import dev.morphia.query.experimental.filters.Filters; import dev.morphia.query.experimental.filters.Filters;
import emu.grasscutter.*; import emu.grasscutter.*;
@ -20,24 +18,19 @@ import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.GameMainQuest; import emu.grasscutter.game.quest.GameMainQuest;
import emu.grasscutter.game.world.SceneGroupInstance; import emu.grasscutter.game.world.SceneGroupInstance;
import emu.grasscutter.utils.objects.Returnable; 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.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.stream.Stream; import java.util.stream.Stream;
import javax.annotation.Nullable;
import lombok.Getter; import static com.mongodb.client.model.Filters.eq;
public final class DatabaseHelper { public final class DatabaseHelper {
@Getter @Getter
private static final ExecutorService eventExecutor = private static final ExecutorService eventExecutor =
new ThreadPoolExecutor( Executors.newFixedThreadPool(4);
6,
6,
60,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(),
FastThreadLocalThread::new,
new ThreadPoolExecutor.AbortPolicy());
/** /**
* Saves an object on the account datastore. * Saves an object on the account datastore.

View File

@ -241,8 +241,7 @@ public interface HandbookActions {
// Create the entity. // Create the entity.
for (var i = 1; i <= request.getAmount(); i++) { for (var i = 1; i <= request.getAmount(); i++) {
var entity = var entity = new EntityMonster(scene, entityData, player.getPosition(), level);
new EntityMonster(scene, entityData, player.getPosition(), player.getRotation(), level);
scene.addEntity(entity); scene.addEntity(entity);
} }

View File

@ -12,12 +12,13 @@ import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.net.proto.AchievementOuterClass.Achievement.Status; import emu.grasscutter.net.proto.AchievementOuterClass.Achievement.Status;
import emu.grasscutter.server.event.player.PlayerCompleteAchievementEvent; import emu.grasscutter.server.event.player.PlayerCompleteAchievementEvent;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import lombok.*;
import org.bson.types.ObjectId;
import javax.annotation.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntSupplier; import java.util.function.IntSupplier;
import javax.annotation.Nullable;
import lombok.*;
import org.bson.types.ObjectId;
@Entity("achievements") @Entity("achievements")
@Data @Data
@ -44,15 +45,30 @@ public class Achievements {
return 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) { public static Achievements create(int uid) {
var newAchievement = var newAchievement = blank();
Achievements.of() newAchievement.setUid(uid);
.uid(uid)
.achievementList(init())
.finishedAchievementNum(0)
.takenGoalRewardIdList(Lists.newArrayList())
.build();
newAchievement.save(); newAchievement.save();
return newAchievement; return newAchievement;
} }

View File

@ -11,15 +11,17 @@ import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass; import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass;
import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify; import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify;
import emu.grasscutter.utils.JsonUtils; import emu.grasscutter.utils.JsonUtils;
import java.util.*; import emu.grasscutter.utils.objects.DatabaseObject;
import lombok.*; import lombok.*;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import java.util.*;
@Entity("activities") @Entity("activities")
@Data @Data
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Builder(builderMethodName = "of") @Builder(builderMethodName = "of")
public class PlayerActivityData { public class PlayerActivityData implements DatabaseObject<PlayerActivityData> {
@Id String id; @Id String id;
int uid; int uid;
int activityId; int activityId;
@ -34,8 +36,25 @@ public class PlayerActivityData {
return DatabaseHelper.getPlayerActivityData(player.getUid(), activityId); 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() { 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) { public synchronized void addWatcherProgress(int watcherId) {

View File

@ -1,7 +1,5 @@
package emu.grasscutter.game.avatar; package emu.grasscutter.game.avatar;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
import dev.morphia.annotations.*; import dev.morphia.annotations.*;
import emu.grasscutter.GameConstants; import emu.grasscutter.GameConstants;
import emu.grasscutter.data.GameData; 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.reliquary.*;
import emu.grasscutter.data.excels.trial.TrialAvatarTemplateData; import emu.grasscutter.data.excels.trial.TrialAvatarTemplateData;
import emu.grasscutter.data.excels.weapon.*; import emu.grasscutter.data.excels.weapon.*;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.inventory.*; import emu.grasscutter.game.inventory.*;
import emu.grasscutter.game.player.Player; 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.net.proto.TrialAvatarInfoOuterClass.TrialAvatarInfo;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.helpers.ProtoHelper; import emu.grasscutter.utils.helpers.ProtoHelper;
import emu.grasscutter.utils.objects.DatabaseObject;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import java.util.*;
import java.util.stream.Stream;
import javax.annotation.*;
import lombok.*; import lombok.*;
import org.bson.types.ObjectId; 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) @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 Int2ObjectMap<GameItem> equips;
@Transient @Getter private final Int2FloatOpenHashMap fightProperties; @Transient @Getter private final Int2FloatOpenHashMap fightProperties;
@Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides; @Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides;
@ -989,8 +990,25 @@ public class Avatar {
return entity != null ? entity.getId() : 0; 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() { 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() { public AvatarInfo toProto() {

View File

@ -60,7 +60,7 @@ public class AvatarStorage extends BasePlayerManager implements Iterable<Avatar>
this.avatars.put(avatar.getAvatarId(), avatar); this.avatars.put(avatar.getAvatarId(), avatar);
this.avatarsGuid.put(avatar.getGuid(), avatar); this.avatarsGuid.put(avatar.getGuid(), avatar);
avatar.save(); avatar.save(true);
return true; return true;
} }
@ -165,7 +165,7 @@ public class AvatarStorage extends BasePlayerManager implements Iterable<Avatar>
if ((avatar.getAvatarId() == 10000007) || (avatar.getAvatarId() == 10000005)) { if ((avatar.getAvatarId() == 10000007) || (avatar.getAvatarId() == 10000005)) {
avatar.setSkillDepot(skillDepot); avatar.setSkillDepot(skillDepot);
avatar.setSkillDepotData(skillDepot); avatar.setSkillDepotData(skillDepot);
avatar.save(); avatar.save(true);
} }
} }

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.BattlePassScheduleOuterClass.BattlePassSchedule;
import emu.grasscutter.net.proto.BattlePassUnlockStatusOuterClass.BattlePassUnlockStatus; import emu.grasscutter.net.proto.BattlePassUnlockStatusOuterClass.BattlePassUnlockStatus;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import lombok.Getter;
import org.bson.types.ObjectId;
import java.time.*; import java.time.*;
import java.time.temporal.TemporalAdjusters; import java.time.temporal.TemporalAdjusters;
import java.util.*; import java.util.*;
import lombok.Getter;
import org.bson.types.ObjectId;
@Entity(value = "battlepass", useDiscriminator = false) @Entity(value = "battlepass", useDiscriminator = false)
public class BattlePassManager extends BasePlayerDataManager { public class BattlePassManager extends BasePlayerDataManager {
@ -40,7 +41,10 @@ public class BattlePassManager extends BasePlayerDataManager {
public BattlePassManager(Player player) { public BattlePassManager(Player player) {
super(player); super(player);
this.ownerUid = player.getUid(); this.ownerUid = player.getUid();
this.missions = new HashMap<>();
this.takenRewards = new HashMap<>();
} }
public void setPlayer(Player player) { public void setPlayer(Player player) {

View File

@ -18,8 +18,8 @@ public class EntityHomeAnimal extends EntityMonster implements Rebornable {
@Getter private final int rebirthCD; @Getter private final int rebirthCD;
private final AtomicBoolean disappeared = new AtomicBoolean(); private final AtomicBoolean disappeared = new AtomicBoolean();
public EntityHomeAnimal(Scene scene, HomeWorldAnimalData data, Position pos, Position rot) { public EntityHomeAnimal(Scene scene, HomeWorldAnimalData data, Position pos) {
super(scene, GameData.getMonsterDataMap().get(data.getMonsterID()), pos, rot, 1); super(scene, GameData.getMonsterDataMap().get(data.getMonsterID()), pos, 1);
this.rebornPos = pos.clone(); this.rebornPos = pos.clone();
this.rebirth = data.getIsRebirth(); this.rebirth = data.getIsRebirth();

View File

@ -54,14 +54,14 @@ public class EntityMonster extends GameEntity {
@Getter private List<Player> playerOnBattle; @Getter private List<Player> playerOnBattle;
@Nullable @Getter @Setter private SceneMonster metaMonster; @Nullable @Getter @Setter private SceneMonster metaMonster;
public EntityMonster(Scene scene, MonsterData monsterData, Position pos, Position rot, int level) { public EntityMonster(Scene scene, MonsterData monsterData, Position pos, int level) {
super(scene); super(scene);
this.id = this.getWorld().getNextEntityId(EntityIdType.MONSTER); this.id = this.getWorld().getNextEntityId(EntityIdType.MONSTER);
this.monsterData = monsterData; this.monsterData = monsterData;
this.fightProperties = new Int2FloatOpenHashMap(); this.fightProperties = new Int2FloatOpenHashMap();
this.position = new Position(pos); this.position = new Position(pos);
this.rotation = new Position(rot); this.rotation = new Position();
this.bornPos = this.getPosition().clone(); this.bornPos = this.getPosition().clone();
this.level = level; this.level = level;
this.playerOnBattle = new ArrayList<>(); this.playerOnBattle = new ArrayList<>();

View File

@ -15,9 +15,10 @@ import emu.grasscutter.scripts.data.controller.EntityController;
import emu.grasscutter.server.event.entity.*; import emu.grasscutter.server.event.entity.*;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import java.util.*;
import lombok.*; import lombok.*;
import java.util.*;
public abstract class GameEntity { public abstract class GameEntity {
@Getter private final Scene scene; @Getter private final Scene scene;
@Getter protected int id; @Getter protected int id;
@ -34,8 +35,7 @@ public abstract class GameEntity {
@Getter @Setter private boolean lockHP; @Getter @Setter private boolean lockHP;
@Setter(AccessLevel.PROTECTED) @Setter(AccessLevel.PROTECTED)
@Getter @Getter private boolean isDead = false;
private boolean isDead = false;
// Lua controller for specific actions // Lua controller for specific actions
@Getter @Setter private EntityController entityController; @Getter @Setter private EntityController entityController;

View File

@ -107,8 +107,7 @@ public class HomeSceneItem {
return new EntityHomeAnimal( return new EntityHomeAnimal(
scene, scene,
GameData.getHomeWorldAnimalDataMap().get(homeAnimalItem.getFurnitureId()), GameData.getHomeWorldAnimalDataMap().get(homeAnimalItem.getFurnitureId()),
homeAnimalItem.getSpawnPos(), homeAnimalItem.getSpawnPos());
homeAnimalItem.getSpawnRot());
}) })
.toList(); .toList();
} }

View File

@ -25,6 +25,7 @@ public class HomeWorld extends World {
this.home = owner.isOnline() ? owner.getHome() : GameHome.getByUid(owner.getUid()); this.home = owner.isOnline() ? owner.getHome() : GameHome.getByUid(owner.getUid());
this.refreshModuleManager(); this.refreshModuleManager();
server.registerHomeWorld(this);
} }
@Override @Override

View File

@ -20,13 +20,14 @@ import emu.grasscutter.net.proto.ReliquaryOuterClass.Reliquary;
import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo; import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo;
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo; import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
import emu.grasscutter.net.proto.WeaponOuterClass.Weapon; import emu.grasscutter.net.proto.WeaponOuterClass.Weapon;
import emu.grasscutter.utils.objects.WeightedList; import emu.grasscutter.utils.objects.*;
import java.util.*;
import lombok.*; import lombok.*;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import java.util.*;
@Entity(value = "items", useDiscriminator = false) @Entity(value = "items", useDiscriminator = false)
public class GameItem { public class GameItem implements DatabaseObject<GameItem> {
@Id private ObjectId id; @Id private ObjectId id;
@Indexed private int ownerId; @Indexed private int ownerId;
@Getter @Setter private int itemId; @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() { public void save() {
if (this.count > 0 && this.ownerId > 0) { if (this.count > 0 && this.ownerId > 0) {
DatabaseHelper.saveItem(this); this.deferSave();
} else if (this.getObjectId() != null) { } else {
DatabaseHelper.deleteItem(this); 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() { public SceneWeaponInfo createSceneWeaponInfo() {
var weaponInfo = var weaponInfo =
SceneWeaponInfo.newBuilder() SceneWeaponInfo.newBuilder()

View File

@ -1,7 +1,5 @@
package emu.grasscutter.game.inventory; package emu.grasscutter.game.inventory;
import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.common.ItemParamData; import emu.grasscutter.data.common.ItemParamData;
@ -18,10 +16,13 @@ import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.longs.*; import it.unimi.dsi.fastutil.longs.*;
import java.util.*;
import javax.annotation.Nullable;
import lombok.val; 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> { public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
private final Long2ObjectMap<GameItem> store; private final Long2ObjectMap<GameItem> store;
private final Int2ObjectMap<InventoryTab> inventoryTypes; private final Int2ObjectMap<InventoryTab> inventoryTypes;
@ -178,7 +179,7 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
changedItems.add(result); changedItems.add(result);
} }
} }
if (changedItems.size() == 0) { if (changedItems.isEmpty()) {
return; return;
} }
if (reason != null) { if (reason != null) {
@ -298,8 +299,7 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
// Add // Add
switch (type) { switch (type) {
case ITEM_WEAPON: case ITEM_WEAPON, ITEM_RELIQUARY -> {
case ITEM_RELIQUARY:
if (tab.getSize() >= tab.getMaxCapacity()) { if (tab.getSize() >= tab.getMaxCapacity()) {
return null; return null;
} }
@ -310,23 +310,23 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
// Set ownership and save to db // Set ownership and save to db
item.save(); item.save();
return item; return item;
case ITEM_VIRTUAL: }
case ITEM_VIRTUAL -> {
// Handle // Handle
this.addVirtualItem(item.getItemId(), item.getCount()); this.addVirtualItem(item.getItemId(), item.getCount());
return item; return item;
default: }
default -> {
switch (item.getItemData().getMaterialType()) { switch (item.getItemData().getMaterialType()) {
case MATERIAL_AVATAR: case MATERIAL_AVATAR, MATERIAL_FLYCLOAK, MATERIAL_COSTUME, MATERIAL_NAMECARD -> {
case MATERIAL_FLYCLOAK:
case MATERIAL_COSTUME:
case MATERIAL_NAMECARD:
Grasscutter.getLogger() Grasscutter.getLogger()
.warn( .warn(
"Attempted to add a " "Attempted to add a "
+ item.getItemData().getMaterialType().name() + item.getItemData().getMaterialType().name()
+ " to inventory, but item definition lacks isUseOnGain. This indicates a Resources error."); + " to inventory, but item definition lacks isUseOnGain. This indicates a Resources error.");
return null; return null;
default: }
default -> {
if (tab == null) { if (tab == null) {
return null; return null;
} }
@ -350,7 +350,9 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
existingItem.save(); existingItem.save();
return existingItem; return existingItem;
} }
}
} }
}
} }
} }

View File

@ -104,8 +104,7 @@ public final class BlossomActivity {
var monsterData = GameData.getMonsterDataMap().get((int) entry); var monsterData = GameData.getMonsterDataMap().get((int) entry);
var level = scene.getEntityLevel(1, worldLevelOverride); var level = scene.getEntityLevel(1, worldLevelOverride);
var entity = var entity = new EntityMonster(scene, monsterData, pos.nearby2d(4f), level);
new EntityMonster(scene, monsterData, pos.nearby2d(4f), Position.ZERO, level);
scene.addEntity(entity); scene.addEntity(entity);
newMonsters.add(entity); newMonsters.add(entity);
} }

View File

@ -8,7 +8,7 @@ import emu.grasscutter.data.excels.world.WeatherData;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.*; import emu.grasscutter.game.*;
import emu.grasscutter.game.ability.AbilityManager; 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.activity.ActivityManager;
import emu.grasscutter.game.avatar.*; import emu.grasscutter.game.avatar.*;
import emu.grasscutter.game.battlepass.BattlePassManager; 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.server.packet.send.*;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import emu.grasscutter.utils.helpers.DateHelper; import emu.grasscutter.utils.helpers.DateHelper;
import emu.grasscutter.utils.objects.FieldFetch; import emu.grasscutter.utils.objects.*;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import lombok.*; import lombok.*;
@ -66,7 +66,7 @@ import java.util.concurrent.*;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS; import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
@Entity(value = "players", useDiscriminator = false) @Entity(value = "players", useDiscriminator = false)
public class Player implements PlayerHook, FieldFetch { public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
@Id private int id; @Id private int id;
@Indexed(options = @IndexOptions(unique = true)) @Indexed(options = @IndexOptions(unique = true))
@Getter private String accountId; @Getter private String accountId;
@ -261,6 +261,7 @@ public class Player implements PlayerHook, FieldFetch {
this.clientAbilityInitFinishHandler = new InvokeHandler(PacketClientAbilityInitFinishNotify.class); this.clientAbilityInitFinishHandler = new InvokeHandler(PacketClientAbilityInitFinishNotify.class);
this.birthday = new PlayerBirthday(); this.birthday = new PlayerBirthday();
this.achievements = Achievements.blank();
this.rewardedLevels = new HashSet<>(); this.rewardedLevels = new HashSet<>();
this.homeRewardedLevels = new HashSet<>(); this.homeRewardedLevels = new HashSet<>();
this.seenRealmList = new HashSet<>(); this.seenRealmList = new HashSet<>();
@ -275,8 +276,10 @@ public class Player implements PlayerHook, FieldFetch {
this.energyManager = new EnergyManager(this); this.energyManager = new EnergyManager(this);
this.resinManager = new ResinManager(this); this.resinManager = new ResinManager(this);
this.forgingManager = new ForgingManager(this); this.forgingManager = new ForgingManager(this);
this.deforestationManager = new DeforestationManager(this);
this.progressManager = new PlayerProgressManager(this); this.progressManager = new PlayerProgressManager(this);
this.furnitureManager = new FurnitureManager(this); this.furnitureManager = new FurnitureManager(this);
this.battlePassManager = new BattlePassManager(this);
this.cookingManager = new CookingManager(this); this.cookingManager = new CookingManager(this);
this.cookingCompoundManager = new CookingCompoundManager(this); this.cookingCompoundManager = new CookingCompoundManager(this);
this.satiationManager = new SatiationManager(this); this.satiationManager = new SatiationManager(this);
@ -300,19 +303,6 @@ public class Player implements PlayerHook, FieldFetch {
this.applyStartingSceneTags(); this.applyStartingSceneTags();
this.getFlyCloakList().add(140001); this.getFlyCloakList().add(140001);
this.getNameCardList().add(210001); 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 @Override
@ -1340,8 +1330,25 @@ public class Player implements PlayerHook, FieldFetch {
this.getTeamManager().setPlayer(this); 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() { 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 // Called from tokenrsp
@ -1378,6 +1385,14 @@ public class Player implements PlayerHook, FieldFetch {
this.getPlayerProgress().setPlayer(this); // Add reference to the player. this.getPlayerProgress().setPlayer(this); // Add reference to the player.
} }
/**
* Invoked when the player selects their avatar.
*/
public void onPlayerBorn() {
Grasscutter.getThreadPool().submit(
this.getQuestManager()::onPlayerBorn);
}
public void onLogin() { public void onLogin() {
// Quest - Commented out because a problem is caused if you log out while this quest is active // Quest - Commented out because a problem is caused if you log out while this quest is active
/* /*
@ -1504,20 +1519,19 @@ public class Player implements PlayerHook, FieldFetch {
this.getProfile().syncWithCharacter(this); this.getProfile().syncWithCharacter(this);
this.getCoopRequests().clear(); this.getCoopRequests().clear();
this.getEnterHomeRequests().values().forEach(req -> this.expireEnterHomeRequest(req, true)); this.getEnterHomeRequests().values()
.forEach(req -> this.expireEnterHomeRequest(req, true));
this.getEnterHomeRequests().clear(); this.getEnterHomeRequests().clear();
// Save to db // Save to db
this.save(); this.save(true);
this.getTeamManager().saveAvatars(); this.getTeamManager().saveAvatars();
this.getFriendsList().save(); this.getFriendsList().save();
// Call quit event. // Call quit event.
PlayerQuitEvent event = new PlayerQuitEvent(this); new PlayerQuitEvent(this).call();
event.call();
} catch (Throwable e) { } catch (Throwable e) {
e.printStackTrace(); Grasscutter.getLogger().warn("Player (UID {}) failed to save.", this.getUid(), e);
Grasscutter.getLogger().warn("Player (UID {}) save failure", getUid());
} finally { } finally {
removeFromServer(); removeFromServer();
} }
@ -1525,9 +1539,10 @@ public class Player implements PlayerHook, FieldFetch {
public void removeFromServer() { public void removeFromServer() {
// Remove from server. // Remove from server.
//Note: DON'T DELETE BY UID,BECAUSE THERE ARE MULTIPLE SAME UID PLAYERS WHEN DUPLICATED LOGIN! // 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 //s o I decide to delete by object rather than uid
getServer().getPlayers().values().removeIf(player1 -> player1 == this); this.getServer().getPlayers().values()
.removeIf(player1 -> player1 == this);
} }
public int getLegendaryKey() { public int getLegendaryKey() {

View File

@ -33,9 +33,12 @@ public final class PlayerProgressManager extends BasePlayerDataManager {
public static final Set<Integer> IGNORED_OPEN_STATES = public static final Set<Integer> IGNORED_OPEN_STATES =
Set.of( Set.of(
1404 // OPEN_STATE_MENGDE_INFUSEDCRYSTAL, causes quest 'Mine Craft' to be given to the 1404, // OPEN_STATE_MENGDE_INFUSEDCRYSTAL, causes quest 'Mine Craft' to be given to the
// player at the start of the game. // player at the start of the game.
// This should be removed when city reputation is implemented. // This should be removed when city reputation is implemented.
57 // OPEN_STATE_PERSONAL_LINE, causes the prompt for showing character hangout quests to
// be permanently shown.
// This should be removed when character story quests are implemented.
); );
// Set of open states that are set per default for all accounts. Can be overwritten by an entry in // Set of open states that are set per default for all accounts. Can be overwritten by an entry in
// `map`. // `map`.

View File

@ -1,11 +1,10 @@
package emu.grasscutter.game.player; package emu.grasscutter.game.player;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
import dev.morphia.annotations.*; import dev.morphia.annotations.*;
import emu.grasscutter.*; import emu.grasscutter.*;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData; import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData;
import emu.grasscutter.database.Database;
import emu.grasscutter.game.avatar.Avatar; import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.props.*; import emu.grasscutter.game.props.*;
@ -23,9 +22,12 @@ import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import lombok.*;
import java.util.*; import java.util.*;
import java.util.stream.Stream; import java.util.stream.Stream;
import lombok.*;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
@Entity @Entity
public final class TeamManager extends BasePlayerDataManager { public final class TeamManager extends BasePlayerDataManager {
@ -404,7 +406,7 @@ public final class TeamManager extends BasePlayerDataManager {
// Unload removed entities // Unload removed entities
for (var entity : existingAvatars.values()) { for (var entity : existingAvatars.values()) {
this.getPlayer().getScene().removeEntity(entity); this.getPlayer().getScene().removeEntity(entity);
entity.getAvatar().save(); entity.getAvatar().save(true);
} }
// Set new selected character index // Set new selected character index
@ -962,11 +964,13 @@ public final class TeamManager extends BasePlayerDataManager {
return respawnPoint.get().getPointData().getTranPos(); return respawnPoint.get().getPointData().getTranPos();
} }
/**
* Performs a bulk save operation on all avatars.
*/
public void saveAvatars() { public void saveAvatars() {
// Save all avatars from active team Database.saveAll(this.getActiveTeam().stream()
for (EntityAvatar entity : this.getActiveTeam()) { .map(EntityAvatar::getAvatar)
entity.getAvatar().save(); .toList());
}
} }
public void onPlayerLogin() { // Hack for now to fix resonances on login public void onPlayerLogin() { // Hack for now to fix resonances on login

View File

@ -1,8 +1,5 @@
package emu.grasscutter.game.quest; package emu.grasscutter.game.quest;
import static emu.grasscutter.GameConstants.DEBUG;
import static emu.grasscutter.config.Configuration.*;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.binout.*; import emu.grasscutter.data.binout.*;
@ -15,12 +12,16 @@ import emu.grasscutter.net.proto.GivingRecordOuterClass.GivingRecord;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import lombok.*;
import javax.annotation.Nonnull;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; 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 { public final class QuestManager extends BasePlayerManager {
@Getter private final Player player; @Getter private final Player player;
@ -221,11 +222,14 @@ public final class QuestManager extends BasePlayerManager {
this.player.sendPacket(new PacketGivingRecordNotify(this.getGivingRecords())); this.player.sendPacket(new PacketGivingRecordNotify(this.getGivingRecords()));
} }
public void onLogin() { public void onPlayerBorn() {
if (this.isQuestingEnabled()) { if (this.isQuestingEnabled()) {
this.enableQuests(); this.enableQuests();
this.sendGivingRecords(); this.sendGivingRecords();
} }
}
public void onLogin() {
List<GameMainQuest> activeQuests = getActiveMainQuests(); List<GameMainQuest> activeQuests = getActiveMainQuests();
List<GameQuest> activeSubs = new ArrayList<>(activeQuests.size()); List<GameQuest> activeSubs = new ArrayList<>(activeQuests.size());
@ -566,7 +570,7 @@ public final class QuestManager extends BasePlayerManager {
* @param quest The ID of the quest. * @param quest The ID of the quest.
*/ */
public void checkQuestAlreadyFulfilled(GameQuest quest) { public void checkQuestAlreadyFulfilled(GameQuest quest) {
Grasscutter.getThreadPool() eventExecutor
.submit( .submit(
() -> { () -> {
for (var condition : quest.getQuestData().getFinishCond()) { for (var condition : quest.getQuestData().getFinishCond()) {

View File

@ -1,21 +0,0 @@
package emu.grasscutter.game.quest.conditions;
import emu.grasscutter.data.excels.quest.QuestData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.QuestValueCond;
import emu.grasscutter.game.quest.enums.QuestCond;
@QuestValueCond(QuestCond.QUEST_COND_MAIN_COOP_START)
public class ConditionMainCoopStart extends BaseCondition {
@Override
public boolean execute(
Player owner,
QuestData questData,
QuestData.QuestAcceptCondition condition,
String paramStr,
int... params) {
return condition.getParam()[0] == params[0]
&& (condition.getParam()[1] == 0 || condition.getParam()[1] == params[1]);
}
}

View File

@ -54,7 +54,7 @@ public enum QuestCond implements QuestTrigger {
QUEST_COND_QUEST_GLOBAL_VAR_LESS(46), QUEST_COND_QUEST_GLOBAL_VAR_LESS(46),
QUEST_COND_PERSONAL_LINE_UNLOCK(47), QUEST_COND_PERSONAL_LINE_UNLOCK(47),
QUEST_COND_CITY_REPUTATION_REQUEST(48), // missing QUEST_COND_CITY_REPUTATION_REQUEST(48), // missing
QUEST_COND_MAIN_COOP_START(49), QUEST_COND_MAIN_COOP_START(49), // missing
QUEST_COND_MAIN_COOP_ENTER_SAVE_POINT(50), // missing QUEST_COND_MAIN_COOP_ENTER_SAVE_POINT(50), // missing
QUEST_COND_CITY_REPUTATION_LEVEL(51), // missing, only NPC groups QUEST_COND_CITY_REPUTATION_LEVEL(51), // missing, only NPC groups
QUEST_COND_CITY_REPUTATION_UNLOCK(52), // missing, currently unused QUEST_COND_CITY_REPUTATION_UNLOCK(52), // missing, currently unused

View File

@ -816,8 +816,8 @@ public class Scene {
int level = this.getEntityLevel(entry.getLevel(), worldLevelOverride); int level = this.getEntityLevel(entry.getLevel(), worldLevelOverride);
EntityMonster monster = EntityMonster monster = new EntityMonster(this, data, entry.getPos(), level);
new EntityMonster(this, data, entry.getPos(), entry.getRot(), level); monster.getRotation().set(entry.getRot());
monster.setGroupId(entry.getGroup().getGroupId()); monster.setGroupId(entry.getGroup().getGroupId());
monster.setPoseId(entry.getPoseId()); monster.setPoseId(entry.getPoseId());
monster.setConfigId(entry.getConfigId()); monster.setConfigId(entry.getConfigId());

View File

@ -1,7 +1,5 @@
package emu.grasscutter.game.world; package emu.grasscutter.game.world;
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.dungeon.DungeonData; import emu.grasscutter.data.excels.dungeon.DungeonData;
import emu.grasscutter.game.entity.*; import emu.grasscutter.game.entity.*;
@ -20,10 +18,13 @@ import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.packet.send.*; import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.ConversionUtils; import emu.grasscutter.utils.ConversionUtils;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import java.util.*;
import lombok.*; import lombok.*;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.*;
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
public class World implements Iterable<Player> { public class World implements Iterable<Player> {
@Getter private final GameServer server; @Getter private final GameServer server;
@Getter private Player host; @Getter private Player host;
@ -72,8 +73,6 @@ public class World implements Iterable<Player> {
this.scenes = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); this.scenes = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>());
this.entity = new EntityWorld(this); this.entity = new EntityWorld(this);
this.lastUpdateTime = System.currentTimeMillis(); this.lastUpdateTime = System.currentTimeMillis();
server.registerWorld(this);
} }
public int getLevelEntityId() { public int getLevelEntityId() {
@ -268,7 +267,7 @@ public class World implements Iterable<Player> {
scene.removePlayer(player); scene.removePlayer(player);
// Info packet for other players // Info packet for other players
if (this.getPlayers().size() > 0) { if (!this.getPlayers().isEmpty()) {
this.updatePlayerInfos(player); this.updatePlayerInfos(player);
} }
@ -398,50 +397,37 @@ public class World implements Iterable<Player> {
return false; return false;
} }
Scene oldScene = player.getScene(); Scene oldScene = null;
var newScene = this.getSceneById(teleportProperties.getSceneId()); if (player.getScene() != null) {
oldScene = player.getScene();
// Move directly in the same scene.
if (newScene == oldScene && teleportProperties.getTeleportType() == TeleportType.COMMAND) {
// Set player position and rotation
if (teleportProperties.getTeleportTo() != null) {
player.getPosition().set(teleportProperties.getTeleportTo());
}
if (teleportProperties.getTeleportRot() != null) {
player.getRotation().set(teleportProperties.getTeleportRot());
}
player.sendPacket(new PacketSceneEntityAppearNotify(player));
return true;
}
if (oldScene != null) {
// Don't deregister scenes if the player is going to tp back into them // Don't deregister scenes if the player is going to tp back into them
if (oldScene == newScene) { if (oldScene.getId() == teleportProperties.getSceneId()) {
oldScene.setDontDestroyWhenEmpty(true); oldScene.setDontDestroyWhenEmpty(true);
} }
oldScene.removePlayer(player); oldScene.removePlayer(player);
} }
if (newScene != null) { var newScene = this.getSceneById(teleportProperties.getSceneId());
newScene.addPlayer(player); newScene.addPlayer(player);
player.getTeamManager().applyAbilities(newScene); player.getTeamManager().applyAbilities(newScene);
// Dungeon // Dungeon
// Dungeon system is handling this already // Dungeon system is handling this already
// if(dungeonData!=null){ // if(dungeonData!=null){
// var dungeonManager = new DungeonManager(newScene, dungeonData); // var dungeonManager = new DungeonManager(newScene, dungeonData);
// dungeonManager.startDungeon(); // dungeonManager.startDungeon();
// } // }
SceneConfig config = newScene.getScriptManager().getConfig(); SceneConfig config = newScene.getScriptManager().getConfig();
if (teleportProperties.getTeleportTo() == null && config != null) { if (teleportProperties.getTeleportTo() == null && config != null) {
if (config.born_pos != null) { if (config.born_pos != null) {
teleportProperties.setTeleportTo(config.born_pos); teleportProperties.setTeleportTo(config.born_pos);
} }
if (config.born_rot != null) { if (config.born_rot != null) {
teleportProperties.setTeleportRot(config.born_rot); teleportProperties.setTeleportRot(config.born_rot);
}
} }
} }
@ -453,7 +439,7 @@ public class World implements Iterable<Player> {
player.getRotation().set(teleportProperties.getTeleportRot()); player.getRotation().set(teleportProperties.getTeleportRot());
} }
if (oldScene != null && newScene != null && newScene != oldScene) { if (oldScene != null && newScene != oldScene) {
newScene.setPrevScenePoint(oldScene.getPrevScenePoint()); newScene.setPrevScenePoint(oldScene.getPrevScenePoint());
oldScene.setDontDestroyWhenEmpty(false); oldScene.setDontDestroyWhenEmpty(false);
} }

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

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

View File

@ -1037,7 +1037,8 @@ public class SceneScriptManager {
} }
// Spawn mob // Spawn mob
EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, monster.rot, level); EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level);
entity.getRotation().set(monster.rot);
entity.setGroupId(groupId); entity.setGroupId(groupId);
entity.setBlockId(blockId); entity.setBlockId(blockId);
entity.setConfigId(monster.config_id); entity.setConfigId(monster.config_id);

View File

@ -1,55 +1,49 @@
package emu.grasscutter.server.game; 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.*;
import emu.grasscutter.Grasscutter.ServerRunMode; import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account; import emu.grasscutter.game.Account;
import emu.grasscutter.game.battlepass.BattlePassSystem; import emu.grasscutter.game.battlepass.BattlePassSystem;
import emu.grasscutter.game.chat.ChatSystem; import emu.grasscutter.game.chat.*;
import emu.grasscutter.game.chat.ChatSystemHandler;
import emu.grasscutter.game.combine.CombineManger; import emu.grasscutter.game.combine.CombineManger;
import emu.grasscutter.game.drop.DropSystem; import emu.grasscutter.game.drop.*;
import emu.grasscutter.game.drop.DropSystemLegacy;
import emu.grasscutter.game.dungeons.DungeonSystem; import emu.grasscutter.game.dungeons.DungeonSystem;
import emu.grasscutter.game.expedition.ExpeditionSystem; import emu.grasscutter.game.expedition.ExpeditionSystem;
import emu.grasscutter.game.gacha.GachaSystem; import emu.grasscutter.game.gacha.GachaSystem;
import emu.grasscutter.game.home.HomeWorld; import emu.grasscutter.game.home.*;
import emu.grasscutter.game.home.HomeWorldMPSystem; import emu.grasscutter.game.managers.cooking.*;
import emu.grasscutter.game.managers.cooking.CookingCompoundManager;
import emu.grasscutter.game.managers.cooking.CookingManager;
import emu.grasscutter.game.managers.energy.EnergyManager; import emu.grasscutter.game.managers.energy.EnergyManager;
import emu.grasscutter.game.managers.stamina.StaminaManager; import emu.grasscutter.game.managers.stamina.StaminaManager;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.QuestSystem; import emu.grasscutter.game.quest.QuestSystem;
import emu.grasscutter.game.shop.ShopSystem; import emu.grasscutter.game.shop.ShopSystem;
import emu.grasscutter.game.systems.AnnouncementSystem; import emu.grasscutter.game.systems.*;
import emu.grasscutter.game.systems.InventorySystem;
import emu.grasscutter.game.systems.MultiplayerSystem;
import emu.grasscutter.game.talk.TalkSystem; import emu.grasscutter.game.talk.TalkSystem;
import emu.grasscutter.game.tower.TowerSystem; import emu.grasscutter.game.tower.TowerSystem;
import emu.grasscutter.game.world.World; import emu.grasscutter.game.world.*;
import emu.grasscutter.game.world.WorldDataSystem;
import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
import emu.grasscutter.server.dispatch.DispatchClient; import emu.grasscutter.server.dispatch.DispatchClient;
import emu.grasscutter.server.event.game.ServerTickEvent; import emu.grasscutter.server.event.game.ServerTickEvent;
import emu.grasscutter.server.event.internal.ServerStartEvent; import emu.grasscutter.server.event.internal.*;
import emu.grasscutter.server.event.internal.ServerStopEvent;
import emu.grasscutter.server.event.types.ServerEvent; import emu.grasscutter.server.event.types.ServerEvent;
import emu.grasscutter.server.scheduler.ServerTaskScheduler; import emu.grasscutter.server.scheduler.ServerTaskScheduler;
import emu.grasscutter.task.TaskMap; import emu.grasscutter.task.TaskMap;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*; 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.net.*;
import java.time.*; import java.time.*;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import kcp.highway.*;
import lombok.*; import static emu.grasscutter.config.Configuration.*;
import org.jetbrains.annotations.*; import static emu.grasscutter.utils.lang.Language.translate;
@Getter @Getter
public final class GameServer extends KcpServer implements Iterable<Player> { 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 Set<World> worlds;
private final Int2ObjectMap<HomeWorld> homeWorlds; private final Int2ObjectMap<HomeWorld> homeWorlds;
@Getter private boolean started = false;
@Setter private DispatchClient dispatchClient; @Setter private DispatchClient dispatchClient;
// Server systems // Server systems
@ -140,7 +135,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
channelConfig.setUseConvChannel(true); channelConfig.setUseConvChannel(true);
channelConfig.setAckNoDelay(false); channelConfig.setAckNoDelay(false);
this.init(GameSessionManager.getListener(), channelConfig, address); this.init(GameSessionManager.getInstance(), channelConfig, address);
EnergyManager.initialize(); EnergyManager.initialize();
StaminaManager.initialize(); StaminaManager.initialize();
@ -311,6 +306,11 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
world.save(); // Save the player's world world.save(); // Save the player's world
} }
public void registerHomeWorld(HomeWorld homeWorld) {
this.getHomeWorlds().put(homeWorld.getOwnerUid(), homeWorld);
this.registerWorld(homeWorld);
}
public HomeWorld getHomeWorldOrCreate(Player owner) { public HomeWorld getHomeWorldOrCreate(Player owner) {
return this.getHomeWorlds() return this.getHomeWorlds()
.computeIfAbsent(owner.getUid(), (uid) -> new HomeWorld(this, owner)); .computeIfAbsent(owner.getUid(), (uid) -> new HomeWorld(this, owner));
@ -342,6 +342,8 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
.info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort())); .info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort()));
ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now());
event.call(); event.call();
this.started = true;
} }
public void onServerShutdown() { public void onServerShutdown() {
@ -356,10 +358,10 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
this.stop(); // Stop the server. this.stop(); // Stop the server.
try { try {
var threadPool = GameSessionManager.getLogicThread(); var threadPool = GameSessionManager.getExecutor();
// Shutdown network thread. // Shutdown network thread.
threadPool.shutdownGracefully(); threadPool.shutdown();
// Wait for the network thread to finish. // Wait for the network thread to finish.
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) { if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
Grasscutter.getLogger().error("Logic thread did not terminate!"); Grasscutter.getLogger().error("Logic thread did not terminate!");

View File

@ -1,7 +1,5 @@
package emu.grasscutter.server.game; package emu.grasscutter.server.game;
import static emu.grasscutter.config.Configuration.GAME_INFO;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.net.packet.*; import emu.grasscutter.net.packet.*;
@ -9,6 +7,8 @@ import emu.grasscutter.server.event.game.ReceivePacketEvent;
import emu.grasscutter.server.game.GameSession.SessionState; import emu.grasscutter.server.game.GameSession.SessionState;
import it.unimi.dsi.fastutil.ints.*; import it.unimi.dsi.fastutil.ints.*;
import static emu.grasscutter.config.Configuration.GAME_INFO;
public final class GameServerPacketHandler { public final class GameServerPacketHandler {
private final Int2ObjectMap<PacketHandler> handlers; private final Int2ObjectMap<PacketHandler> handlers;
@ -76,13 +76,11 @@ public final class GameServerPacketHandler {
} }
// Invoke event. // Invoke event.
ReceivePacketEvent event = new ReceivePacketEvent(session, opcode, payload); var event = new ReceivePacketEvent(session, opcode, payload);
event.call(); if (event.call()) // If event is not canceled, continue.
if (!event.isCanceled()) // If event is not canceled, continue. handler.handle(session, header, event.getPacketData());
handler.handle(session, header, event.getPacketData());
} catch (Exception ex) { } catch (Exception ex) {
// TODO Remove this when no more needed Grasscutter.getLogger().warn("Unable to handle packet.", ex);
ex.printStackTrace();
} }
return; // Packet successfully handled return; // Packet successfully handled
} }

View File

@ -1,24 +1,26 @@
package emu.grasscutter.server.game; 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;
import emu.grasscutter.Grasscutter.ServerDebugMode; import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.game.Account; import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import emu.grasscutter.net.*;
import emu.grasscutter.net.packet.*; import emu.grasscutter.net.packet.*;
import emu.grasscutter.server.event.game.SendPacketEvent; import emu.grasscutter.server.event.game.SendPacketEvent;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import io.netty.buffer.*; import io.netty.buffer.*;
import lombok.*;
import java.io.File; import java.io.File;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.file.Path; 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 final GameServer server;
private GameSessionManager.KcpTunnel tunnel; private KcpTunnel tunnel;
@Getter @Setter private Account account; @Getter @Setter private Account account;
@Getter private Player player; @Getter private Player player;
@ -146,7 +148,7 @@ public class GameSession implements GameSessionManager.KcpChannel {
if (packet.shouldEncrypt) { if (packet.shouldEncrypt) {
Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey); Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey);
} }
tunnel.writeData(bytes); this.tunnel.writeData(bytes);
} catch (Exception ignored) { } catch (Exception ignored) {
Grasscutter.getLogger().debug("Unable to send packet to client."); Grasscutter.getLogger().debug("Unable to send packet to client.");
} }
@ -154,13 +156,13 @@ public class GameSession implements GameSessionManager.KcpChannel {
} }
@Override @Override
public void onConnected(GameSessionManager.KcpTunnel tunnel) { public void onConnected(KcpTunnel tunnel) {
this.tunnel = tunnel; this.tunnel = tunnel;
Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString())); Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString()));
} }
@Override @Override
public void handleReceive(byte[] bytes) { public void onMessage(byte[] bytes) {
// Decrypt and turn back into a packet // Decrypt and turn back into a packet
Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY); Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY);
ByteBuf packet = Unpooled.wrappedBuffer(bytes); ByteBuf packet = Unpooled.wrappedBuffer(bytes);
@ -226,8 +228,8 @@ public class GameSession implements GameSessionManager.KcpChannel {
// Handle // Handle
getServer().getPacketHandler().handle(this, opcode, header, payload); getServer().getPacketHandler().handle(this, opcode, header, payload);
} }
} catch (Exception e) { } catch (Exception exception) {
e.printStackTrace(); Grasscutter.getLogger().warn("Unable to handle packet.", exception);
} finally { } finally {
// byteBuf.release(); //Needn't // byteBuf.release(); //Needn't
packet.release(); packet.release();
@ -235,8 +237,9 @@ public class GameSession implements GameSessionManager.KcpChannel {
} }
@Override @Override
public void handleClose() { public void onDisconnected() {
setState(SessionState.INACTIVE); setState(SessionState.INACTIVE);
// send disconnection pack in case of reconnection // send disconnection pack in case of reconnection
Grasscutter.getLogger() Grasscutter.getLogger()
.info(translate("messages.game.disconnect", this.getAddress().toString())); .info(translate("messages.game.disconnect", this.getAddress().toString()));

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.CancelCoopTaskReqOuterClass;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketCancelCoopTaskRsp;
@Opcodes(PacketOpcodes.CancelCoopTaskReq)
public class HandlerCancelCoopTaskReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
CancelCoopTaskReqOuterClass.CancelCoopTaskReq req =
CancelCoopTaskReqOuterClass.CancelCoopTaskReq.parseFrom(payload);
var chapterId = req.getChapterId();
Grasscutter.getLogger().warn("Call to unimplemented packet CancelCoopTaskReq");
// TODO: Actually cancel the quests.
session.send(new PacketCancelCoopTaskRsp(chapterId));
}
}

View File

@ -2,7 +2,6 @@ package emu.grasscutter.server.packet.recv;
import emu.grasscutter.net.packet.*; import emu.grasscutter.net.packet.*;
import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketCoopDataNotify;
import emu.grasscutter.server.packet.send.PacketPersonalLineAllDataRsp; import emu.grasscutter.server.packet.send.PacketPersonalLineAllDataRsp;
@Opcodes(PacketOpcodes.PersonalLineAllDataReq) @Opcodes(PacketOpcodes.PersonalLineAllDataReq)
@ -13,7 +12,5 @@ public class HandlerPersonalLineAllDataReq extends PacketHandler {
session.send( session.send(
new PacketPersonalLineAllDataRsp( new PacketPersonalLineAllDataRsp(
session.getPlayer().getQuestManager().getMainQuests().values())); session.getPlayer().getQuestManager().getMainQuests().values()));
// TODO: this should maybe be at player login?
session.send(new PacketCoopDataNotify());
} }
} }

View File

@ -61,7 +61,7 @@ public class HandlerQuestCreateEntityReq extends PacketHandler {
val monsterId = entity.getMonsterId(); val monsterId = entity.getMonsterId();
val level = entity.getLevel(); val level = entity.getLevel();
MonsterData monsterData = GameData.getMonsterDataMap().get(monsterId); MonsterData monsterData = GameData.getMonsterDataMap().get(monsterId);
gameEntity = new EntityMonster(scene, monsterData, pos, rot, level); gameEntity = new EntityMonster(scene, monsterData, pos, level);
} }
case NPC_ID -> {} case NPC_ID -> {}
} }

View File

@ -69,6 +69,7 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler {
// Login done // Login done
session.getPlayer().onLogin(); session.getPlayer().onLogin();
session.getPlayer().onPlayerBorn();
// Born resp packet // Born resp packet
session.send(new BasePacket(PacketOpcodes.SetPlayerBornDataRsp)); session.send(new BasePacket(PacketOpcodes.SetPlayerBornDataRsp));

View File

@ -1,31 +0,0 @@
package emu.grasscutter.server.packet.recv;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.quest.enums.QuestCond;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.StartCoopPointReqOuterClass;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.packet.send.PacketStartCoopPointRsp;
@Opcodes(PacketOpcodes.StartCoopPointReq)
public class HandlerStartCoopPointReq extends PacketHandler {
@Override
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
StartCoopPointReqOuterClass.StartCoopPointReq req =
StartCoopPointReqOuterClass.StartCoopPointReq.parseFrom(payload);
var coopPoint = req.getCoopPoint();
var coopPointData =
GameData.getCoopPointDataMap().values().stream()
.filter(i -> i.getId() == coopPoint)
.toList();
if (!coopPointData.isEmpty()) {
var player = session.getPlayer();
var questManager = player.getQuestManager();
questManager.queueEvent(
QuestCond.QUEST_COND_MAIN_COOP_START, coopPointData.get(0).getChapterId(), 0);
}
session.send(new PacketStartCoopPointRsp(coopPoint));
}
}

View File

@ -1,16 +0,0 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.CancelCoopTaskRspOuterClass;
public class PacketCancelCoopTaskRsp extends BasePacket {
public PacketCancelCoopTaskRsp(int chapterId) {
super(PacketOpcodes.SetCoopChapterViewedRsp);
CancelCoopTaskRspOuterClass.CancelCoopTaskRsp proto =
CancelCoopTaskRspOuterClass.CancelCoopTaskRsp.newBuilder().setChapterId(chapterId).build();
this.setData(proto);
}
}

View File

@ -1,49 +0,0 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.data.GameData;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.CoopChapterOuterClass;
import emu.grasscutter.net.proto.CoopDataNotifyOuterClass;
import emu.grasscutter.net.proto.CoopPointOuterClass;
public class PacketCoopDataNotify extends BasePacket {
public PacketCoopDataNotify() {
super(PacketOpcodes.CoopDataNotify);
var proto = CoopDataNotifyOuterClass.CoopDataNotify.newBuilder();
proto.setIsHaveProgress(false);
// TODO: implement: determine the actual current progress point.
// Add every chapter and add the start point to each chapter regardless of actual progress.
GameData.getCoopChapterDataMap()
.values()
.forEach(
i -> {
var chapter = CoopChapterOuterClass.CoopChapter.newBuilder();
chapter.setId(i.getId());
// TODO: implement: look at unlockCond to determine what state each chapter should be
// in.
// Set every chapter to "Accept" regardless of accept conditions.
chapter.setStateValue(3); // 3 == STATE_ACCEPT
var point = CoopPointOuterClass.CoopPoint.newBuilder();
var pointList =
GameData.getCoopPointDataMap().values().stream()
.filter(
j -> j.getChapterId() == i.getId() && j.getType().equals("POINT_START"))
.toList();
if (!pointList.isEmpty()) {
int pointId = pointList.get(0).getId();
point.setId(pointId);
chapter.addCoopPointList(point);
}
proto.addChapterList(chapter);
});
this.setData(proto);
}
}

View File

@ -1,16 +0,0 @@
package emu.grasscutter.server.packet.send;
import emu.grasscutter.net.packet.*;
import emu.grasscutter.net.proto.StartCoopPointRspOuterClass;
public class PacketStartCoopPointRsp extends BasePacket {
public PacketStartCoopPointRsp(int coopPoint) {
super(PacketOpcodes.StartCoopPointRsp);
StartCoopPointRspOuterClass.StartCoopPointRsp proto =
StartCoopPointRspOuterClass.StartCoopPointRsp.newBuilder().setCoopPoint(coopPoint).build();
this.setData(proto);
}
}

View File

@ -10,7 +10,6 @@ import emu.grasscutter.data.common.ItemUseData;
import emu.grasscutter.data.excels.*; import emu.grasscutter.data.excels.*;
import emu.grasscutter.data.excels.achievement.AchievementData; import emu.grasscutter.data.excels.achievement.AchievementData;
import emu.grasscutter.data.excels.avatar.AvatarData; import emu.grasscutter.data.excels.avatar.AvatarData;
import emu.grasscutter.server.http.handlers.GachaHandler;
import emu.grasscutter.utils.*; import emu.grasscutter.utils.*;
import emu.grasscutter.utils.lang.Language; import emu.grasscutter.utils.lang.Language;
import emu.grasscutter.utils.lang.Language.TextStrings; import emu.grasscutter.utils.lang.Language.TextStrings;
@ -312,19 +311,8 @@ public final class Tools {
return sbs.stream().map(StringBuilder::toString).toList(); return sbs.stream().map(StringBuilder::toString).toList();
} }
public static void generateGachaMappings() {
var path = GachaHandler.getGachaMappingsPath();
if (!Files.exists(path)) {
try {
Grasscutter.getLogger().debug("Creating default '" + path + "' data");
Tools.createGachaMappings(path);
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to create gacha mappings. \n" + exception);
}
}
}
public static void createGachaMappings(Path location) throws IOException { public static void createGachaMappings(Path location) throws IOException {
ResourceLoader.loadResources();
List<String> jsons = createGachaMappingJsons(); List<String> jsons = createGachaMappingJsons();
var usedLocales = new HashSet<String>(); var usedLocales = new HashSet<String>();
StringBuilder sb = new StringBuilder("mappings = {\n"); StringBuilder sb = new StringBuilder("mappings = {\n");

View File

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

View File

@ -1,60 +1,18 @@
package io.grasscutter; package io.grasscutter;
import com.mchange.util.AssertException; import io.grasscutter.virtual.*;
import emu.grasscutter.Grasscutter; import lombok.*;
import emu.grasscutter.config.Configuration;
import java.io.IOException;
import lombok.Getter;
import okhttp3.OkHttpClient; 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 { 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 public static VirtualAccount account;
@Getter private static int gamePort = -1; @Setter
@Getter public static VirtualPlayer player;
/** @Getter public static VirtualGameSession gameSession;
* 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);
}
}
} }

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