mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-07-04 05:53:42 +00:00
Compare commits
40 Commits
hyper-opti
...
v1.7.3
Author | SHA1 | Date | |
---|---|---|---|
205b79dc02 | |||
0e033e3f77 | |||
583a41ab2c | |||
cf6fb275be | |||
269f7b4fbf | |||
9b4ce34f4a | |||
f86259a430 | |||
837e30e04b | |||
f5703e5964 | |||
bc8e7c21ce | |||
b7a9d28f02 | |||
770cd62370 | |||
6745d1126e | |||
0803618bf5 | |||
cfc8a4866f | |||
fd75ba7b9b | |||
d32a75e980 | |||
9a198bd231 | |||
453dc9717d | |||
582d7af9c4 | |||
cab3bfb5a7 | |||
cf574e99cb | |||
3094facb88 | |||
6e309b6fee | |||
b5e35f5409 | |||
a3fd10c3be | |||
b6e7d69949 | |||
5faf39d359 | |||
0dd95450b1 | |||
0f0e7aca68 | |||
5ee4812ac5 | |||
ec2bfffdd1 | |||
7f5059cb8f | |||
43db7eba8f | |||
ff6a51db30 | |||
047feaf4aa | |||
88315ec712 | |||
fdad4218e7 | |||
5f5e6c38b1 | |||
92bd09eeed |
@ -58,7 +58,7 @@ sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
|
||||
group = 'io.grasscutter'
|
||||
version = '1.7.1'
|
||||
version = '1.7.3'
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
|
188
docs/events/windtrace/README.md
Normal file
188
docs/events/windtrace/README.md
Normal file
@ -0,0 +1,188 @@
|
||||
# Hide and Seek!
|
||||
Documentation on how the **Hide and Seek** game works.\
|
||||
Externally dubbed: `Windtrace`.
|
||||
|
||||
# Map IDs
|
||||
TODO: Document the map IDs of Windtrace.
|
||||
|
||||
TODO: Investigate `ServerGlobalValueChangeNotify`
|
||||
|
||||
# Asking Players to Play in a Co-Op Game
|
||||
1. The client will send `DraftOwnerStartInviteReq`
|
||||
2. The server will send `DraftOwnerInviteNotify` to all clients.
|
||||
3. The server will send `DraftOwnerStartInviteRsp`
|
||||
|
||||
# Matching in a Co-Op Game
|
||||
1. World owner talks to Gygax and begins a Windtrace game.
|
||||
2. The packet `DraftOwnerInviteNotify` is sent to clients.
|
||||
3. Clients will respond with `DraftGuestReplyInviteReq` (client-side)
|
||||
4. The server will respond with `DraftGuestReplyInviteRsp`
|
||||
5. The server will respond with `DraftInviteResultNotify`
|
||||
|
||||
# Starting Windtrace
|
||||
1. If `DraftInviteResultNotify` is a success, the server will send a series of packets.
|
||||
1. A series of `SceneEntityAppearNotify` packets.
|
||||
2. `NpcTalkStateNotify`
|
||||
3. `PlayerEnterSceneNotify`
|
||||
4. `MultistagePlayInfoNotify`
|
||||
2. The players are then teleported to the Windtrace map in their locations.
|
||||
3. Server will send packets to clients. (this is server boilerplate)
|
||||
4. The server sends another `MultistagePlayInfoNotify` to clients.
|
||||
|
||||
# Changing Avatars - Others
|
||||
1. The server will send a `AvatarEquipChangeNotify` packet to clients.
|
||||
2. The server will send a `SceneTeamUpdateNotify` packet to clients.
|
||||
3. The server will send a `HideAndSeekPlayerSetAvatarNotify` packet to clients.
|
||||
|
||||
# Getting Ready
|
||||
1. The client will send `HideAndSeekSetReadyReq` to the server.
|
||||
2. The server will reply with `HideAndSeekPlayerReadyNotify` to clients.
|
||||
3. The server will send `MultistagePlayInfoNotify` to clients.
|
||||
4. The server will reply with `HideAndSeekSetReadyRsp` to the client.
|
||||
5. If all players are ready, the server will move on to start Windtrace.
|
||||
|
||||
# Starting Windtrace
|
||||
1. When all players are ready, the server will send a series of packets to players.
|
||||
1. `GalleryStartNotify`
|
||||
2. `SceneGalleryInfoNotify`
|
||||
3. `MultistagePlayInfoNotify`
|
||||
4. `MultistagePlayStageEndNotify`
|
||||
5. This will only get sent at the `1.` countdown.
|
||||
|
||||
### Notes:
|
||||
- `GuestReplyInviteRsp` is sent **after** `DraftInviteResultNotify`.
|
||||
|
||||
## `DraftOwnerInviteNotify`
|
||||
- `invite_deadline_time` - This is the time when the invite expires.
|
||||
- `draft_id` - The value is always `3001` for Windtrace.
|
||||
|
||||
## `DraftOwnerStartInviteReq`
|
||||
- `draft_id` - The value is always `3001` for Windtrace.
|
||||
|
||||
## `DraftOwnerStartInviteRsp`
|
||||
- `draft_id` - The value is always `3001` for Windtrace.
|
||||
- `invite_fail_info_list` - A list of players who weren't invited.
|
||||
- `retcode` - The response code.
|
||||
- `wrong_uid` - Always `0`. (undocumented)
|
||||
|
||||
## `DraftGuestReplyInviteReq`
|
||||
- `draft_id` - The value is always `3001` for Windtrace.
|
||||
- `is_agree` - A boolean value for whether the client accepts the invite.
|
||||
|
||||
## `DraftGuestReplyInviteRsp`
|
||||
- `draft_id` - The value is always `3001` for Windtrace.
|
||||
- `retcode` - Response code for the request.
|
||||
- `is_agree` - A boolean value for whether the server acknowledges the client's invite acceptation.
|
||||
|
||||
## `DraftInviteResultNotify`
|
||||
- `draft_id` - The value is always `3001` for Windtrace.
|
||||
- `is_all_agree` - A boolean value for whether all clients accepted the invite.
|
||||
|
||||
## `NpcTalkStateNotify`
|
||||
- `is_ban` - This value is always true when entering Windtrace.
|
||||
|
||||
## `PlayerEnterSceneNotify`
|
||||
- `pos` - This is where the player will be teleported to.
|
||||
- This value depends on if the player is a hunter or a runner.
|
||||
- This value is set by the server and must be hardcoded/read from a JSON file.
|
||||
|
||||
## `MultistagePlayStageEndNotify`
|
||||
- `play_index` - Value picked by the server. (use 1)
|
||||
- `group_id` - This value is always `133002121` for Windtrace.
|
||||
|
||||
## `MultistagePlayInfoNotify` - Initial + PostEnterSceneReq
|
||||
- Image Reference: 
|
||||
- `info` - MultistagePlayInfo data.
|
||||
- `group_id` - The value is always `133002121` for Windtrace.
|
||||
- `play_index` - Value picked by the server. (use 1)
|
||||
- `hide_and_seek_info` - Information about Windtrace.
|
||||
- `hider_uid_list` - A list of UIDs (ints) of the hiders.
|
||||
- `hunter_uid` - The UID (int) of the hunter.
|
||||
- `map_id` - The ID of the Windtrace map.
|
||||
- `stage_type` - Windtrace state.
|
||||
- This will be `HIDE_AND_SEEK_STAGE_TYPE_PREPARE`.
|
||||
- `battle_info_map` - Contains a dictionary of UID -> `HideAndSeekPlayerBattleInfo` objects.
|
||||
- `skill_list` - Array of 3 values of skill IDs chosen by the player.
|
||||
- `avatar_id` - The ID of the avatar the player wants to use.
|
||||
- `is_ready` - The player's in-game ready state.
|
||||
- `costume_id` - The costume the player's avatar is wearing.
|
||||
|
||||
## `MultistagePlayInfoNotify` - Picking Avatars
|
||||
- Image Reference: 
|
||||
- **Note:** This packet matches the initial structure and data.
|
||||
- `info.hide_and_seek_info.stage_type` - This will be `HIDE_AND_SEEK_STAGE_TYPE_PICK`.
|
||||
|
||||
## `MultistagePlayInfoNotify` - Starting Windtrace
|
||||
- Image Reference: 
|
||||
- **Note:** This packet matches the initial structure and data.
|
||||
- `info.hide_and_seek_info.stage_type` - This will be `HIDE_AND_SEEK_STAGE_TYPE_HIDE`.
|
||||
|
||||
## `MultistagePlayInfoNotify` - Seeking Time
|
||||
- Image Reference: 
|
||||
- **Note:** This packet matches the initial structure and data.
|
||||
- `info.hide_and_seek_info.stage_type` - This will be `HIDE_AND_SEEK_STAGE_TYPE_SEEK`.
|
||||
|
||||
## `MultistagePlayInfoNotify` - Finish Windtrace
|
||||
- Image Reference: 
|
||||
- **Note:** This packet matches the initial structure and data.
|
||||
- `info.hide_and_seek_info.stage_type` - This will be `HIDE_AND_SEEK_STAGE_TYPE_SETTLE`.
|
||||
|
||||
## `HideAndSeekPlayerSetAvatarNotify`
|
||||
- `avatar_id` - The ID of the new avatar the player wants to use.
|
||||
- `uid` - The UID of the player who changed their avatar.
|
||||
- `costume_id` - The costume the player's avatar is wearing.
|
||||
|
||||
## `HideAndSeekSetReadyRsp`
|
||||
- `retcode` - Response code for the request.
|
||||
|
||||
## `HideAndSeekPlayerReadyNotify`
|
||||
- `uid_list` - A list of UIDs (ints) of the players who are ready.
|
||||
|
||||
## `GalleryStartNotify`
|
||||
- `gallery_id` - TODO: Check if this value is always `7056` for Windtrace.
|
||||
- `start_time` - This value is always `2444` for Windtrace.
|
||||
- This value is `200` when displaying game end statistics.
|
||||
- `owner_uid` - The UID of the player who started the Windtrace game.
|
||||
- `player_count` - The number of players in the Windtrace game.
|
||||
- `end_time` - This value is always the same as `start_time`.
|
||||
|
||||
## `SceneGalleryInfoNotify` - Starting Windtrace
|
||||
- `gallery_info` - SceneGalleryInfo data.
|
||||
- `end_time` - This value is always the same as `start_time`.
|
||||
- `start_time` - This value is always `2444` for Windtrace.
|
||||
- This value is `200` when displaying game end statistics.
|
||||
- `gallery_id` - This value is always the same as `gallery_id` from `GalleryStartNotify`.
|
||||
- `stage` - The current stage of the gallery.
|
||||
- This will be `GALLERY_STAGE_TYPE_START`.
|
||||
- `owner_uid` - The UID of the player who started the Windtrace game.
|
||||
- `hide_and_seek_info` - SceneGalleryHideAndSeekInfo
|
||||
- `visible_uid_list` - List of UIDs (ints) of the players who were left alive.
|
||||
- `caught_uid_list` - List of UIDs (ints) of the players who have been caught.
|
||||
- `player_count` - The amount of players in the Windtrace game.
|
||||
- `pre_start_end_time` - This value is always `0` for Windtrace.
|
||||
|
||||
## `HideAndSeekSettleNotify`
|
||||
- `reason` - The reason for the game ending.
|
||||
- `winner_list` - A list of UIDs (ints) of the players who won the game.
|
||||
- `settle_info_list` - HideAndSeekSettleInfo data.
|
||||
- This is a list of players who participated in the game.
|
||||
|
||||
## `HideAndSeekSettleInfo`
|
||||
- `card_list` - A collection of `ExhibitionDisplayInfo`
|
||||
- If unknown: hardcode the specified values. 
|
||||
- These values are repeated during testing.
|
||||
- `uid` - The UID of the player who participated in the game.
|
||||
- `nickname` - The player's nickname.
|
||||
- `head_image` - This value is always `0`.
|
||||
- `online_id` - This value is always blank.
|
||||
- `profile_picture` - `ProfilePicture` object.
|
||||
- `play_index` - Value picked by the server. (use 1)
|
||||
- `stage_type` - The stage type. (inconclusive; TODO)
|
||||
- `cost_time` - The amount of time the player took to complete the game.
|
||||
- `score_list` - A list of player scores.
|
||||
|
||||
## `ExhibitionDisplayInfo`
|
||||
- `id` - The ID of the reward.
|
||||
- `param` - The amount of the reward given.
|
||||
- `detail_param` - This value is *mostly* 0.
|
||||
- This value **matches** param when the reward is of the amount of time spent playing. (participation reward)
|
BIN
docs/events/windtrace/images/defaultexhibitioninfo.png
Normal file
BIN
docs/events/windtrace/images/defaultexhibitioninfo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
BIN
docs/events/windtrace/images/multistageplayinfo.png
Normal file
BIN
docs/events/windtrace/images/multistageplayinfo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
BIN
docs/events/windtrace/images/pickavatar.png
Normal file
BIN
docs/events/windtrace/images/pickavatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 42 KiB |
BIN
docs/events/windtrace/images/seektime.png
Normal file
BIN
docs/events/windtrace/images/seektime.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
BIN
docs/events/windtrace/images/startwindtrace.png
Normal file
BIN
docs/events/windtrace/images/startwindtrace.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
@ -68,7 +68,7 @@ class MlgmXyysd_Animation_Company_Proxy:
|
||||
]
|
||||
|
||||
def request(self, flow: http.HTTPFlow) -> None:
|
||||
if flow.request.host in self.LIST_DOMAINS:
|
||||
if flow.request.pretty_host in self.LIST_DOMAINS:
|
||||
if USE_SSL:
|
||||
flow.request.scheme = "https"
|
||||
else:
|
||||
|
@ -1,5 +1,8 @@
|
||||
package emu.grasscutter;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.SERVER;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
import ch.qos.logback.classic.*;
|
||||
import emu.grasscutter.auth.*;
|
||||
import emu.grasscutter.command.*;
|
||||
@ -18,20 +21,16 @@ import emu.grasscutter.tools.Tools;
|
||||
import emu.grasscutter.utils.*;
|
||||
import emu.grasscutter.utils.lang.Language;
|
||||
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||
import java.io.*;
|
||||
import java.util.Calendar;
|
||||
import java.util.concurrent.*;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.*;
|
||||
import org.jline.reader.*;
|
||||
import org.jline.terminal.*;
|
||||
import org.reflections.Reflections;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.*;
|
||||
import java.util.Calendar;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.SERVER;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
public final class Grasscutter {
|
||||
public static final File configFile = new File("./config.json");
|
||||
public static final Reflections reflector = new Reflections("emu.grasscutter");
|
||||
@ -159,6 +158,8 @@ public final class Grasscutter {
|
||||
|
||||
// Generate handbooks.
|
||||
Tools.createGmHandbooks(false);
|
||||
// Generate gacha mappings.
|
||||
Tools.generateGachaMappings();
|
||||
}
|
||||
|
||||
// Start servers.
|
||||
@ -182,18 +183,12 @@ public final class Grasscutter {
|
||||
// Hook into shutdown event.
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown));
|
||||
|
||||
// Start database heartbeat.
|
||||
Database.startSaveThread();
|
||||
|
||||
// Open console.
|
||||
Grasscutter.startConsole();
|
||||
}
|
||||
|
||||
/** Server shutdown event. */
|
||||
private static void onShutdown() {
|
||||
// Save all data.
|
||||
Database.saveAll();
|
||||
|
||||
// Disable all plugins.
|
||||
if (pluginManager != null) pluginManager.disablePlugins();
|
||||
// Shutdown the game server.
|
||||
@ -203,14 +198,14 @@ public final class Grasscutter {
|
||||
// Wait for Grasscutter's thread pool to finish.
|
||||
var executor = Grasscutter.getThreadPool();
|
||||
executor.shutdown();
|
||||
if (!executor.awaitTermination(1, TimeUnit.MINUTES)) {
|
||||
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
// Wait for database operations to finish.
|
||||
var dbExecutor = DatabaseHelper.getEventExecutor();
|
||||
dbExecutor.shutdown();
|
||||
if (!dbExecutor.awaitTermination(2, TimeUnit.MINUTES)) {
|
||||
if (!dbExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
dbExecutor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
package emu.grasscutter.command;
|
||||
|
||||
import emu.grasscutter.game.world.Position;
|
||||
import java.util.*;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.regex.*;
|
||||
@ -54,4 +55,78 @@ public class CommandHelpers {
|
||||
});
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -32,9 +32,12 @@ public final class DebugCommand implements CommandHandler {
|
||||
|
||||
var scene = sender.getScene();
|
||||
var entityId = Integer.parseInt(args.get(0));
|
||||
// TODO Might want to allow groupId specification,
|
||||
// because there can be more than one entity with
|
||||
// the given config ID.
|
||||
var entity =
|
||||
args.size() > 1 && args.get(1).equals("config")
|
||||
? scene.getEntityByConfigId(entityId)
|
||||
? scene.getFirstEntityByConfigId(entityId)
|
||||
: scene.getEntityById(entityId);
|
||||
if (entity == null) {
|
||||
sender.dropMessage("Entity not found.");
|
||||
|
@ -51,7 +51,10 @@ public final class EntityCommand implements CommandHandler {
|
||||
}
|
||||
|
||||
param.scene = targetPlayer.getScene();
|
||||
var entity = param.scene.getEntityByConfigId(param.configId);
|
||||
// TODO Might want to allow groupId specification,
|
||||
// because there can be more than one entity with
|
||||
// the given config ID.
|
||||
var entity = param.scene.getFirstEntityByConfigId(param.configId);
|
||||
|
||||
if (entity == null) {
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.entity.not_found_error"));
|
||||
|
@ -12,7 +12,7 @@ import lombok.val;
|
||||
@Command(
|
||||
label = "setSceneTag",
|
||||
aliases = {"tag"},
|
||||
usage = {"<add|remove|unlockall> <sceneTagId>"},
|
||||
usage = {"<add|remove|unlockall|reset> <sceneTagId>"},
|
||||
permission = "player.setscenetag",
|
||||
permissionTargeted = "player.setscenetag.others")
|
||||
public final class SetSceneTagCommand implements CommandHandler {
|
||||
@ -20,7 +20,7 @@ public final class SetSceneTagCommand implements CommandHandler {
|
||||
|
||||
@Override
|
||||
public void execute(Player sender, Player targetPlayer, List<String> args) {
|
||||
if (args.size() == 0) {
|
||||
if (args.isEmpty()) {
|
||||
sendUsageMessage(sender);
|
||||
return;
|
||||
}
|
||||
@ -39,6 +39,9 @@ public final class SetSceneTagCommand implements CommandHandler {
|
||||
if (actionStr.equals("unlockall")) {
|
||||
unlockAllSceneTags(targetPlayer);
|
||||
return;
|
||||
} else if (actionStr.equals("reset") || actionStr.equals("restore")) {
|
||||
resetAllSceneTags(targetPlayer);
|
||||
return;
|
||||
} else {
|
||||
CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
|
||||
return;
|
||||
@ -49,7 +52,7 @@ public final class SetSceneTagCommand implements CommandHandler {
|
||||
|
||||
var sceneData =
|
||||
sceneTagData.values().stream().filter(sceneTag -> sceneTag.getId() == userVal).findFirst();
|
||||
if (sceneData == null) {
|
||||
if (sceneData.isEmpty()) {
|
||||
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.id");
|
||||
return;
|
||||
}
|
||||
@ -80,15 +83,15 @@ public final class SetSceneTagCommand implements CommandHandler {
|
||||
.toList()
|
||||
.forEach(
|
||||
sceneTag -> {
|
||||
if (targetPlayer.getSceneTags().get(sceneTag.getSceneId()) == null) {
|
||||
targetPlayer.getSceneTags().put(sceneTag.getSceneId(), new HashSet<>());
|
||||
}
|
||||
targetPlayer
|
||||
.getSceneTags()
|
||||
.computeIfAbsent(sceneTag.getSceneId(), k -> new HashSet<>());
|
||||
targetPlayer.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId());
|
||||
});
|
||||
|
||||
// Remove default SceneTags, as most are "before" or "locked" states
|
||||
allData.stream()
|
||||
.filter(sceneTag -> sceneTag.isDefaultValid())
|
||||
.filter(SceneTagData::isDefaultValid)
|
||||
// Only remove for big world as some other scenes only have defaults
|
||||
.filter(sceneTag -> sceneTag.getSceneId() == 3)
|
||||
.forEach(
|
||||
@ -99,6 +102,22 @@ public final class SetSceneTagCommand implements CommandHandler {
|
||||
this.setSceneTags(targetPlayer);
|
||||
}
|
||||
|
||||
private void resetAllSceneTags(Player targetPlayer) {
|
||||
targetPlayer.getSceneTags().clear();
|
||||
// targetPlayer.applyStartingSceneTags(); // private
|
||||
GameData.getSceneTagDataMap().values().stream()
|
||||
.filter(SceneTagData::isDefaultValid)
|
||||
.forEach(
|
||||
sceneTag -> {
|
||||
targetPlayer
|
||||
.getSceneTags()
|
||||
.computeIfAbsent(sceneTag.getSceneId(), k -> new HashSet<>());
|
||||
targetPlayer.getSceneTags().get(sceneTag.getSceneId()).add(sceneTag.getId());
|
||||
});
|
||||
|
||||
this.setSceneTags(targetPlayer);
|
||||
}
|
||||
|
||||
private void setSceneTags(Player targetPlayer) {
|
||||
targetPlayer.sendPacket(new PacketPlayerWorldSceneInfoListNotify(targetPlayer));
|
||||
}
|
||||
|
@ -21,9 +21,9 @@ import lombok.Setter;
|
||||
label = "spawn",
|
||||
aliases = {"drop", "s"},
|
||||
usage = {
|
||||
"<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>",
|
||||
"<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>"
|
||||
"<itemId> [x<amount>] [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>] [<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>] [<rotX> <rotY> <rotZ>]"
|
||||
},
|
||||
permission = "server.spawn",
|
||||
permissionTargeted = "server.spawn.others")
|
||||
@ -53,14 +53,23 @@ public final class SpawnCommand implements CommandHandler {
|
||||
sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
Position pos = new Position(targetPlayer.getPosition());
|
||||
Position rot = new Position(targetPlayer.getRotation());
|
||||
|
||||
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:
|
||||
try {
|
||||
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);
|
||||
pos = CommandHelpers.parsePosition(args.get(1), args.get(2), args.get(3), pos, rot);
|
||||
} catch (NumberFormatException ignored) {
|
||||
CommandHandler.sendMessage(
|
||||
sender, translate(sender, "commands.execution.argument_error"));
|
||||
@ -77,6 +86,8 @@ public final class SpawnCommand implements CommandHandler {
|
||||
sendUsageMessage(sender);
|
||||
return;
|
||||
}
|
||||
param.pos = pos;
|
||||
param.rot = rot;
|
||||
|
||||
MonsterData monsterData = GameData.getMonsterDataMap().get(param.id);
|
||||
GadgetData gadgetData = GameData.getGadgetDataMap().get(param.id);
|
||||
@ -102,12 +113,8 @@ public final class SpawnCommand implements CommandHandler {
|
||||
}
|
||||
|
||||
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++) {
|
||||
Position pos = GetRandomPositionInCircle(param.pos, maxRadius).addY(3);
|
||||
pos = GetRandomPositionInCircle(param.pos, maxRadius).addY(3);
|
||||
GameEntity entity = null;
|
||||
if (itemData != null) {
|
||||
entity = createItem(itemData, param, pos);
|
||||
@ -128,12 +135,12 @@ public final class SpawnCommand implements CommandHandler {
|
||||
}
|
||||
|
||||
private EntityItem createItem(ItemData itemData, SpawnParameters param, Position pos) {
|
||||
return new EntityItem(param.scene, null, itemData, pos, 1, true);
|
||||
return new EntityItem(param.scene, null, itemData, pos, param.rot, 1, true);
|
||||
}
|
||||
|
||||
private EntityMonster createMonster(
|
||||
MonsterData monsterData, SpawnParameters param, Position pos) {
|
||||
var entity = new EntityMonster(param.scene, monsterData, pos, param.lvl);
|
||||
var entity = new EntityMonster(param.scene, monsterData, pos, param.rot, param.lvl);
|
||||
if (param.ai != -1) {
|
||||
entity.setAiId(param.ai);
|
||||
}
|
||||
@ -144,16 +151,13 @@ public final class SpawnCommand implements CommandHandler {
|
||||
GadgetData gadgetData, SpawnParameters param, Position pos, Player targetPlayer) {
|
||||
EntityBaseGadget entity;
|
||||
if (gadgetData.getType() == EntityType.Vehicle) {
|
||||
entity =
|
||||
new EntityVehicle(
|
||||
param.scene, targetPlayer, param.id, 0, pos, targetPlayer.getRotation());
|
||||
entity = new EntityVehicle(param.scene, targetPlayer, param.id, 0, pos, param.rot);
|
||||
} else {
|
||||
entity = new EntityGadget(param.scene, param.id, pos, targetPlayer.getRotation());
|
||||
entity = new EntityGadget(param.scene, param.id, pos, param.rot);
|
||||
if (param.state != -1) {
|
||||
((EntityGadget) entity).setState(param.state);
|
||||
}
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
@ -207,6 +211,7 @@ public final class SpawnCommand implements CommandHandler {
|
||||
@Setter public int def = -1;
|
||||
@Setter public int ai = -1;
|
||||
@Setter public Position pos = null;
|
||||
@Setter public Position rot = null;
|
||||
public Scene scene = null;
|
||||
}
|
||||
}
|
||||
|
@ -16,24 +16,10 @@ import java.util.List;
|
||||
permissionTargeted = "player.teleport.others")
|
||||
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
|
||||
public void execute(Player sender, Player targetPlayer, List<String> args) {
|
||||
Position pos = targetPlayer.getPosition();
|
||||
float x = pos.getX();
|
||||
float y = pos.getY();
|
||||
float z = pos.getZ();
|
||||
Position pos = new Position(targetPlayer.getPosition());
|
||||
Position rot = new Position(targetPlayer.getRotation());
|
||||
int sceneId = targetPlayer.getSceneId();
|
||||
|
||||
switch (args.size()) {
|
||||
@ -46,9 +32,7 @@ public final class TeleportCommand implements CommandHandler {
|
||||
} // Fallthrough
|
||||
case 3:
|
||||
try {
|
||||
x = this.parseRelative(args.get(0), x);
|
||||
y = this.parseRelative(args.get(1), y);
|
||||
z = this.parseRelative(args.get(2), z);
|
||||
pos = CommandHelpers.parsePosition(args.get(0), args.get(1), args.get(2), pos, rot);
|
||||
} catch (NumberFormatException ignored) {
|
||||
CommandHandler.sendMessage(
|
||||
sender, translate(sender, "commands.teleport.invalid_position"));
|
||||
@ -59,11 +43,10 @@ public final class TeleportCommand implements CommandHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
Position target_pos = new Position(x, y, z);
|
||||
boolean result =
|
||||
targetPlayer
|
||||
.getWorld()
|
||||
.transferPlayerToScene(targetPlayer, sceneId, TeleportType.COMMAND, target_pos);
|
||||
.transferPlayerToScene(targetPlayer, sceneId, TeleportType.COMMAND, pos);
|
||||
|
||||
if (!result) {
|
||||
CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.exists_error"));
|
||||
@ -71,7 +54,13 @@ public final class TeleportCommand implements CommandHandler {
|
||||
CommandHandler.sendMessage(
|
||||
sender,
|
||||
translate(
|
||||
sender, "commands.teleport.success", targetPlayer.getNickname(), x, y, z, sceneId));
|
||||
sender,
|
||||
"commands.teleport.success",
|
||||
targetPlayer.getNickname(),
|
||||
pos.getX(),
|
||||
pos.getY(),
|
||||
pos.getZ(),
|
||||
sceneId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
package emu.grasscutter.data;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.server.http.handlers.GachaHandler;
|
||||
import emu.grasscutter.tools.Tools;
|
||||
import emu.grasscutter.utils.*;
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
@ -114,8 +112,6 @@ public class DataLoader {
|
||||
} catch (Exception e) {
|
||||
Grasscutter.getLogger().error("An error occurred while trying to check the data folder.", e);
|
||||
}
|
||||
|
||||
generateGachaMappings();
|
||||
}
|
||||
|
||||
private static void checkAndCopyData(String name) {
|
||||
@ -131,16 +127,4 @@ public class DataLoader {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,6 +214,14 @@ public final class GameData {
|
||||
private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap =
|
||||
new Int2ObjectOpenHashMap<>();
|
||||
|
||||
@Getter
|
||||
private static final Int2ObjectMap<CoopChapterData> coopChapterDataMap =
|
||||
new Int2ObjectOpenHashMap<>();
|
||||
|
||||
@Getter
|
||||
private static final Int2ObjectMap<CoopPointData> coopPointDataMap =
|
||||
new Int2ObjectOpenHashMap<>();
|
||||
|
||||
@Getter
|
||||
private static final Int2ObjectMap<CompoundData> compoundDataMap = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
|
@ -19,6 +19,7 @@ public final class PointData {
|
||||
@Getter private Position size;
|
||||
@Getter private boolean forbidSimpleUnlock;
|
||||
@Getter private boolean unlocked;
|
||||
@Getter private boolean groupLimit;
|
||||
|
||||
@SerializedName(
|
||||
value = "dungeonIds",
|
||||
@ -28,7 +29,7 @@ public final class PointData {
|
||||
|
||||
@SerializedName(
|
||||
value = "dungeonRandomList",
|
||||
alternate = {"OIBKFJNBLHO"})
|
||||
alternate = {"GLEKJMEEOMH"})
|
||||
@Getter
|
||||
private int[] dungeonRandomList;
|
||||
|
||||
|
@ -0,0 +1,46 @@
|
||||
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;
|
||||
}
|
||||
}
|
24
src/main/java/emu/grasscutter/data/excels/CoopPointData.java
Normal file
24
src/main/java/emu/grasscutter/data/excels/CoopPointData.java
Normal file
@ -0,0 +1,24 @@
|
||||
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;
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
package emu.grasscutter.data.excels.dungeon;
|
||||
|
||||
import emu.grasscutter.data.*;
|
||||
import emu.grasscutter.game.dungeons.enums.*;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
|
||||
@ResourceType(name = "DungeonEntryExcelConfigData.json")
|
||||
@ -12,4 +14,46 @@ public class DungeonEntryData extends GameResource {
|
||||
|
||||
private int dungeonEntryId;
|
||||
private int sceneId;
|
||||
private DungunEntryType type;
|
||||
private DungeonEntryCondCombType condComb;
|
||||
private List<DungeonEntryCondition> satisfiedCond;
|
||||
|
||||
public static class DungeonEntryCondition {
|
||||
private DungeonEntrySatisfiedConditionType type;
|
||||
private int param1;
|
||||
}
|
||||
|
||||
public DungunEntryType getType() {
|
||||
if (type == null) {
|
||||
return DungunEntryType.DUNGEN_ENTRY_TYPE_NONE;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
public DungeonEntryCondCombType getCondComb() {
|
||||
if (condComb == null) {
|
||||
return DungeonEntryCondCombType.DUNGEON_ENTRY_COND_COMB_NONE;
|
||||
}
|
||||
return condComb;
|
||||
}
|
||||
|
||||
public int getLevelCondition() {
|
||||
for (var cond : satisfiedCond) {
|
||||
if (cond.type != null
|
||||
&& cond.type.equals(DungeonEntrySatisfiedConditionType.DUNGEON_ENTRY_CONDITION_LEVEL)) {
|
||||
return cond.param1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getQuestCondition() {
|
||||
for (var cond : satisfiedCond) {
|
||||
if (cond.type != null
|
||||
&& cond.type.equals(DungeonEntrySatisfiedConditionType.DUNGEON_ENTRY_CONDITION_QUEST)) {
|
||||
return cond.param1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,82 @@
|
||||
package emu.grasscutter.data.excels.tower;
|
||||
|
||||
import emu.grasscutter.data.*;
|
||||
import java.util.List;
|
||||
import lombok.*;
|
||||
|
||||
@ResourceType(name = "TowerLevelExcelConfigData.json")
|
||||
@Getter
|
||||
public class TowerLevelData extends GameResource {
|
||||
|
||||
private int levelId;
|
||||
private int levelIndex;
|
||||
private int levelGroupId;
|
||||
private int dungeonId;
|
||||
private List<TowerLevelCond> conds;
|
||||
|
||||
public static class TowerLevelCond {
|
||||
private TowerCondType towerCondType;
|
||||
private List<Integer> argumentList;
|
||||
}
|
||||
|
||||
public enum TowerCondType {
|
||||
TOWER_COND_NONE,
|
||||
TOWER_COND_CHALLENGE_LEFT_TIME_MORE_THAN,
|
||||
TOWER_COND_LEFT_HP_GREATER_THAN
|
||||
}
|
||||
|
||||
// Not actual data in TowerLevelExcelConfigData.
|
||||
// Just packaging condition parameters for convenience.
|
||||
@Getter
|
||||
public class TowerCondTimeParams {
|
||||
private int param1;
|
||||
private int minimumTimeInSeconds;
|
||||
|
||||
public TowerCondTimeParams(int param1, int minimumTimeInSeconds) {
|
||||
this.param1 = param1;
|
||||
this.minimumTimeInSeconds = minimumTimeInSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public class TowerCondHpParams {
|
||||
private int sceneId;
|
||||
private int configId;
|
||||
private int minimumHpPercentage;
|
||||
|
||||
public TowerCondHpParams(int sceneId, int configId, int minimumHpPercentage) {
|
||||
this.sceneId = sceneId;
|
||||
this.configId = configId;
|
||||
this.minimumHpPercentage = minimumHpPercentage;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return this.getLevelId();
|
||||
}
|
||||
|
||||
public int getLevelId() {
|
||||
return levelId;
|
||||
public TowerCondType getCondType(int star) {
|
||||
if (star < 0 || conds == null || star >= conds.size()) {
|
||||
return TowerCondType.TOWER_COND_NONE;
|
||||
}
|
||||
var condType = conds.get(star).towerCondType;
|
||||
return condType == null ? TowerCondType.TOWER_COND_NONE : condType;
|
||||
}
|
||||
|
||||
public int getLevelGroupId() {
|
||||
return levelGroupId;
|
||||
public TowerCondTimeParams getTimeCond(int star) {
|
||||
if (star < 0 || conds == null || star >= conds.size()) {
|
||||
return null;
|
||||
}
|
||||
var params = conds.get(star).argumentList;
|
||||
return new TowerCondTimeParams(params.get(0), params.get(1));
|
||||
}
|
||||
|
||||
public int getLevelIndex() {
|
||||
return levelIndex;
|
||||
public TowerCondHpParams getHpCond(int star) {
|
||||
if (star < 0 || conds == null || star >= conds.size()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getDungeonId() {
|
||||
return dungeonId;
|
||||
var params = conds.get(star).argumentList;
|
||||
return new TowerCondHpParams(params.get(0), params.get(1), params.get(2));
|
||||
}
|
||||
}
|
||||
|
@ -1,85 +0,0 @@
|
||||
package emu.grasscutter.database;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.utils.objects.DatabaseObject;
|
||||
import org.slf4j.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* Complicated manager of the MongoDB database.
|
||||
* Handles caching, data operations, and more.
|
||||
*/
|
||||
public interface Database {
|
||||
Logger logger = LoggerFactory.getLogger("Database");
|
||||
List<DatabaseObject<?>> objects = new CopyOnWriteArrayList<>();
|
||||
|
||||
/**
|
||||
* Queues an object to be saved.
|
||||
*
|
||||
* @param object The object to save.
|
||||
*/
|
||||
static void save(DatabaseObject<?> object) {
|
||||
if (object.saveImmediately()) {
|
||||
object.save();
|
||||
} else {
|
||||
objects.add(object);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bulk save of all deferred objects.
|
||||
*/
|
||||
static void saveAll() {
|
||||
var size = objects.size();
|
||||
Database.saveAll(objects);
|
||||
|
||||
logger.debug("Performed auto save on {} objects.", size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bulk save of all deferred objects.
|
||||
*
|
||||
* @param objects The objects to save.
|
||||
*/
|
||||
static void saveAll(List<? extends DatabaseObject<?>> objects) {
|
||||
// Sort all objects into their respective databases.
|
||||
var gameObjects = objects.stream()
|
||||
.filter(DatabaseObject::isGameObject)
|
||||
.toList();
|
||||
var accountObjects = objects.stream()
|
||||
.filter(o -> !o.isGameObject())
|
||||
.toList();
|
||||
|
||||
// Clear the collective list.
|
||||
objects.clear();
|
||||
|
||||
// Save all objects.
|
||||
var executor = DatabaseHelper.getEventExecutor();
|
||||
if (Grasscutter.getRunMode() != Grasscutter.ServerRunMode.DISPATCH_ONLY) {
|
||||
executor.submit(() -> {
|
||||
DatabaseManager.getGameDatastore().save(gameObjects);
|
||||
});
|
||||
}
|
||||
if (Grasscutter.getRunMode() != Grasscutter.ServerRunMode.GAME_ONLY) {
|
||||
executor.submit(() -> {
|
||||
DatabaseManager.getAccountDatastore().save(accountObjects);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the auto-save thread.
|
||||
* Runs every 5 minutes.
|
||||
*/
|
||||
static void startSaveThread() {
|
||||
var timer = new Timer();
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
Database.saveAll();
|
||||
}
|
||||
}, 0, 1000 * 60 * 5);
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package emu.grasscutter.database;
|
||||
|
||||
import static com.mongodb.client.model.Filters.eq;
|
||||
|
||||
import dev.morphia.query.*;
|
||||
import dev.morphia.query.experimental.filters.Filters;
|
||||
import emu.grasscutter.*;
|
||||
@ -18,19 +20,24 @@ import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.GameMainQuest;
|
||||
import emu.grasscutter.game.world.SceneGroupInstance;
|
||||
import emu.grasscutter.utils.objects.Returnable;
|
||||
import lombok.Getter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static com.mongodb.client.model.Filters.eq;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
|
||||
public final class DatabaseHelper {
|
||||
@Getter
|
||||
private static final ExecutorService eventExecutor =
|
||||
Executors.newFixedThreadPool(4);
|
||||
new ThreadPoolExecutor(
|
||||
6,
|
||||
6,
|
||||
60,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingDeque<>(),
|
||||
FastThreadLocalThread::new,
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
|
||||
/**
|
||||
* Saves an object on the account datastore.
|
||||
|
@ -241,7 +241,8 @@ public interface HandbookActions {
|
||||
|
||||
// Create the entity.
|
||||
for (var i = 1; i <= request.getAmount(); i++) {
|
||||
var entity = new EntityMonster(scene, entityData, player.getPosition(), level);
|
||||
var entity =
|
||||
new EntityMonster(scene, entityData, player.getPosition(), player.getRotation(), level);
|
||||
scene.addEntity(entity);
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import emu.grasscutter.data.binout.*;
|
||||
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
|
||||
import emu.grasscutter.game.ability.actions.*;
|
||||
import emu.grasscutter.game.ability.mixins.*;
|
||||
import emu.grasscutter.game.entity.EntityAvatar;
|
||||
import emu.grasscutter.game.entity.GameEntity;
|
||||
import emu.grasscutter.game.player.*;
|
||||
import emu.grasscutter.game.props.FightProperty;
|
||||
@ -562,6 +563,14 @@ public final class AbilityManager extends BasePlayerManager {
|
||||
if (killState.getKilled()) {
|
||||
scene.killEntity(entity);
|
||||
} else if (!entity.isAlive()) {
|
||||
if (entity instanceof EntityAvatar) {
|
||||
// TODO Should EntityAvatar act on this invocation?
|
||||
// It bugs revival due to resetting HP to max when
|
||||
// the avatar should just stay dead.
|
||||
Grasscutter.getLogger()
|
||||
.trace("Entity of ID {} is EntityAvatar. Ignoring", invoke.getEntityId());
|
||||
return;
|
||||
}
|
||||
entity.setFightProperty(
|
||||
FightProperty.FIGHT_PROP_CUR_HP,
|
||||
entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP));
|
||||
|
@ -12,13 +12,12 @@ import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.net.proto.AchievementOuterClass.Achievement.Status;
|
||||
import emu.grasscutter.server.event.player.PlayerCompleteAchievementEvent;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import lombok.*;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.IntSupplier;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.*;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
@Entity("achievements")
|
||||
@Data
|
||||
@ -45,30 +44,15 @@ public class Achievements {
|
||||
return achievements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blank achievements object.
|
||||
*
|
||||
* @return The achievements object.
|
||||
*/
|
||||
public static Achievements blank() {
|
||||
return Achievements.of()
|
||||
public static Achievements create(int uid) {
|
||||
var newAchievement =
|
||||
Achievements.of()
|
||||
.uid(uid)
|
||||
.achievementList(init())
|
||||
.finishedAchievementNum(0)
|
||||
.takenGoalRewardIdList(Lists.newArrayList())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and saves a blank achievements object.
|
||||
*
|
||||
* @param uid The UID of the player.
|
||||
* @return The achievements object.
|
||||
*/
|
||||
public static Achievements create(int uid) {
|
||||
var newAchievement = blank();
|
||||
newAchievement.setUid(uid);
|
||||
newAchievement.save();
|
||||
|
||||
return newAchievement;
|
||||
}
|
||||
|
||||
|
@ -11,17 +11,15 @@ import emu.grasscutter.game.props.ActionReason;
|
||||
import emu.grasscutter.net.proto.ActivityWatcherInfoOuterClass;
|
||||
import emu.grasscutter.server.packet.send.PacketActivityUpdateWatcherNotify;
|
||||
import emu.grasscutter.utils.JsonUtils;
|
||||
import emu.grasscutter.utils.objects.DatabaseObject;
|
||||
import java.util.*;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Entity("activities")
|
||||
@Data
|
||||
@FieldDefaults(level = AccessLevel.PRIVATE)
|
||||
@Builder(builderMethodName = "of")
|
||||
public class PlayerActivityData implements DatabaseObject<PlayerActivityData> {
|
||||
public class PlayerActivityData {
|
||||
@Id String id;
|
||||
int uid;
|
||||
int activityId;
|
||||
@ -36,25 +34,8 @@ public class PlayerActivityData implements DatabaseObject<PlayerActivityData> {
|
||||
return DatabaseHelper.getPlayerActivityData(player.getUid(), activityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save() {
|
||||
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();
|
||||
}
|
||||
DatabaseHelper.savePlayerActivityData(this);
|
||||
}
|
||||
|
||||
public synchronized void addWatcherProgress(int watcherId) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package emu.grasscutter.game.avatar;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
|
||||
import dev.morphia.annotations.*;
|
||||
import emu.grasscutter.GameConstants;
|
||||
import emu.grasscutter.data.GameData;
|
||||
@ -13,6 +15,7 @@ import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData.InherentProudSkil
|
||||
import emu.grasscutter.data.excels.reliquary.*;
|
||||
import emu.grasscutter.data.excels.trial.TrialAvatarTemplateData;
|
||||
import emu.grasscutter.data.excels.weapon.*;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.entity.*;
|
||||
import emu.grasscutter.game.inventory.*;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
@ -28,19 +31,15 @@ import emu.grasscutter.net.proto.TrialAvatarGrantRecordOuterClass.TrialAvatarGra
|
||||
import emu.grasscutter.net.proto.TrialAvatarInfoOuterClass.TrialAvatarInfo;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.helpers.ProtoHelper;
|
||||
import emu.grasscutter.utils.objects.DatabaseObject;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
import javax.annotation.*;
|
||||
import lombok.*;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import javax.annotation.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
|
||||
@Entity(value = "avatars", useDiscriminator = false)
|
||||
public class Avatar implements DatabaseObject<Avatar> {
|
||||
public class Avatar {
|
||||
@Transient @Getter private final Int2ObjectMap<GameItem> equips;
|
||||
@Transient @Getter private final Int2FloatOpenHashMap fightProperties;
|
||||
@Transient @Getter private final Int2FloatOpenHashMap fightPropOverrides;
|
||||
@ -990,25 +989,8 @@ public class Avatar implements DatabaseObject<Avatar> {
|
||||
return entity != null ? entity.getId() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save() {
|
||||
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();
|
||||
}
|
||||
DatabaseHelper.saveAvatar(this);
|
||||
}
|
||||
|
||||
public AvatarInfo toProto() {
|
||||
|
@ -60,7 +60,7 @@ public class AvatarStorage extends BasePlayerManager implements Iterable<Avatar>
|
||||
this.avatars.put(avatar.getAvatarId(), avatar);
|
||||
this.avatarsGuid.put(avatar.getGuid(), avatar);
|
||||
|
||||
avatar.save(true);
|
||||
avatar.save();
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -165,7 +165,7 @@ public class AvatarStorage extends BasePlayerManager implements Iterable<Avatar>
|
||||
if ((avatar.getAvatarId() == 10000007) || (avatar.getAvatarId() == 10000005)) {
|
||||
avatar.setSkillDepot(skillDepot);
|
||||
avatar.setSkillDepotData(skillDepot);
|
||||
avatar.save(true);
|
||||
avatar.save();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,12 +14,11 @@ import emu.grasscutter.net.proto.BattlePassRewardTakeOptionOuterClass.BattlePass
|
||||
import emu.grasscutter.net.proto.BattlePassScheduleOuterClass.BattlePassSchedule;
|
||||
import emu.grasscutter.net.proto.BattlePassUnlockStatusOuterClass.BattlePassUnlockStatus;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import lombok.Getter;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.*;
|
||||
import lombok.Getter;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
@Entity(value = "battlepass", useDiscriminator = false)
|
||||
public class BattlePassManager extends BasePlayerDataManager {
|
||||
@ -41,10 +40,7 @@ public class BattlePassManager extends BasePlayerDataManager {
|
||||
|
||||
public BattlePassManager(Player player) {
|
||||
super(player);
|
||||
|
||||
this.ownerUid = player.getUid();
|
||||
this.missions = new HashMap<>();
|
||||
this.takenRewards = new HashMap<>();
|
||||
}
|
||||
|
||||
public void setPlayer(Player player) {
|
||||
|
@ -38,6 +38,7 @@ public final class DungeonManager {
|
||||
private boolean ended = false;
|
||||
private int newestWayPoint = 0;
|
||||
@Getter private int startSceneTime = 0;
|
||||
@Setter @Getter private boolean towerDungeon = false;
|
||||
|
||||
DungeonTrialTeam trialTeam = null;
|
||||
|
||||
@ -323,15 +324,31 @@ public final class DungeonManager {
|
||||
p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_FINISH_DUNGEON);
|
||||
}
|
||||
});
|
||||
var future =
|
||||
scene
|
||||
.getScriptManager()
|
||||
.callEvent(new ScriptArgs(0, EventType.EVENT_DUNGEON_SETTLE, successfully ? 1 : 0));
|
||||
// Note: There is a possible race condition with calling
|
||||
// EVENT_DUNGEON_SETTLE here asynchronously:
|
||||
// 1. EVENT_DUNGEON_SETTLE triggers some Lua-side logic,
|
||||
// which may happen after 2 (below) finishes.
|
||||
// 2. Some DungeonSettleListener could be comparing some
|
||||
// Lua variable before its setting in 1 (above) finishes.
|
||||
// For safety, ensure all events have finished before returning.
|
||||
try {
|
||||
future.get();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void endDungeon(BaseDungeonResult.DungeonEndReason endReason) {
|
||||
if (scene.getDungeonSettleListeners() != null) {
|
||||
scene.getDungeonSettleListeners().forEach(o -> o.onDungeonSettle(this, endReason));
|
||||
}
|
||||
if (isTowerDungeon()) {
|
||||
scene.getPlayers().get(0).getTowerManager().onEnd();
|
||||
}
|
||||
ended = true;
|
||||
}
|
||||
|
||||
|
@ -131,7 +131,11 @@ public final class DungeonSystem extends BaseGameSystem {
|
||||
dungeonId);
|
||||
|
||||
if (player.getWorld().transferPlayerToScene(player, data.getSceneId(), data)) {
|
||||
dungeonSettleListeners.forEach(player.getScene()::addDungeonSettleObserver);
|
||||
var scene = player.getScene();
|
||||
var dungeonManager = new DungeonManager(scene, data);
|
||||
dungeonManager.setTowerDungeon(true);
|
||||
scene.setDungeonManager(dungeonManager);
|
||||
dungeonSettleListeners.forEach(scene::addDungeonSettleObserver);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -164,11 +168,40 @@ public final class DungeonSystem extends BaseGameSystem {
|
||||
dungeonManager.unsetTrialTeam(player);
|
||||
}
|
||||
// clean temp team if it has
|
||||
player.getTeamManager().cleanTemporaryTeam();
|
||||
if (!player.getTeamManager().cleanTemporaryTeam()) {
|
||||
// no temp team. Will use real current team, but check
|
||||
// for any dead avatar to prevent switching into them.
|
||||
player.getTeamManager().checkCurrentAvatarIsAlive(null);
|
||||
}
|
||||
player.getTowerManager().clearEntry();
|
||||
dungeonManager.setTowerDungeon(false);
|
||||
|
||||
// Transfer player back to world
|
||||
player.getWorld().transferPlayerToScene(player, prevScene, prevPos);
|
||||
player.sendPacket(new BasePacket(PacketOpcodes.PlayerQuitDungeonRsp));
|
||||
// Transfer player back to world after a small delay.
|
||||
// This wait is important for avoiding double teleports,
|
||||
// which specifically happen when player quits a dungeon
|
||||
// by teleporting to map waypoints.
|
||||
// From testing, 200ms seem reasonable.
|
||||
player.getWorld().queueTransferPlayerToScene(player, prevScene, prevPos, 200);
|
||||
}
|
||||
|
||||
public void restartDungeon(Player player) {
|
||||
var scene = player.getScene();
|
||||
var dungeonManager = scene.getDungeonManager();
|
||||
var dungeonData = dungeonManager.getDungeonData();
|
||||
var sceneId = dungeonData.getSceneId();
|
||||
|
||||
// Forward over previous scene and scene point
|
||||
var prevScene = scene.getPrevScene();
|
||||
var pointId = scene.getPrevScenePoint();
|
||||
|
||||
// Destroy then create scene again to reinitialize script state
|
||||
scene.getPlayers().forEach(scene::removePlayer);
|
||||
if (player.getWorld().transferPlayerToScene(player, sceneId, dungeonData)) {
|
||||
scene = player.getScene();
|
||||
scene.setPrevScene(prevScene);
|
||||
scene.setPrevScenePoint(pointId);
|
||||
scene.setDungeonManager(new DungeonManager(scene, dungeonData));
|
||||
scene.addDungeonSettleObserver(basicDungeonSettleObserver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ public class TowerDungeonSettleListener implements DungeonSettleListener {
|
||||
@Override
|
||||
public void onDungeonSettle(DungeonManager dungeonManager, DungeonEndReason endReason) {
|
||||
var scene = dungeonManager.getScene();
|
||||
|
||||
var dungeonData = dungeonManager.getDungeonData();
|
||||
if (scene.getLoadedGroups().stream()
|
||||
.anyMatch(
|
||||
@ -22,17 +23,18 @@ public class TowerDungeonSettleListener implements DungeonSettleListener {
|
||||
}
|
||||
|
||||
var towerManager = scene.getPlayers().get(0).getTowerManager();
|
||||
var stars = towerManager.getCurLevelStars();
|
||||
|
||||
towerManager.notifyCurLevelRecordChangeWhenDone(3);
|
||||
towerManager.notifyCurLevelRecordChangeWhenDone(stars);
|
||||
scene.broadcastPacket(
|
||||
new PacketTowerFloorRecordChangeNotify(
|
||||
towerManager.getCurrentFloorId(), 3, towerManager.canEnterScheduleFloor()));
|
||||
towerManager.getCurrentFloorId(), stars, towerManager.canEnterScheduleFloor()));
|
||||
|
||||
var challenge = scene.getChallenge();
|
||||
var dungeonStats =
|
||||
new DungeonEndStats(
|
||||
scene.getKilledMonsterCount(), challenge.getFinishedTime(), 0, endReason);
|
||||
var result = new TowerResult(dungeonData, dungeonStats, towerManager, challenge);
|
||||
var result = new TowerResult(dungeonData, dungeonStats, towerManager, challenge, stars);
|
||||
|
||||
scene.broadcastPacket(new PacketDungeonSettleNotify(result));
|
||||
}
|
||||
|
@ -80,9 +80,16 @@ public class WorldChallenge {
|
||||
return;
|
||||
}
|
||||
this.progress = true;
|
||||
this.startedAt = System.currentTimeMillis();
|
||||
this.startedAt = getScene().getSceneTimeSeconds();
|
||||
getScene().broadcastPacket(new PacketDungeonChallengeBeginNotify(this));
|
||||
challengeTriggers.forEach(t -> t.onBegin(this));
|
||||
|
||||
var player = scene.getPlayers().get(0);
|
||||
var dungeonManager = scene.getDungeonManager();
|
||||
var towerManager = player.getTowerManager();
|
||||
if (dungeonManager != null && dungeonManager.isTowerDungeon() && towerManager != null) {
|
||||
towerManager.onBegin();
|
||||
}
|
||||
}
|
||||
|
||||
public void done() {
|
||||
|
@ -1,10 +1,29 @@
|
||||
package emu.grasscutter.game.dungeons.challenge.trigger;
|
||||
|
||||
import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
|
||||
import emu.grasscutter.server.packet.send.PacketChallengeDataNotify;
|
||||
|
||||
public class InTimeTrigger extends ChallengeTrigger {
|
||||
@Override
|
||||
public void onBegin(WorldChallenge challenge) {
|
||||
// Show time remaining UI
|
||||
var scene = challenge.getScene();
|
||||
scene.broadcastPacket(
|
||||
new PacketChallengeDataNotify(
|
||||
challenge,
|
||||
2,
|
||||
// Compensate for time passed so far in scene.
|
||||
challenge.getTimeLimit() + scene.getSceneTimeSeconds()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckTimeout(WorldChallenge challenge) {
|
||||
// In Tower challenges, time can run out without
|
||||
// causing the challenge to fail. (Player just
|
||||
// gets 0 stars when they ultimately finish.)
|
||||
var dungeonManager = challenge.getScene().getDungeonManager();
|
||||
if (dungeonManager != null && dungeonManager.isTowerDungeon()) return;
|
||||
|
||||
var current = challenge.getScene().getSceneTimeSeconds();
|
||||
if (current - challenge.getStartedAt() > challenge.getTimeLimit()) {
|
||||
challenge.fail();
|
||||
|
@ -13,17 +13,20 @@ public class TowerResult extends BaseDungeonResult {
|
||||
boolean canJump;
|
||||
boolean hasNextLevel;
|
||||
int nextFloorId;
|
||||
int currentStars;
|
||||
|
||||
public TowerResult(
|
||||
DungeonData dungeonData,
|
||||
DungeonEndStats dungeonStats,
|
||||
TowerManager towerManager,
|
||||
WorldChallenge challenge) {
|
||||
WorldChallenge challenge,
|
||||
int currentStars) {
|
||||
super(dungeonData, dungeonStats);
|
||||
this.challenge = challenge;
|
||||
this.canJump = towerManager.hasNextFloor();
|
||||
this.hasNextLevel = towerManager.hasNextLevel();
|
||||
this.nextFloorId = hasNextLevel ? 0 : towerManager.getNextFloorId();
|
||||
this.currentStars = currentStars;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -40,14 +43,16 @@ public class TowerResult extends BaseDungeonResult {
|
||||
TowerLevelEndNotify.newBuilder()
|
||||
.setIsSuccess(challenge.isSuccess())
|
||||
.setContinueState(continueStatus)
|
||||
.addFinishedStarCondList(1)
|
||||
.addFinishedStarCondList(2)
|
||||
.addFinishedStarCondList(3)
|
||||
.addRewardItemList(
|
||||
ItemParamOuterClass.ItemParam.newBuilder().setItemId(201).setCount(1000).build());
|
||||
ItemParamOuterClass.ItemParam.newBuilder().setItemId(201).setCount(1000));
|
||||
|
||||
for (int i = 1; i <= currentStars; i++) {
|
||||
towerLevelEndNotify.addFinishedStarCondList(i);
|
||||
}
|
||||
|
||||
if (nextFloorId > 0 && canJump) {
|
||||
towerLevelEndNotify.setNextFloorId(nextFloorId);
|
||||
}
|
||||
builder.setTowerLevelEndNotify(towerLevelEndNotify);
|
||||
builder.setTowerLevelEndNotify(towerLevelEndNotify.build());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package emu.grasscutter.game.dungeons.enums;
|
||||
|
||||
public enum DungeonEntryCondCombType {
|
||||
DUNGEON_ENTRY_COND_COMB_NONE,
|
||||
DUNGEON_ENTRY_COND_COMB_LOGIC_OR,
|
||||
DUNGEON_ENTRY_COND_COMB_LOGIC_AND
|
||||
}
|
@ -67,6 +67,11 @@ public class EntityAvatar extends GameEntity {
|
||||
}
|
||||
|
||||
this.initAbilities();
|
||||
|
||||
// New EntityAvatar instances are created on every scene transition.
|
||||
// Ensure that isDead is properly carried over between scenes.
|
||||
// Otherwise avatars could have 0 HP but not considered dead.
|
||||
this.checkIfDead();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -18,8 +18,8 @@ public class EntityHomeAnimal extends EntityMonster implements Rebornable {
|
||||
@Getter private final int rebirthCD;
|
||||
private final AtomicBoolean disappeared = new AtomicBoolean();
|
||||
|
||||
public EntityHomeAnimal(Scene scene, HomeWorldAnimalData data, Position pos) {
|
||||
super(scene, GameData.getMonsterDataMap().get(data.getMonsterID()), pos, 1);
|
||||
public EntityHomeAnimal(Scene scene, HomeWorldAnimalData data, Position pos, Position rot) {
|
||||
super(scene, GameData.getMonsterDataMap().get(data.getMonsterID()), pos, rot, 1);
|
||||
|
||||
this.rebornPos = pos.clone();
|
||||
this.rebirth = data.getIsRebirth();
|
||||
|
@ -1,5 +1,7 @@
|
||||
package emu.grasscutter.game.entity;
|
||||
|
||||
import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_MONSTER_HP_CHANGE;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.binout.config.ConfigEntityMonster;
|
||||
import emu.grasscutter.data.common.PropGrowCurve;
|
||||
@ -28,12 +30,9 @@ import emu.grasscutter.scripts.data.*;
|
||||
import emu.grasscutter.server.event.entity.EntityDamageEvent;
|
||||
import emu.grasscutter.utils.helpers.ProtoHelper;
|
||||
import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap;
|
||||
import lombok.*;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
|
||||
import static emu.grasscutter.scripts.constants.EventType.EVENT_SPECIFIC_MONSTER_HP_CHANGE;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.*;
|
||||
|
||||
public class EntityMonster extends GameEntity {
|
||||
@Getter(onMethod_ = @Override)
|
||||
@ -41,8 +40,10 @@ public class EntityMonster extends GameEntity {
|
||||
|
||||
@Getter(onMethod_ = @Override)
|
||||
private final Position position;
|
||||
|
||||
@Getter(onMethod_ = @Override)
|
||||
private final Position rotation;
|
||||
|
||||
@Getter private final MonsterData monsterData;
|
||||
@Getter private final ConfigEntityMonster configEntityMonster;
|
||||
@Getter private final Position bornPos;
|
||||
@ -54,21 +55,23 @@ public class EntityMonster extends GameEntity {
|
||||
@Getter private List<Player> playerOnBattle;
|
||||
@Nullable @Getter @Setter private SceneMonster metaMonster;
|
||||
|
||||
public EntityMonster(Scene scene, MonsterData monsterData, Position pos, int level) {
|
||||
public EntityMonster(
|
||||
Scene scene, MonsterData monsterData, Position pos, Position rot, int level) {
|
||||
super(scene);
|
||||
|
||||
this.id = this.getWorld().getNextEntityId(EntityIdType.MONSTER);
|
||||
this.monsterData = monsterData;
|
||||
this.fightProperties = new Int2FloatOpenHashMap();
|
||||
this.position = new Position(pos);
|
||||
this.rotation = new Position();
|
||||
this.rotation = new Position(rot);
|
||||
this.bornPos = this.getPosition().clone();
|
||||
this.level = level;
|
||||
this.playerOnBattle = new ArrayList<>();
|
||||
|
||||
if (GameData.getMonsterMappingMap().containsKey(this.getMonsterId())) {
|
||||
this.configEntityMonster = GameData.getMonsterConfigData().get(
|
||||
GameData.getMonsterMappingMap().get(this.getMonsterId()).getMonsterJson());
|
||||
this.configEntityMonster =
|
||||
GameData.getMonsterConfigData()
|
||||
.get(GameData.getMonsterMappingMap().get(this.getMonsterId()).getMonsterJson());
|
||||
} else {
|
||||
this.configEntityMonster = null;
|
||||
}
|
||||
@ -77,7 +80,7 @@ public class EntityMonster extends GameEntity {
|
||||
if (getMonsterWeaponId() > 0) {
|
||||
this.weaponEntity = new EntityWeapon(scene, getMonsterWeaponId());
|
||||
scene.getWeaponEntities().put(this.weaponEntity.getId(), this.weaponEntity);
|
||||
//this.weaponEntityId = getWorld().getNextEntityId(EntityIdType.WEAPON);
|
||||
// this.weaponEntityId = getWorld().getNextEntityId(EntityIdType.WEAPON);
|
||||
}
|
||||
|
||||
this.recalcStats();
|
||||
@ -87,18 +90,15 @@ public class EntityMonster extends GameEntity {
|
||||
private void addConfigAbility(String name) {
|
||||
var data = GameData.getAbilityData(name);
|
||||
if (data != null) {
|
||||
this.getWorld().getHost()
|
||||
.getAbilityManager()
|
||||
.addAbilityToEntity(this, data);
|
||||
this.getWorld().getHost().getAbilityManager().addAbilityToEntity(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initAbilities() {
|
||||
// Affix abilities
|
||||
var optionalGroup = this.getScene().getLoadedGroups().stream()
|
||||
.filter(g -> g.id == this.getGroupId())
|
||||
.findAny();
|
||||
var optionalGroup =
|
||||
this.getScene().getLoadedGroups().stream().filter(g -> g.id == this.getGroupId()).findAny();
|
||||
List<Integer> affixes = null;
|
||||
if (optionalGroup.isPresent()) {
|
||||
var group = optionalGroup.get();
|
||||
@ -118,7 +118,7 @@ public class EntityMonster extends GameEntity {
|
||||
var affix = GameData.getMonsterAffixDataMap().get(affixId.intValue());
|
||||
if (!affix.isPreAdd()) continue;
|
||||
|
||||
//Add the ability
|
||||
// Add the ability
|
||||
for (var name : affix.getAbilityName()) {
|
||||
this.addConfigAbility(name);
|
||||
}
|
||||
@ -126,14 +126,12 @@ public class EntityMonster extends GameEntity {
|
||||
}
|
||||
|
||||
// TODO: Research if any monster is non humanoid
|
||||
for(var ability : GameData.getConfigGlobalCombat()
|
||||
.getDefaultAbilities()
|
||||
.getNonHumanoidMoveAbilities()) {
|
||||
for (var ability :
|
||||
GameData.getConfigGlobalCombat().getDefaultAbilities().getNonHumanoidMoveAbilities()) {
|
||||
this.addConfigAbility(ability);
|
||||
}
|
||||
|
||||
if (this.configEntityMonster != null &&
|
||||
this.configEntityMonster.getAbilities() != null) {
|
||||
if (this.configEntityMonster != null && this.configEntityMonster.getAbilities() != null) {
|
||||
for (var configAbilityData : this.configEntityMonster.getAbilities()) {
|
||||
this.addConfigAbility(configAbilityData.abilityName);
|
||||
}
|
||||
@ -143,9 +141,8 @@ public class EntityMonster extends GameEntity {
|
||||
var group = optionalGroup.get();
|
||||
var monster = group.monsters.get(getConfigId());
|
||||
if (monster != null && monster.isElite) {
|
||||
this.addConfigAbility(GameData.getConfigGlobalCombat()
|
||||
.getDefaultAbilities()
|
||||
.getMonterEliteAbilityName());
|
||||
this.addConfigAbility(
|
||||
GameData.getConfigGlobalCombat().getDefaultAbilities().getMonterEliteAbilityName());
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,8 +151,8 @@ public class EntityMonster extends GameEntity {
|
||||
var affix = GameData.getMonsterAffixDataMap().get(affixId.intValue());
|
||||
if (affix.isPreAdd()) continue;
|
||||
|
||||
//Add the ability
|
||||
for(var name : affix.getAbilityName()) {
|
||||
// Add the ability
|
||||
for (var name : affix.getAbilityName()) {
|
||||
this.addConfigAbility(name);
|
||||
}
|
||||
}
|
||||
@ -163,7 +160,7 @@ public class EntityMonster extends GameEntity {
|
||||
|
||||
var levelEntityConfig = getScene().getSceneData().getLevelEntityConfig();
|
||||
var config = GameData.getConfigLevelEntityDataMap().get(levelEntityConfig);
|
||||
if (config == null){
|
||||
if (config == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -194,7 +191,8 @@ public class EntityMonster extends GameEntity {
|
||||
|
||||
@Override
|
||||
public void onInteract(Player player, GadgetInteractReq interactReq) {
|
||||
EnvAnimalGatherConfigData gatherData = GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId());
|
||||
EnvAnimalGatherConfigData gatherData =
|
||||
GameData.getEnvAnimalGatherConfigDataMap().get(this.getMonsterData().getId());
|
||||
|
||||
if (gatherData == null) {
|
||||
return;
|
||||
@ -208,7 +206,11 @@ public class EntityMonster extends GameEntity {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Lua event
|
||||
getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId()));
|
||||
getScene()
|
||||
.getScriptManager()
|
||||
.callEvent(
|
||||
new ScriptArgs(
|
||||
this.getGroupId(), EventType.EVENT_ANY_MONSTER_LIVE, this.getConfigId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -231,7 +233,14 @@ public class EntityMonster extends GameEntity {
|
||||
@Override
|
||||
public void runLuaCallbacks(EntityDamageEvent event) {
|
||||
super.runLuaCallbacks(event);
|
||||
getScene().getScriptManager().callEvent(new ScriptArgs(this.getGroupId(), EVENT_SPECIFIC_MONSTER_HP_CHANGE, getConfigId(), monsterData.getId())
|
||||
getScene()
|
||||
.getScriptManager()
|
||||
.callEvent(
|
||||
new ScriptArgs(
|
||||
this.getGroupId(),
|
||||
EVENT_SPECIFIC_MONSTER_HP_CHANGE,
|
||||
getConfigId(),
|
||||
monsterData.getId())
|
||||
.setSourceEntityId(getId())
|
||||
.setParam3((int) this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP))
|
||||
.setEventSource(getConfigId()));
|
||||
@ -250,29 +259,63 @@ public class EntityMonster extends GameEntity {
|
||||
challenge.ifPresent(c -> c.onMonsterDeath(this));
|
||||
|
||||
if (scriptManager.isInit() && this.getGroupId() > 0) {
|
||||
Optional.ofNullable(scriptManager.getScriptMonsterSpawnService()).ifPresent(s -> s.onMonsterDead(this));
|
||||
Optional.ofNullable(scriptManager.getScriptMonsterSpawnService())
|
||||
.ifPresent(s -> s.onMonsterDead(this));
|
||||
|
||||
// prevent spawn monster after success
|
||||
/*if (challenge.map(c -> c.inProgress()).orElse(true)) {
|
||||
scriptManager.callEvent(new ScriptArgs(EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()).setGroupId(this.getGroupId()));
|
||||
} else if (getScene().getChallenge() == null) {
|
||||
}*/
|
||||
scriptManager.callEvent(new ScriptArgs(this.getGroupId(), EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()));
|
||||
// Ensure each EVENT_ANY_MONSTER_DIE runs to completion.
|
||||
// Multiple such events firing at the same time may cause
|
||||
// the same lua trigger to fire multiple times, when it
|
||||
// should happen only once.
|
||||
var future =
|
||||
scriptManager.callEvent(
|
||||
new ScriptArgs(
|
||||
this.getGroupId(), EventType.EVENT_ANY_MONSTER_DIE, this.getConfigId()));
|
||||
try {
|
||||
future.get();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
// Battle Pass trigger
|
||||
scene.getPlayers().forEach(p -> p.getBattlePassManager().triggerMission(WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1));
|
||||
scene
|
||||
.getPlayers()
|
||||
.forEach(
|
||||
p ->
|
||||
p.getBattlePassManager()
|
||||
.triggerMission(
|
||||
WatcherTriggerType.TRIGGER_MONSTER_DIE, this.getMonsterId(), 1));
|
||||
|
||||
scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_MONSTER_DIE, this.getMonsterId()));
|
||||
scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_KILL_MONSTER, this.getMonsterId()));
|
||||
scene.getPlayers().forEach(p -> p.getQuestManager().queueEvent(QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER, this.getGroupId()));
|
||||
scene
|
||||
.getPlayers()
|
||||
.forEach(
|
||||
p ->
|
||||
p.getQuestManager()
|
||||
.queueEvent(QuestContent.QUEST_CONTENT_MONSTER_DIE, this.getMonsterId()));
|
||||
scene
|
||||
.getPlayers()
|
||||
.forEach(
|
||||
p ->
|
||||
p.getQuestManager()
|
||||
.queueEvent(QuestContent.QUEST_CONTENT_KILL_MONSTER, this.getMonsterId()));
|
||||
scene
|
||||
.getPlayers()
|
||||
.forEach(
|
||||
p ->
|
||||
p.getQuestManager()
|
||||
.queueEvent(QuestContent.QUEST_CONTENT_CLEAR_GROUP_MONSTER, this.getGroupId()));
|
||||
|
||||
SceneGroupInstance groupInstance = scene.getScriptManager().getGroupInstanceById(this.getGroupId());
|
||||
if(groupInstance != null && metaMonster != null)
|
||||
SceneGroupInstance groupInstance =
|
||||
scene.getScriptManager().getGroupInstanceById(this.getGroupId());
|
||||
if (groupInstance != null && metaMonster != null)
|
||||
groupInstance.getDeadEntities().add(metaMonster.config_id);
|
||||
|
||||
scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId());
|
||||
scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER, this.getMonsterData().getType().getValue());
|
||||
scene.triggerDungeonEvent(DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
|
||||
scene.triggerDungeonEvent(
|
||||
DungeonPassConditionType.DUNGEON_COND_KILL_GROUP_MONSTER, this.getGroupId());
|
||||
scene.triggerDungeonEvent(
|
||||
DungeonPassConditionType.DUNGEON_COND_KILL_TYPE_MONSTER,
|
||||
this.getMonsterData().getType().getValue());
|
||||
scene.triggerDungeonEvent(
|
||||
DungeonPassConditionType.DUNGEON_COND_KILL_MONSTER, this.getMonsterId());
|
||||
}
|
||||
|
||||
public void recalcStats() {
|
||||
@ -280,45 +323,61 @@ public class EntityMonster extends GameEntity {
|
||||
MonsterData data = this.getMonsterData();
|
||||
|
||||
// Get hp percent, set to 100% if none
|
||||
float hpPercent = this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0 ? 1f : this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) / this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||
float hpPercent =
|
||||
this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) <= 0
|
||||
? 1f
|
||||
: this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP)
|
||||
/ this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP);
|
||||
|
||||
// Clear properties
|
||||
this.getFightProperties().clear();
|
||||
|
||||
// Base stats
|
||||
MonsterData.definedFightProperties.forEach(prop -> this.setFightProperty(prop, data.getFightProperty(prop)));
|
||||
MonsterData.definedFightProperties.forEach(
|
||||
prop -> this.setFightProperty(prop, data.getFightProperty(prop)));
|
||||
|
||||
// Level curve
|
||||
MonsterCurveData curve = GameData.getMonsterCurveDataMap().get(this.getLevel());
|
||||
if (curve != null) {
|
||||
for (PropGrowCurve growCurve : data.getPropGrowCurves()) {
|
||||
FightProperty prop = FightProperty.getPropByName(growCurve.getType());
|
||||
this.setFightProperty(prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve()));
|
||||
this.setFightProperty(
|
||||
prop, this.getFightProperty(prop) * curve.getMultByProp(growCurve.getGrowCurve()));
|
||||
}
|
||||
}
|
||||
|
||||
// Set % stats
|
||||
FightProperty.forEachCompoundProperty(c -> this.setFightProperty(c.getResult(),
|
||||
this.getFightProperty(c.getFlat()) + (this.getFightProperty(c.getBase()) * (1f + this.getFightProperty(c.getPercent())))));
|
||||
FightProperty.forEachCompoundProperty(
|
||||
c ->
|
||||
this.setFightProperty(
|
||||
c.getResult(),
|
||||
this.getFightProperty(c.getFlat())
|
||||
+ (this.getFightProperty(c.getBase())
|
||||
* (1f + this.getFightProperty(c.getPercent())))));
|
||||
|
||||
// Set current hp
|
||||
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent);
|
||||
this.setFightProperty(
|
||||
FightProperty.FIGHT_PROP_CUR_HP,
|
||||
this.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP) * hpPercent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SceneEntityInfo toProto() {
|
||||
var data = this.getMonsterData();
|
||||
|
||||
var authority = EntityAuthorityInfo.newBuilder()
|
||||
var authority =
|
||||
EntityAuthorityInfo.newBuilder()
|
||||
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
||||
.setRendererChangedInfo(EntityRendererChangedInfo.newBuilder())
|
||||
.setAiInfo(SceneEntityAiInfo.newBuilder()
|
||||
.setAiInfo(
|
||||
SceneEntityAiInfo.newBuilder()
|
||||
.setIsAiOpen(true)
|
||||
.setBornPos(this.getBornPos().toProto()))
|
||||
.setBornPos(this.getBornPos().toProto())
|
||||
.build();
|
||||
|
||||
var entityInfo = SceneEntityInfo.newBuilder()
|
||||
var entityInfo =
|
||||
SceneEntityInfo.newBuilder()
|
||||
.setEntityId(this.getId())
|
||||
.setEntityType(ProtEntityType.PROT_ENTITY_TYPE_MONSTER)
|
||||
.setMotionInfo(this.getMotionInfo())
|
||||
@ -329,12 +388,14 @@ public class EntityMonster extends GameEntity {
|
||||
|
||||
this.addAllFightPropsToEntityInfo(entityInfo);
|
||||
|
||||
entityInfo.addPropList(PropPair.newBuilder()
|
||||
entityInfo.addPropList(
|
||||
PropPair.newBuilder()
|
||||
.setType(PlayerProperty.PROP_LEVEL.getId())
|
||||
.setPropValue(ProtoHelper.newPropValue(PlayerProperty.PROP_LEVEL, this.getLevel()))
|
||||
.build());
|
||||
|
||||
var monsterInfo = SceneMonsterInfo.newBuilder()
|
||||
var monsterInfo =
|
||||
SceneMonsterInfo.newBuilder()
|
||||
.setMonsterId(getMonsterId())
|
||||
.setGroupId(this.getGroupId())
|
||||
.setConfigId(this.getConfigId())
|
||||
@ -346,16 +407,19 @@ public class EntityMonster extends GameEntity {
|
||||
|
||||
if (this.metaMonster != null) {
|
||||
if (this.metaMonster.special_name_id != 0) {
|
||||
monsterInfo.setTitleId(this.metaMonster.title_id)
|
||||
monsterInfo
|
||||
.setTitleId(this.metaMonster.title_id)
|
||||
.setSpecialNameId(this.metaMonster.special_name_id);
|
||||
} else if (data.getDescribeData() != null) {
|
||||
monsterInfo.setTitleId(data.getDescribeData().getTitleId())
|
||||
monsterInfo
|
||||
.setTitleId(data.getDescribeData().getTitleId())
|
||||
.setSpecialNameId(data.getSpecialNameId());
|
||||
}
|
||||
}
|
||||
|
||||
if (this.getMonsterWeaponId() > 0) {
|
||||
SceneWeaponInfo weaponInfo = SceneWeaponInfo.newBuilder()
|
||||
SceneWeaponInfo weaponInfo =
|
||||
SceneWeaponInfo.newBuilder()
|
||||
.setEntityId(this.getWeaponEntity() != null ? this.getWeaponEntity().getId() : 0)
|
||||
.setGadgetId(this.getMonsterWeaponId())
|
||||
.setAbilityInfo(AbilitySyncStateInfo.newBuilder())
|
||||
|
@ -15,9 +15,8 @@ import emu.grasscutter.scripts.data.controller.EntityController;
|
||||
import emu.grasscutter.server.event.entity.*;
|
||||
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.util.*;
|
||||
import lombok.*;
|
||||
|
||||
public abstract class GameEntity {
|
||||
@Getter private final Scene scene;
|
||||
@ -35,7 +34,8 @@ public abstract class GameEntity {
|
||||
@Getter @Setter private boolean lockHP;
|
||||
|
||||
@Setter(AccessLevel.PROTECTED)
|
||||
@Getter private boolean isDead = false;
|
||||
@Getter
|
||||
private boolean isDead = false;
|
||||
|
||||
// Lua controller for specific actions
|
||||
@Getter @Setter private EntityController entityController;
|
||||
@ -174,13 +174,7 @@ public abstract class GameEntity {
|
||||
}
|
||||
|
||||
this.lastAttackType = attackType;
|
||||
|
||||
// Check if dead
|
||||
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
|
||||
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
|
||||
this.isDead = true;
|
||||
}
|
||||
|
||||
this.checkIfDead();
|
||||
this.runLuaCallbacks(event);
|
||||
|
||||
// Packets
|
||||
@ -194,6 +188,17 @@ public abstract class GameEntity {
|
||||
}
|
||||
}
|
||||
|
||||
public void checkIfDead() {
|
||||
if (this.getFightProperties() == null || !hasFightProperty(FightProperty.FIGHT_PROP_CUR_HP)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP) <= 0f) {
|
||||
this.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
|
||||
this.isDead = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the Lua callbacks for {@link EntityDamageEvent}.
|
||||
*
|
||||
@ -333,6 +338,8 @@ public abstract class GameEntity {
|
||||
if (entityController != null) {
|
||||
entityController.onDie(this, getLastAttackType());
|
||||
}
|
||||
|
||||
this.isDead = true;
|
||||
}
|
||||
|
||||
/** Invoked when a global ability value is updated. */
|
||||
|
@ -1,6 +1,6 @@
|
||||
package emu.grasscutter.game.entity.gadget;
|
||||
|
||||
import emu.grasscutter.game.dungeons.challenge.DungeonChallenge;
|
||||
import emu.grasscutter.game.dungeons.challenge.WorldChallenge;
|
||||
import emu.grasscutter.game.entity.EntityGadget;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.proto.GadgetInteractReqOuterClass.GadgetInteractReq;
|
||||
@ -18,7 +18,7 @@ public final class GadgetRewardStatue extends GadgetContent {
|
||||
public boolean onInteract(Player player, GadgetInteractReq req) {
|
||||
var dungeonManager = player.getScene().getDungeonManager();
|
||||
|
||||
if (player.getScene().getChallenge() instanceof DungeonChallenge) {
|
||||
if (player.getScene().getChallenge() instanceof WorldChallenge) {
|
||||
var useCondensed =
|
||||
req.getResinCostType() == ResinCostTypeOuterClass.ResinCostType.RESIN_COST_TYPE_CONDENSE;
|
||||
dungeonManager.getStatueDrops(player, useCondensed, getGadget().getGroupId());
|
||||
|
@ -107,7 +107,8 @@ public class HomeSceneItem {
|
||||
return new EntityHomeAnimal(
|
||||
scene,
|
||||
GameData.getHomeWorldAnimalDataMap().get(homeAnimalItem.getFurnitureId()),
|
||||
homeAnimalItem.getSpawnPos());
|
||||
homeAnimalItem.getSpawnPos(),
|
||||
homeAnimalItem.getSpawnRot());
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ public class HomeWorld extends World {
|
||||
|
||||
this.home = owner.isOnline() ? owner.getHome() : GameHome.getByUid(owner.getUid());
|
||||
this.refreshModuleManager();
|
||||
server.registerHomeWorld(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -20,14 +20,13 @@ import emu.grasscutter.net.proto.ReliquaryOuterClass.Reliquary;
|
||||
import emu.grasscutter.net.proto.SceneReliquaryInfoOuterClass.SceneReliquaryInfo;
|
||||
import emu.grasscutter.net.proto.SceneWeaponInfoOuterClass.SceneWeaponInfo;
|
||||
import emu.grasscutter.net.proto.WeaponOuterClass.Weapon;
|
||||
import emu.grasscutter.utils.objects.*;
|
||||
import emu.grasscutter.utils.objects.WeightedList;
|
||||
import java.util.*;
|
||||
import lombok.*;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Entity(value = "items", useDiscriminator = false)
|
||||
public class GameItem implements DatabaseObject<GameItem> {
|
||||
public class GameItem {
|
||||
@Id private ObjectId id;
|
||||
@Indexed private int ownerId;
|
||||
@Getter @Setter private int itemId;
|
||||
@ -262,36 +261,14 @@ public class GameItem implements DatabaseObject<GameItem> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save() {
|
||||
if (this.count > 0 && this.ownerId > 0) {
|
||||
this.deferSave();
|
||||
} else {
|
||||
DatabaseHelper.saveItem(this);
|
||||
} else if (this.getObjectId() != null) {
|
||||
DatabaseHelper.deleteItem(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
*
|
||||
* @param immediate If true, this will be a {@link DatabaseObject#save()} call instead of a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save(boolean immediate) {
|
||||
if (this.count < 0 || this.ownerId <= 0) {
|
||||
DatabaseHelper.deleteItem(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
DatabaseObject.super.save();
|
||||
} else {
|
||||
this.save();
|
||||
}
|
||||
}
|
||||
|
||||
public SceneWeaponInfo createSceneWeaponInfo() {
|
||||
var weaponInfo =
|
||||
SceneWeaponInfo.newBuilder()
|
||||
|
@ -1,5 +1,7 @@
|
||||
package emu.grasscutter.game.inventory;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.common.ItemParamData;
|
||||
@ -16,12 +18,9 @@ import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import it.unimi.dsi.fastutil.longs.*;
|
||||
import lombok.val;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.*;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.INVENTORY_LIMITS;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.val;
|
||||
|
||||
public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
private final Long2ObjectMap<GameItem> store;
|
||||
@ -179,7 +178,7 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
changedItems.add(result);
|
||||
}
|
||||
}
|
||||
if (changedItems.isEmpty()) {
|
||||
if (changedItems.size() == 0) {
|
||||
return;
|
||||
}
|
||||
if (reason != null) {
|
||||
@ -299,7 +298,8 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
|
||||
// Add
|
||||
switch (type) {
|
||||
case ITEM_WEAPON, ITEM_RELIQUARY -> {
|
||||
case ITEM_WEAPON:
|
||||
case ITEM_RELIQUARY:
|
||||
if (tab.getSize() >= tab.getMaxCapacity()) {
|
||||
return null;
|
||||
}
|
||||
@ -310,23 +310,23 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
// Set ownership and save to db
|
||||
item.save();
|
||||
return item;
|
||||
}
|
||||
case ITEM_VIRTUAL -> {
|
||||
case ITEM_VIRTUAL:
|
||||
// Handle
|
||||
this.addVirtualItem(item.getItemId(), item.getCount());
|
||||
return item;
|
||||
}
|
||||
default -> {
|
||||
default:
|
||||
switch (item.getItemData().getMaterialType()) {
|
||||
case MATERIAL_AVATAR, MATERIAL_FLYCLOAK, MATERIAL_COSTUME, MATERIAL_NAMECARD -> {
|
||||
case MATERIAL_AVATAR:
|
||||
case MATERIAL_FLYCLOAK:
|
||||
case MATERIAL_COSTUME:
|
||||
case MATERIAL_NAMECARD:
|
||||
Grasscutter.getLogger()
|
||||
.warn(
|
||||
"Attempted to add a "
|
||||
+ item.getItemData().getMaterialType().name()
|
||||
+ " to inventory, but item definition lacks isUseOnGain. This indicates a Resources error.");
|
||||
return null;
|
||||
}
|
||||
default -> {
|
||||
default:
|
||||
if (tab == null) {
|
||||
return null;
|
||||
}
|
||||
@ -353,8 +353,6 @@ public class Inventory extends BasePlayerManager implements Iterable<GameItem> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void putItem(GameItem item, InventoryTab tab) {
|
||||
this.player.getCodex().checkAddedItem(item);
|
||||
|
@ -104,7 +104,8 @@ public final class BlossomActivity {
|
||||
|
||||
var monsterData = GameData.getMonsterDataMap().get((int) entry);
|
||||
var level = scene.getEntityLevel(1, worldLevelOverride);
|
||||
var entity = new EntityMonster(scene, monsterData, pos.nearby2d(4f), level);
|
||||
var entity =
|
||||
new EntityMonster(scene, monsterData, pos.nearby2d(4f), Position.ZERO, level);
|
||||
scene.addEntity(entity);
|
||||
newMonsters.add(entity);
|
||||
}
|
||||
|
@ -394,10 +394,11 @@ public class EnergyManager extends BasePlayerManager {
|
||||
public void refillTeamEnergy(PropChangeReason changeReason, boolean isFlat) {
|
||||
for (var entityAvatar : this.player.getTeamManager().getActiveTeam()) {
|
||||
// giving the exact amount read off the AvatarSkillData.json
|
||||
var skillDepot = entityAvatar.getAvatar().getSkillDepot();
|
||||
if (skillDepot != null) {
|
||||
entityAvatar.addEnergy(
|
||||
entityAvatar.getAvatar().getSkillDepot().getEnergySkillData().getCostElemVal(),
|
||||
changeReason,
|
||||
isFlat);
|
||||
skillDepot.getEnergySkillData().getCostElemVal(), changeReason, isFlat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import emu.grasscutter.data.excels.world.WeatherData;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.*;
|
||||
import emu.grasscutter.game.ability.AbilityManager;
|
||||
import emu.grasscutter.game.achievement.*;
|
||||
import emu.grasscutter.game.achievement.Achievements;
|
||||
import emu.grasscutter.game.activity.ActivityManager;
|
||||
import emu.grasscutter.game.avatar.*;
|
||||
import emu.grasscutter.game.battlepass.BattlePassManager;
|
||||
@ -55,7 +55,7 @@ import emu.grasscutter.server.game.GameSession.SessionState;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.*;
|
||||
import emu.grasscutter.utils.helpers.DateHelper;
|
||||
import emu.grasscutter.utils.objects.*;
|
||||
import emu.grasscutter.utils.objects.FieldFetch;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import lombok.*;
|
||||
|
||||
@ -66,7 +66,7 @@ import java.util.concurrent.*;
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
|
||||
@Entity(value = "players", useDiscriminator = false)
|
||||
public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
public class Player implements PlayerHook, FieldFetch {
|
||||
@Id private int id;
|
||||
@Indexed(options = @IndexOptions(unique = true))
|
||||
@Getter private String accountId;
|
||||
@ -176,6 +176,7 @@ public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
@Getter @Setter private Set<Date> moonCardGetTimes;
|
||||
|
||||
@Transient @Getter private boolean paused;
|
||||
@Transient @Getter @Setter private Future<?> queuedTeleport;
|
||||
@Transient @Getter @Setter private int enterSceneToken;
|
||||
@Transient @Getter @Setter private SceneLoadState sceneLoadState = SceneLoadState.NONE;
|
||||
@Transient private boolean hasSentLoginPackets;
|
||||
@ -261,7 +262,6 @@ public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
this.clientAbilityInitFinishHandler = new InvokeHandler(PacketClientAbilityInitFinishNotify.class);
|
||||
|
||||
this.birthday = new PlayerBirthday();
|
||||
this.achievements = Achievements.blank();
|
||||
this.rewardedLevels = new HashSet<>();
|
||||
this.homeRewardedLevels = new HashSet<>();
|
||||
this.seenRealmList = new HashSet<>();
|
||||
@ -276,10 +276,8 @@ public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
this.energyManager = new EnergyManager(this);
|
||||
this.resinManager = new ResinManager(this);
|
||||
this.forgingManager = new ForgingManager(this);
|
||||
this.deforestationManager = new DeforestationManager(this);
|
||||
this.progressManager = new PlayerProgressManager(this);
|
||||
this.furnitureManager = new FurnitureManager(this);
|
||||
this.battlePassManager = new BattlePassManager(this);
|
||||
this.cookingManager = new CookingManager(this);
|
||||
this.cookingCompoundManager = new CookingCompoundManager(this);
|
||||
this.satiationManager = new SatiationManager(this);
|
||||
@ -303,6 +301,19 @@ public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
this.applyStartingSceneTags();
|
||||
this.getFlyCloakList().add(140001);
|
||||
this.getNameCardList().add(210001);
|
||||
|
||||
this.mapMarksManager = new MapMarksManager(this);
|
||||
this.staminaManager = new StaminaManager(this);
|
||||
this.sotsManager = new SotSManager(this);
|
||||
this.energyManager = new EnergyManager(this);
|
||||
this.resinManager = new ResinManager(this);
|
||||
this.deforestationManager = new DeforestationManager(this);
|
||||
this.forgingManager = new ForgingManager(this);
|
||||
this.progressManager = new PlayerProgressManager(this);
|
||||
this.furnitureManager = new FurnitureManager(this);
|
||||
this.cookingManager = new CookingManager(this);
|
||||
this.cookingCompoundManager = new CookingCompoundManager(this);
|
||||
this.satiationManager = new SatiationManager(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -1330,25 +1341,8 @@ public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
this.getTeamManager().setPlayer(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves this object to the database.
|
||||
* As of Grasscutter 1.7.1, this is by default a {@link DatabaseObject#deferSave()} call.
|
||||
*/
|
||||
public void save() {
|
||||
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();
|
||||
}
|
||||
DatabaseHelper.savePlayer(this);
|
||||
}
|
||||
|
||||
// Called from tokenrsp
|
||||
@ -1385,14 +1379,6 @@ public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
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() {
|
||||
// Quest - Commented out because a problem is caused if you log out while this quest is active
|
||||
/*
|
||||
@ -1519,19 +1505,20 @@ public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
this.getProfile().syncWithCharacter(this);
|
||||
|
||||
this.getCoopRequests().clear();
|
||||
this.getEnterHomeRequests().values()
|
||||
.forEach(req -> this.expireEnterHomeRequest(req, true));
|
||||
this.getEnterHomeRequests().values().forEach(req -> this.expireEnterHomeRequest(req, true));
|
||||
this.getEnterHomeRequests().clear();
|
||||
|
||||
// Save to db
|
||||
this.save(true);
|
||||
this.save();
|
||||
this.getTeamManager().saveAvatars();
|
||||
this.getFriendsList().save();
|
||||
|
||||
// Call quit event.
|
||||
new PlayerQuitEvent(this).call();
|
||||
PlayerQuitEvent event = new PlayerQuitEvent(this);
|
||||
event.call();
|
||||
} catch (Throwable e) {
|
||||
Grasscutter.getLogger().warn("Player (UID {}) failed to save.", this.getUid(), e);
|
||||
e.printStackTrace();
|
||||
Grasscutter.getLogger().warn("Player (UID {}) save failure", getUid());
|
||||
} finally {
|
||||
removeFromServer();
|
||||
}
|
||||
@ -1539,10 +1526,34 @@ public class Player implements DatabaseObject<Player>, PlayerHook, FieldFetch {
|
||||
|
||||
public void removeFromServer() {
|
||||
// Remove from server.
|
||||
// Note: DON'T DELETE BY UID, BECAUSE THERE ARE MULTIPLE SAME UID PLAYERS WHEN DUPLICATED LOGIN!
|
||||
//s o I decide to delete by object rather than uid
|
||||
this.getServer().getPlayers().values()
|
||||
.removeIf(player1 -> player1 == this);
|
||||
//Note: DON'T DELETE BY UID,BECAUSE THERE ARE MULTIPLE SAME UID PLAYERS WHEN DUPLICATED LOGIN!
|
||||
//so I decide to delete by object rather than uid
|
||||
getServer().getPlayers().values().removeIf(player1 -> player1 == this);
|
||||
}
|
||||
|
||||
public void unfreezeUnlockedScenePoints(int sceneId) {
|
||||
// Unfreeze previously unlocked scene points. For example,
|
||||
// the first weapon mats domain needs some script interaction
|
||||
// to unlock. It needs to be unfrozen when GetScenePointReq
|
||||
// comes in to be interactable again.
|
||||
GameData.getScenePointEntryMap().values().stream()
|
||||
.filter(scenePointEntry ->
|
||||
// Note: Only DungeonEntry scene points need to be unfrozen
|
||||
scenePointEntry.getPointData().getType().equals("DungeonEntry")
|
||||
// groupLimit says this scene point needs to be unfrozen
|
||||
&& scenePointEntry.getPointData().isGroupLimit())
|
||||
.forEach(scenePointEntry -> {
|
||||
// If this is a previously unlocked scene point,
|
||||
// send unfreeze packet.
|
||||
val pointId = scenePointEntry.getPointData().getId();
|
||||
if (unlockedScenePoints.get(sceneId).contains(pointId)) {
|
||||
this.sendPacket(new PacketUnfreezeGroupLimitNotify(pointId, sceneId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void unfreezeUnlockedScenePoints() {
|
||||
unlockedScenePoints.keySet().forEach(sceneId -> unfreezeUnlockedScenePoints(sceneId));
|
||||
}
|
||||
|
||||
public int getLegendaryKey() {
|
||||
|
@ -33,12 +33,9 @@ public final class PlayerProgressManager extends BasePlayerDataManager {
|
||||
|
||||
public static final Set<Integer> IGNORED_OPEN_STATES =
|
||||
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.
|
||||
// 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
|
||||
// `map`.
|
||||
|
@ -1,10 +1,11 @@
|
||||
package emu.grasscutter.game.player;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
|
||||
import dev.morphia.annotations.*;
|
||||
import emu.grasscutter.*;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData;
|
||||
import emu.grasscutter.database.Database;
|
||||
import emu.grasscutter.game.avatar.Avatar;
|
||||
import emu.grasscutter.game.entity.*;
|
||||
import emu.grasscutter.game.props.*;
|
||||
@ -22,12 +23,9 @@ import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import lombok.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
public final class TeamManager extends BasePlayerDataManager {
|
||||
@ -406,7 +404,7 @@ public final class TeamManager extends BasePlayerDataManager {
|
||||
// Unload removed entities
|
||||
for (var entity : existingAvatars.values()) {
|
||||
this.getPlayer().getScene().removeEntity(entity);
|
||||
entity.getAvatar().save(true);
|
||||
entity.getAvatar().save();
|
||||
}
|
||||
|
||||
// Set new selected character index
|
||||
@ -427,6 +425,30 @@ public final class TeamManager extends BasePlayerDataManager {
|
||||
this.getPlayer().sendPacket(responsePacket);
|
||||
}
|
||||
|
||||
// Ensure new selected character index is alive.
|
||||
// If not, change to another alive one or revive.
|
||||
checkCurrentAvatarIsAlive(currentEntity);
|
||||
}
|
||||
|
||||
public void checkCurrentAvatarIsAlive(EntityAvatar currentEntity) {
|
||||
if (currentEntity == null) {
|
||||
currentEntity = this.getCurrentAvatarEntity();
|
||||
}
|
||||
|
||||
// Ensure currently selected character is still alive
|
||||
if (!this.getActiveTeam().get(this.currentCharacterIndex).isAlive()) {
|
||||
// Character died in a dungeon challenge...
|
||||
int replaceIndex = getDeadAvatarReplacement();
|
||||
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
|
||||
this.currentCharacterIndex = replaceIndex;
|
||||
} else {
|
||||
// Team wiped in dungeon...
|
||||
// Revive and change to first avatar.
|
||||
this.currentCharacterIndex = 0;
|
||||
this.reviveAvatar(this.getCurrentAvatarEntity().getAvatar());
|
||||
}
|
||||
}
|
||||
|
||||
// Check if character changed
|
||||
var newAvatarEntity = this.getCurrentAvatarEntity();
|
||||
if (currentEntity != null && newAvatarEntity != null && currentEntity != newAvatarEntity) {
|
||||
@ -702,15 +724,16 @@ public final class TeamManager extends BasePlayerDataManager {
|
||||
this.updateTeamEntities(null);
|
||||
}
|
||||
|
||||
public void cleanTemporaryTeam() {
|
||||
public boolean cleanTemporaryTeam() {
|
||||
// check if using temporary team
|
||||
if (useTemporarilyTeamIndex < 0) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.useTemporarilyTeamIndex = -1;
|
||||
this.temporaryTeam = null;
|
||||
this.updateTeamEntities(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized void setCurrentTeam(int teamId) {
|
||||
@ -812,20 +835,13 @@ public final class TeamManager extends BasePlayerDataManager {
|
||||
// TODO: Perhaps find a way to get vanilla experience?
|
||||
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
|
||||
} else {
|
||||
// Replacement avatar
|
||||
EntityAvatar replacement = null;
|
||||
int replaceIndex = -1;
|
||||
|
||||
for (int i = 0; i < this.getActiveTeam().size(); i++) {
|
||||
EntityAvatar entity = this.getActiveTeam().get(i);
|
||||
if (entity.isAlive()) {
|
||||
replaceIndex = i;
|
||||
replacement = entity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (replacement == null) {
|
||||
// Find replacement avatar
|
||||
int replaceIndex = getDeadAvatarReplacement();
|
||||
if (0 <= replaceIndex && replaceIndex < this.getActiveTeam().size()) {
|
||||
// Set index and spawn replacement member
|
||||
this.setCurrentCharacterIndex(replaceIndex);
|
||||
this.getPlayer().getScene().addEntity(this.getActiveTeam().get(replaceIndex));
|
||||
} else {
|
||||
// No more living team members...
|
||||
this.getPlayer().sendPacket(new PacketWorldPlayerDieNotify(dieType, killedBy));
|
||||
// Invoke player team death event.
|
||||
@ -833,10 +849,6 @@ public final class TeamManager extends BasePlayerDataManager {
|
||||
new PlayerTeamDeathEvent(
|
||||
this.getPlayer(), this.getActiveTeam().get(this.getCurrentCharacterIndex()));
|
||||
event.call();
|
||||
} else {
|
||||
// Set index and spawn replacement member
|
||||
this.setCurrentCharacterIndex(replaceIndex);
|
||||
this.getPlayer().getScene().addEntity(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
@ -844,6 +856,20 @@ public final class TeamManager extends BasePlayerDataManager {
|
||||
this.getPlayer().sendPacket(new PacketAvatarDieAnimationEndRsp(deadAvatar.getId(), 0));
|
||||
}
|
||||
|
||||
public int getDeadAvatarReplacement() {
|
||||
int replaceIndex = -1;
|
||||
|
||||
for (int i = 0; i < this.getActiveTeam().size(); i++) {
|
||||
EntityAvatar entity = this.getActiveTeam().get(i);
|
||||
if (entity.isAlive()) {
|
||||
replaceIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return replaceIndex;
|
||||
}
|
||||
|
||||
public boolean reviveAvatar(Avatar avatar) {
|
||||
for (EntityAvatar entity : this.getActiveTeam()) {
|
||||
if (entity.getAvatar() == avatar) {
|
||||
@ -964,13 +990,11 @@ public final class TeamManager extends BasePlayerDataManager {
|
||||
return respawnPoint.get().getPointData().getTranPos();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a bulk save operation on all avatars.
|
||||
*/
|
||||
public void saveAvatars() {
|
||||
Database.saveAll(this.getActiveTeam().stream()
|
||||
.map(EntityAvatar::getAvatar)
|
||||
.toList());
|
||||
// Save all avatars from active team
|
||||
for (EntityAvatar entity : this.getActiveTeam()) {
|
||||
entity.getAvatar().save();
|
||||
}
|
||||
}
|
||||
|
||||
public void onPlayerLogin() { // Hack for now to fix resonances on login
|
||||
|
@ -1,5 +1,8 @@
|
||||
package emu.grasscutter.game.quest;
|
||||
|
||||
import static emu.grasscutter.GameConstants.DEBUG;
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.binout.*;
|
||||
@ -12,16 +15,12 @@ import emu.grasscutter.net.proto.GivingRecordOuterClass.GivingRecord;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import lombok.*;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static emu.grasscutter.GameConstants.DEBUG;
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
import javax.annotation.Nonnull;
|
||||
import lombok.*;
|
||||
|
||||
public final class QuestManager extends BasePlayerManager {
|
||||
@Getter private final Player player;
|
||||
@ -222,14 +221,11 @@ public final class QuestManager extends BasePlayerManager {
|
||||
this.player.sendPacket(new PacketGivingRecordNotify(this.getGivingRecords()));
|
||||
}
|
||||
|
||||
public void onPlayerBorn() {
|
||||
public void onLogin() {
|
||||
if (this.isQuestingEnabled()) {
|
||||
this.enableQuests();
|
||||
this.sendGivingRecords();
|
||||
}
|
||||
}
|
||||
|
||||
public void onLogin() {
|
||||
|
||||
List<GameMainQuest> activeQuests = getActiveMainQuests();
|
||||
List<GameQuest> activeSubs = new ArrayList<>(activeQuests.size());
|
||||
@ -299,6 +295,17 @@ public final class QuestManager extends BasePlayerManager {
|
||||
}
|
||||
|
||||
public void enableQuests() {
|
||||
GameData.getBeginCondQuestMap()
|
||||
.keySet()
|
||||
.forEach(
|
||||
x -> {
|
||||
if (x.contains("QUEST_COND_STATE_NOT_EQUAL"))
|
||||
this.triggerEvent(
|
||||
QuestCond.QUEST_COND_STATE_NOT_EQUAL, null, Integer.parseInt(x.substring(26)));
|
||||
if (x.contains("QUEST_COND_STATE_EQUAL"))
|
||||
this.triggerEvent(
|
||||
QuestCond.QUEST_COND_STATE_EQUAL, null, Integer.parseInt(x.substring(22)));
|
||||
});
|
||||
this.triggerEvent(QuestCond.QUEST_COND_NONE, null, 0);
|
||||
this.triggerEvent(QuestCond.QUEST_COND_PLAYER_LEVEL_EQUAL_GREATER, null, 1);
|
||||
}
|
||||
@ -570,7 +577,7 @@ public final class QuestManager extends BasePlayerManager {
|
||||
* @param quest The ID of the quest.
|
||||
*/
|
||||
public void checkQuestAlreadyFulfilled(GameQuest quest) {
|
||||
eventExecutor
|
||||
Grasscutter.getThreadPool()
|
||||
.submit(
|
||||
() -> {
|
||||
for (var condition : quest.getQuestData().getFinishCond()) {
|
||||
|
@ -0,0 +1,21 @@
|
||||
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]);
|
||||
}
|
||||
}
|
@ -20,6 +20,9 @@ public class ConditionStateEqual extends BaseCondition {
|
||||
var questStateValue = condition.getParam()[1];
|
||||
var checkQuest = owner.getQuestManager().getQuestById(questId);
|
||||
|
||||
return checkQuest != null && checkQuest.getState().getValue() == questStateValue;
|
||||
if (checkQuest == null) {
|
||||
return questStateValue == 0;
|
||||
}
|
||||
return checkQuest.getState().getValue() == questStateValue;
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ public class ConditionStateNotEqual extends BaseCondition {
|
||||
var questStateValue = condition.getParam()[1];
|
||||
var checkQuest = owner.getQuestManager().getQuestById(questId);
|
||||
|
||||
return checkQuest != null && checkQuest.getState().getValue() != questStateValue;
|
||||
if (checkQuest == null) {
|
||||
return questStateValue != 0;
|
||||
}
|
||||
return checkQuest.getState().getValue() != questStateValue;
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ public enum QuestCond implements QuestTrigger {
|
||||
QUEST_COND_QUEST_GLOBAL_VAR_LESS(46),
|
||||
QUEST_COND_PERSONAL_LINE_UNLOCK(47),
|
||||
QUEST_COND_CITY_REPUTATION_REQUEST(48), // missing
|
||||
QUEST_COND_MAIN_COOP_START(49), // missing
|
||||
QUEST_COND_MAIN_COOP_START(49),
|
||||
QUEST_COND_MAIN_COOP_ENTER_SAVE_POINT(50), // missing
|
||||
QUEST_COND_CITY_REPUTATION_LEVEL(51), // missing, only NPC groups
|
||||
QUEST_COND_CITY_REPUTATION_UNLOCK(52), // missing, currently unused
|
||||
|
@ -25,6 +25,10 @@ public class TowerLevelRecord {
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getLevelStars(int levelId) {
|
||||
return passedLevelMap.get(levelId);
|
||||
}
|
||||
|
||||
public int getStarCount() {
|
||||
return passedLevelMap.values().stream().mapToInt(Integer::intValue).sum();
|
||||
}
|
||||
|
@ -1,16 +1,22 @@
|
||||
package emu.grasscutter.game.tower;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.tower.TowerLevelData;
|
||||
import emu.grasscutter.game.dungeons.*;
|
||||
import emu.grasscutter.game.player.*;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import java.util.*;
|
||||
import lombok.*;
|
||||
|
||||
public class TowerManager extends BasePlayerManager {
|
||||
private static final List<DungeonSettleListener> towerDungeonSettleListener =
|
||||
List.of(new TowerDungeonSettleListener());
|
||||
|
||||
private int currentPossibleStars = 0;
|
||||
@Getter private boolean inProgress;
|
||||
@Getter private int currentTimeLimit;
|
||||
|
||||
public TowerManager(Player player) {
|
||||
super(player);
|
||||
}
|
||||
@ -32,6 +38,32 @@ public class TowerManager extends BasePlayerManager {
|
||||
return this.getTowerData().currentLevel + 1;
|
||||
}
|
||||
|
||||
public void onTick() {
|
||||
var challenge = player.getScene().getChallenge();
|
||||
if (challenge == null || !challenge.inProgress()) return;
|
||||
|
||||
// Check star conditions and notify client if any failed.
|
||||
int stars = getCurLevelStars();
|
||||
while (stars < currentPossibleStars) {
|
||||
player
|
||||
.getSession()
|
||||
.send(
|
||||
new PacketTowerLevelStarCondNotify(
|
||||
getTowerData().currentFloorId, getCurrentLevel(), currentPossibleStars));
|
||||
currentPossibleStars--;
|
||||
}
|
||||
}
|
||||
|
||||
public void onBegin() {
|
||||
var challenge = player.getScene().getChallenge();
|
||||
inProgress = true;
|
||||
currentTimeLimit = challenge.getTimeLimit();
|
||||
}
|
||||
|
||||
public void onEnd() {
|
||||
inProgress = false;
|
||||
}
|
||||
|
||||
public Map<Integer, TowerLevelRecord> getRecordMap() {
|
||||
Map<Integer, TowerLevelRecord> recordMap = getTowerData().recordMap;
|
||||
if (recordMap == null || recordMap.size() == 0) {
|
||||
@ -84,9 +116,12 @@ public class TowerManager extends BasePlayerManager {
|
||||
// stop using skill
|
||||
player.getSession().send(new PacketCanUseSkillNotify(false));
|
||||
// notify the cond of stars
|
||||
currentPossibleStars = 3;
|
||||
player
|
||||
.getSession()
|
||||
.send(new PacketTowerLevelStarCondNotify(getTowerData().currentFloorId, getCurrentLevel()));
|
||||
.send(
|
||||
new PacketTowerLevelStarCondNotify(
|
||||
getTowerData().currentFloorId, getCurrentLevel(), currentPossibleStars + 1));
|
||||
}
|
||||
|
||||
public void notifyCurLevelRecordChange() {
|
||||
@ -97,6 +132,41 @@ public class TowerManager extends BasePlayerManager {
|
||||
getTowerData().currentFloorId, getCurrentLevel()));
|
||||
}
|
||||
|
||||
public int getCurLevelStars() {
|
||||
var scene = player.getScene();
|
||||
var challenge = scene.getChallenge();
|
||||
if (challenge == null) {
|
||||
Grasscutter.getLogger().error("getCurLevelStars: no challenge registered!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var levelData = GameData.getTowerLevelDataMap().get(getCurrentLevelId());
|
||||
// 0-based indexing. "star" = 0 means checking for 1-star conditions.
|
||||
int star;
|
||||
for (star = 2; star >= 0; star--) {
|
||||
var cond = levelData.getCondType(star);
|
||||
if (cond == TowerLevelData.TowerCondType.TOWER_COND_CHALLENGE_LEFT_TIME_MORE_THAN) {
|
||||
var params = levelData.getTimeCond(star);
|
||||
var timeRemaining =
|
||||
challenge.getTimeLimit() - (scene.getSceneTimeSeconds() - challenge.getStartedAt());
|
||||
if (timeRemaining >= params.getMinimumTimeInSeconds()) {
|
||||
break;
|
||||
}
|
||||
} else if (cond == TowerLevelData.TowerCondType.TOWER_COND_LEFT_HP_GREATER_THAN) {
|
||||
// TODO: Check monolith health
|
||||
break;
|
||||
} else {
|
||||
Grasscutter.getLogger()
|
||||
.error(
|
||||
"getCurLevelStars: Tower level {} has no or unknown condition defined for {} stars",
|
||||
getCurrentLevelId(),
|
||||
star + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return star + 1;
|
||||
}
|
||||
|
||||
public void notifyCurLevelRecordChangeWhenDone(int stars) {
|
||||
Map<Integer, TowerLevelRecord> recordMap = this.getRecordMap();
|
||||
int currentFloorId = getTowerData().currentFloorId;
|
||||
@ -105,8 +175,16 @@ public class TowerManager extends BasePlayerManager {
|
||||
currentFloorId,
|
||||
new TowerLevelRecord(currentFloorId).setLevelStars(getCurrentLevelId(), stars));
|
||||
} else {
|
||||
recordMap.put(
|
||||
currentFloorId, recordMap.get(currentFloorId).setLevelStars(getCurrentLevelId(), stars));
|
||||
// Only update record if better than previous
|
||||
var prevRecord = recordMap.get(currentFloorId);
|
||||
var passedLevelMap = prevRecord.getPassedLevelMap();
|
||||
int prevStars = 0;
|
||||
if (passedLevelMap.containsKey(getCurrentLevelId())) {
|
||||
prevStars = prevRecord.getLevelStars(getCurrentLevelId());
|
||||
}
|
||||
if (stars > prevStars) {
|
||||
recordMap.put(currentFloorId, prevRecord.setLevelStars(getCurrentLevelId(), stars));
|
||||
}
|
||||
}
|
||||
|
||||
this.getTowerData().currentLevel++;
|
||||
|
@ -5,6 +5,13 @@ import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GroupReplacementData {
|
||||
int id;
|
||||
List<Integer> replace_groups;
|
||||
public int id;
|
||||
public List<Integer> replace_groups;
|
||||
|
||||
public GroupReplacementData() {}
|
||||
|
||||
public GroupReplacementData(int id, List<Integer> replace_groups) {
|
||||
this.id = id;
|
||||
this.replace_groups = replace_groups;
|
||||
}
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ public class Scene {
|
||||
return entity;
|
||||
}
|
||||
|
||||
public GameEntity getEntityByConfigId(int configId) {
|
||||
public GameEntity getFirstEntityByConfigId(int configId) {
|
||||
return this.entities.values().stream()
|
||||
.filter(x -> x.getConfigId() == configId)
|
||||
.findFirst()
|
||||
@ -597,6 +597,13 @@ public class Scene {
|
||||
|
||||
blossomManager.onTick();
|
||||
|
||||
// Should be OK to check only player 0,
|
||||
// as no other players could enter Tower
|
||||
var towerManager = getPlayers().get(0).getTowerManager();
|
||||
if (towerManager != null) {
|
||||
towerManager.onTick();
|
||||
}
|
||||
|
||||
this.checkNpcGroup();
|
||||
|
||||
this.finishLoading();
|
||||
@ -816,8 +823,8 @@ public class Scene {
|
||||
|
||||
int level = this.getEntityLevel(entry.getLevel(), worldLevelOverride);
|
||||
|
||||
EntityMonster monster = new EntityMonster(this, data, entry.getPos(), level);
|
||||
monster.getRotation().set(entry.getRot());
|
||||
EntityMonster monster =
|
||||
new EntityMonster(this, data, entry.getPos(), entry.getRot(), level);
|
||||
monster.setGroupId(entry.getGroup().getGroupId());
|
||||
monster.setPoseId(entry.getPoseId());
|
||||
monster.setConfigId(entry.getConfigId());
|
||||
@ -1103,6 +1110,9 @@ public class Scene {
|
||||
if (group.regions != null) {
|
||||
group.regions.values().forEach(getScriptManager()::deregisterRegion);
|
||||
}
|
||||
if (challenge != null && group.id == challenge.getGroup().id) {
|
||||
challenge.fail();
|
||||
}
|
||||
|
||||
scriptManager.getLoadedGroupSetPerBlock().get(block.id).remove(group);
|
||||
this.loadedGroups.remove(group);
|
||||
|
@ -1,5 +1,8 @@
|
||||
package emu.grasscutter.game.world;
|
||||
|
||||
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.data.excels.dungeon.DungeonData;
|
||||
import emu.grasscutter.game.entity.*;
|
||||
@ -17,14 +20,13 @@ import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
|
||||
import emu.grasscutter.server.game.GameServer;
|
||||
import emu.grasscutter.server.packet.send.*;
|
||||
import emu.grasscutter.utils.ConversionUtils;
|
||||
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType.SCRIPT;
|
||||
|
||||
public class World implements Iterable<Player> {
|
||||
@Getter private final GameServer server;
|
||||
@Getter private Player host;
|
||||
@ -44,6 +46,16 @@ public class World implements Iterable<Player> {
|
||||
@Getter private boolean isPaused = false;
|
||||
@Getter private long currentWorldTime;
|
||||
|
||||
private static final ExecutorService eventExecutor =
|
||||
new ThreadPoolExecutor(
|
||||
4,
|
||||
4,
|
||||
60,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingDeque<>(1000),
|
||||
FastThreadLocalThread::new,
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
|
||||
public World(Player player) {
|
||||
this(player, false);
|
||||
}
|
||||
@ -73,6 +85,8 @@ public class World implements Iterable<Player> {
|
||||
this.scenes = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>());
|
||||
this.entity = new EntityWorld(this);
|
||||
this.lastUpdateTime = System.currentTimeMillis();
|
||||
|
||||
server.registerWorld(this);
|
||||
}
|
||||
|
||||
public int getLevelEntityId() {
|
||||
@ -267,7 +281,7 @@ public class World implements Iterable<Player> {
|
||||
scene.removePlayer(player);
|
||||
|
||||
// Info packet for other players
|
||||
if (!this.getPlayers().isEmpty()) {
|
||||
if (this.getPlayers().size() > 0) {
|
||||
this.updatePlayerInfos(player);
|
||||
}
|
||||
|
||||
@ -310,6 +324,21 @@ public class World implements Iterable<Player> {
|
||||
this.getScenes().values().forEach(Scene::saveGroups);
|
||||
}
|
||||
|
||||
public void queueTransferPlayerToScene(Player player, int sceneId, Position pos, int delayMs) {
|
||||
player.setQueuedTeleport(
|
||||
eventExecutor.submit(
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(delayMs);
|
||||
transferPlayerToScene(player, sceneId, pos);
|
||||
} catch (InterruptedException e) {
|
||||
Grasscutter.getLogger()
|
||||
.trace(
|
||||
"queueTransferPlayerToScene: teleport to scene {} is interrupted", sceneId);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public boolean transferPlayerToScene(Player player, int sceneId, Position pos) {
|
||||
return this.transferPlayerToScene(player, sceneId, TeleportType.INTERNAL, null, pos);
|
||||
}
|
||||
@ -380,6 +409,16 @@ public class World implements Iterable<Player> {
|
||||
}
|
||||
|
||||
public boolean transferPlayerToScene(Player player, TeleportProperties teleportProperties) {
|
||||
// If a queued teleport already exists, cancel it. This prevents the player from
|
||||
// becoming stranded in a dungeon due to quitting it by teleporting to a map waypoint.
|
||||
synchronized (player) {
|
||||
var queuedTeleport = player.getQueuedTeleport();
|
||||
if (queuedTeleport != null) {
|
||||
player.setQueuedTeleport(null);
|
||||
queuedTeleport.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the teleport properties are valid.
|
||||
if (teleportProperties.getTeleportTo() == null)
|
||||
teleportProperties.setTeleportTo(player.getPosition());
|
||||
@ -397,19 +436,31 @@ public class World implements Iterable<Player> {
|
||||
return false;
|
||||
}
|
||||
|
||||
Scene oldScene = null;
|
||||
if (player.getScene() != null) {
|
||||
oldScene = player.getScene();
|
||||
Scene oldScene = player.getScene();
|
||||
var newScene = this.getSceneById(teleportProperties.getSceneId());
|
||||
|
||||
// Don't deregister scenes if the player is going to tp back into them
|
||||
if (oldScene.getId() == teleportProperties.getSceneId()) {
|
||||
oldScene.setDontDestroyWhenEmpty(true);
|
||||
// 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
|
||||
if (oldScene == newScene) {
|
||||
oldScene.setDontDestroyWhenEmpty(true);
|
||||
}
|
||||
oldScene.removePlayer(player);
|
||||
}
|
||||
|
||||
var newScene = this.getSceneById(teleportProperties.getSceneId());
|
||||
if (newScene != null) {
|
||||
newScene.addPlayer(player);
|
||||
|
||||
player.getTeamManager().applyAbilities(newScene);
|
||||
@ -430,6 +481,7 @@ public class World implements Iterable<Player> {
|
||||
teleportProperties.setTeleportRot(config.born_rot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set player position and rotation
|
||||
if (teleportProperties.getTeleportTo() != null) {
|
||||
@ -439,7 +491,7 @@ public class World implements Iterable<Player> {
|
||||
player.getRotation().set(teleportProperties.getTeleportRot());
|
||||
}
|
||||
|
||||
if (oldScene != null && newScene != oldScene) {
|
||||
if (oldScene != null && newScene != null && newScene != oldScene) {
|
||||
newScene.setPrevScenePoint(oldScene.getPrevScenePoint());
|
||||
oldScene.setDontDestroyWhenEmpty(false);
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
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);
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
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();
|
||||
}
|
@ -17,6 +17,7 @@ import emu.grasscutter.net.proto.VisionTypeOuterClass;
|
||||
import emu.grasscutter.scripts.constants.EventType;
|
||||
import emu.grasscutter.scripts.data.*;
|
||||
import emu.grasscutter.scripts.service.*;
|
||||
import emu.grasscutter.server.event.game.SceneMetaLoadEvent;
|
||||
import emu.grasscutter.server.packet.send.PacketGroupSuiteNotify;
|
||||
import emu.grasscutter.utils.*;
|
||||
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||
@ -38,6 +39,7 @@ public class SceneScriptManager {
|
||||
private final Map<String, Integer> variables;
|
||||
private SceneMeta meta;
|
||||
private boolean isInit;
|
||||
private boolean noCacheGroupGridsToDisk;
|
||||
|
||||
private final Map<String, SceneTimeAxis> timeAxis = new ConcurrentHashMap<>();
|
||||
|
||||
@ -134,7 +136,9 @@ public class SceneScriptManager {
|
||||
public void registerTrigger(SceneTrigger trigger) {
|
||||
this.triggerInvocations.put(trigger.getName(), new AtomicInteger(0));
|
||||
this.getTriggersByEvent(trigger.getEvent()).add(trigger);
|
||||
Grasscutter.getLogger().trace("Registered trigger {}", trigger.getName());
|
||||
Grasscutter.getLogger()
|
||||
.trace(
|
||||
"Registered trigger {} from group {}", trigger.getName(), trigger.getCurrentGroup().id);
|
||||
}
|
||||
|
||||
public void deregisterTrigger(List<SceneTrigger> triggers) {
|
||||
@ -143,7 +147,11 @@ public class SceneScriptManager {
|
||||
|
||||
public void deregisterTrigger(SceneTrigger trigger) {
|
||||
this.getTriggersByEvent(trigger.getEvent()).remove(trigger);
|
||||
Grasscutter.getLogger().trace("deregistered trigger {}", trigger.getName());
|
||||
Grasscutter.getLogger()
|
||||
.trace(
|
||||
"deregistered trigger {} from group {}",
|
||||
trigger.getName(),
|
||||
trigger.getCurrentGroup().id);
|
||||
}
|
||||
|
||||
public void resetTriggers(int eventId) {
|
||||
@ -265,6 +273,10 @@ public class SceneScriptManager {
|
||||
|
||||
groupInstance.setActiveSuiteId(suiteIndex);
|
||||
groupInstance.setLastTimeRefreshed(getScene().getWorld().getGameTime());
|
||||
|
||||
// Call EVENT_GROUP_REFRESH for any action trigger waiting for it
|
||||
callEvent(new ScriptArgs(groupInstance.getGroupId(), EventType.EVENT_GROUP_REFRESH));
|
||||
|
||||
return suiteIndex;
|
||||
}
|
||||
|
||||
@ -323,7 +335,7 @@ public class SceneScriptManager {
|
||||
group.monsters.values().stream()
|
||||
.filter(
|
||||
m -> {
|
||||
var entity = scene.getEntityByConfigId(m.config_id);
|
||||
var entity = scene.getEntityByConfigId(m.config_id, groupId);
|
||||
return (entity == null
|
||||
|| entity.getGroupId()
|
||||
!= group
|
||||
@ -434,6 +446,17 @@ public class SceneScriptManager {
|
||||
}
|
||||
|
||||
private void init() {
|
||||
var event = new SceneMetaLoadEvent(getScene());
|
||||
event.call();
|
||||
|
||||
if (event.isOverride()) {
|
||||
// Group grids should not be cached to disk when a scene
|
||||
// group override is in effect. Otherwise, when the server
|
||||
// next runs without that override, the cached content
|
||||
// will not make sense.
|
||||
noCacheGroupGridsToDisk = true;
|
||||
}
|
||||
|
||||
var meta = ScriptLoader.getSceneMeta(getScene().getId());
|
||||
if (meta == null) {
|
||||
return;
|
||||
@ -451,7 +474,9 @@ public class SceneScriptManager {
|
||||
return groupGridsCache.get(sceneId);
|
||||
} else {
|
||||
var path = FileUtils.getCachePath("scene" + sceneId + "_grid.json");
|
||||
if (path.toFile().isFile() && !Grasscutter.config.server.game.cacheSceneEntitiesEveryRun) {
|
||||
if (path.toFile().isFile()
|
||||
&& !Grasscutter.config.server.game.cacheSceneEntitiesEveryRun
|
||||
&& !noCacheGroupGridsToDisk) {
|
||||
try {
|
||||
var groupGrids = JsonUtils.loadToList(path, Grid.class);
|
||||
groupGridsCache.put(sceneId, groupGrids);
|
||||
@ -581,6 +606,7 @@ public class SceneScriptManager {
|
||||
}
|
||||
groupGridsCache.put(scene.getId(), groupGrids);
|
||||
|
||||
if (!noCacheGroupGridsToDisk) {
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
} catch (IOException ignored) {
|
||||
@ -589,7 +615,9 @@ public class SceneScriptManager {
|
||||
file.write(JsonUtils.encode(groupGrids));
|
||||
Grasscutter.getLogger().info("Scene {} saved grid file.", getScene().getId());
|
||||
} catch (Exception e) {
|
||||
Grasscutter.getLogger().error("Scene {} unable to save grid file.", getScene().getId(), e);
|
||||
Grasscutter.getLogger()
|
||||
.error("Scene {} unable to save grid file.", getScene().getId(), e);
|
||||
}
|
||||
}
|
||||
return groupGrids;
|
||||
}
|
||||
@ -694,7 +722,7 @@ public class SceneScriptManager {
|
||||
return suite.sceneGadgets.stream()
|
||||
.filter(
|
||||
m -> {
|
||||
var entity = scene.getEntityByConfigId(m.config_id);
|
||||
var entity = scene.getEntityByConfigId(m.config_id, group.id);
|
||||
return (entity == null || entity.getGroupId() != group.id)
|
||||
&& (!m.isOneoff
|
||||
|| !m.persistent
|
||||
@ -712,7 +740,7 @@ public class SceneScriptManager {
|
||||
return suite.sceneMonsters.stream()
|
||||
.filter(
|
||||
m -> {
|
||||
var entity = scene.getEntityByConfigId(m.config_id);
|
||||
var entity = scene.getEntityByConfigId(m.config_id, group.id);
|
||||
return (entity == null
|
||||
|| entity.getGroupId()
|
||||
!= group
|
||||
@ -774,9 +802,9 @@ public class SceneScriptManager {
|
||||
}
|
||||
|
||||
public void startMonsterTideInGroup(
|
||||
SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
|
||||
String source, SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
|
||||
this.scriptMonsterTideService =
|
||||
new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId);
|
||||
new ScriptMonsterTideService(this, source, group, tideCount, sceneLimit, ordersConfigId);
|
||||
}
|
||||
|
||||
public void unloadCurrentMonsterTide() {
|
||||
@ -788,7 +816,7 @@ public class SceneScriptManager {
|
||||
|
||||
public void spawnMonstersByConfigId(SceneGroup group, int configId, int delayTime) {
|
||||
// TODO delay
|
||||
var entity = scene.getEntityByConfigId(configId);
|
||||
var entity = scene.getEntityByConfigId(configId, group.id);
|
||||
if (entity != null && entity.getGroupId() == group.id) {
|
||||
Grasscutter.getLogger()
|
||||
.debug("entity already exists failed in group {} with config {}", group.id, configId);
|
||||
@ -803,11 +831,11 @@ public class SceneScriptManager {
|
||||
}
|
||||
}
|
||||
// Events
|
||||
public void callEvent(int groupId, int eventType) {
|
||||
callEvent(new ScriptArgs(groupId, eventType));
|
||||
public Future<?> callEvent(int groupId, int eventType) {
|
||||
return callEvent(new ScriptArgs(groupId, eventType));
|
||||
}
|
||||
|
||||
public void callEvent(@Nonnull ScriptArgs params) {
|
||||
public Future<?> callEvent(@Nonnull ScriptArgs params) {
|
||||
/**
|
||||
* We use ThreadLocal to trans SceneScriptManager context to ScriptLib, to avoid eval script for
|
||||
* every groups' trigger in every scene instances. But when callEvent is called in a ScriptLib
|
||||
@ -815,7 +843,7 @@ public class SceneScriptManager {
|
||||
* not get it. e.g. CallEvent -> set -> ScriptLib.xxx -> CallEvent -> set -> remove -> NPE ->
|
||||
* (remove) So we use thread pool to clean the stack to avoid this new issue.
|
||||
*/
|
||||
eventExecutor.submit(() -> this.realCallEvent(params));
|
||||
return eventExecutor.submit(() -> this.realCallEvent(params));
|
||||
}
|
||||
|
||||
private void realCallEvent(@Nonnull ScriptArgs params) {
|
||||
@ -884,9 +912,11 @@ public class SceneScriptManager {
|
||||
private boolean evaluateTriggerCondition(SceneTrigger trigger, ScriptArgs params) {
|
||||
Grasscutter.getLogger()
|
||||
.trace(
|
||||
"Call Condition Trigger {}, [{},{},{}]",
|
||||
"Call Condition Trigger {}, [{},{},{}], source_eid {}, target_eid {}",
|
||||
trigger.getCondition(),
|
||||
params.param1,
|
||||
params.param2,
|
||||
params.param3,
|
||||
params.source_eid,
|
||||
params.target_eid);
|
||||
LuaValue ret = this.callScriptFunc(trigger.getCondition(), trigger.currentGroup, params);
|
||||
@ -1037,8 +1067,7 @@ public class SceneScriptManager {
|
||||
}
|
||||
|
||||
// Spawn mob
|
||||
EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, level);
|
||||
entity.getRotation().set(monster.rot);
|
||||
EntityMonster entity = new EntityMonster(getScene(), data, monster.pos, monster.rot, level);
|
||||
entity.setGroupId(groupId);
|
||||
entity.setBlockId(blockId);
|
||||
entity.setConfigId(monster.config_id);
|
||||
@ -1195,7 +1224,7 @@ public class SceneScriptManager {
|
||||
return monsters.values().stream()
|
||||
.noneMatch(
|
||||
m -> {
|
||||
val entity = scene.getEntityByConfigId(m.config_id);
|
||||
val entity = scene.getEntityByConfigId(m.config_id, groupId);
|
||||
return entity != null && entity.getGroupId() == groupId;
|
||||
});
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,7 @@ import emu.grasscutter.scripts.serializer.*;
|
||||
import emu.grasscutter.utils.FileUtils;
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@ -172,6 +172,17 @@ public class ScriptLoader {
|
||||
* @return The sources of the script.
|
||||
*/
|
||||
public static String readScript(String path) {
|
||||
return readScript(path, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the sources of a script.
|
||||
*
|
||||
* @param path The path of the script.
|
||||
* @param useAbsPath Use path as-is; don't look under Scripts resources.
|
||||
* @return The sources of the script.
|
||||
*/
|
||||
public static String readScript(String path, boolean useAbsPath) {
|
||||
// Check if the path is cached.
|
||||
var cached = ScriptLoader.tryGet(ScriptLoader.scriptSources.get(path));
|
||||
if (cached.isPresent()) {
|
||||
@ -179,8 +190,11 @@ public class ScriptLoader {
|
||||
}
|
||||
|
||||
// Attempt to load the script.
|
||||
var scriptPath = FileUtils.getScriptPath(path);
|
||||
if (!Files.exists(scriptPath)) return null;
|
||||
var scriptPath = useAbsPath ? Paths.get(path) : FileUtils.getScriptPath(path);
|
||||
if (!Files.exists(scriptPath)) {
|
||||
Grasscutter.getLogger().error("Could not find script at path {}", path);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
var source = Files.readString(scriptPath);
|
||||
@ -201,6 +215,17 @@ public class ScriptLoader {
|
||||
* @return The compiled script.
|
||||
*/
|
||||
public static CompiledScript getScript(String path) {
|
||||
return getScript(path, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a script and compiles it, or uses the cached varient.
|
||||
*
|
||||
* @param path The path of the script.
|
||||
* @param useAbsPath Use path as-is; don't look under Scripts resources.
|
||||
* @return The compiled script.
|
||||
*/
|
||||
public static CompiledScript getScript(String path, boolean useAbsPath) {
|
||||
// Check if the script is cached.
|
||||
var sc = ScriptLoader.tryGet(ScriptLoader.scriptsCache.get(path));
|
||||
if (sc.isPresent()) {
|
||||
@ -211,15 +236,18 @@ public class ScriptLoader {
|
||||
CompiledScript script;
|
||||
if (Configuration.FAST_REQUIRE) {
|
||||
// Attempt to load the script.
|
||||
var scriptPath = FileUtils.getScriptPath(path);
|
||||
if (!Files.exists(scriptPath)) return null;
|
||||
var scriptPath = useAbsPath ? Paths.get(path) : FileUtils.getScriptPath(path);
|
||||
if (!Files.exists(scriptPath)) {
|
||||
Grasscutter.getLogger().error("Could not find script at path {}", path);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Compile the script from the file.
|
||||
var source = Files.newBufferedReader(scriptPath);
|
||||
script = ScriptLoader.getEngine().compile(source);
|
||||
} else {
|
||||
// Load the script sources.
|
||||
var sources = ScriptLoader.readScript(path);
|
||||
var sources = ScriptLoader.readScript(path, useAbsPath);
|
||||
if (sources == null) return null;
|
||||
|
||||
// Check to see if the script references other scripts.
|
||||
@ -237,7 +265,7 @@ public class ScriptLoader {
|
||||
var scriptName = line.substring(9, line.length() - 1);
|
||||
// Resolve the script path.
|
||||
var scriptPath = "Common/" + scriptName + ".lua";
|
||||
var scriptSource = ScriptLoader.readScript(scriptPath);
|
||||
var scriptSource = ScriptLoader.readScript(scriptPath, useAbsPath);
|
||||
if (scriptSource == null) continue;
|
||||
|
||||
// Append the script source.
|
||||
|
@ -5,6 +5,7 @@ import com.github.davidmoten.rtreemulti.geometry.*;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.world.Position;
|
||||
import emu.grasscutter.scripts.*;
|
||||
import emu.grasscutter.server.event.game.SceneBlockLoadedEvent;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.script.*;
|
||||
@ -64,6 +65,10 @@ public class SceneBlock {
|
||||
.collect(Collectors.toMap(x -> x.id, y -> y, (a, b) -> a));
|
||||
|
||||
this.groups.values().forEach(g -> g.block_id = this.id);
|
||||
|
||||
var event = new SceneBlockLoadedEvent(this);
|
||||
event.call();
|
||||
|
||||
this.sceneGroupIndex =
|
||||
SceneIndexManager.buildIndex(3, this.groups.values(), g -> g.pos.toPoint());
|
||||
} catch (ScriptException exception) {
|
||||
|
@ -3,6 +3,7 @@ package emu.grasscutter.scripts.data;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.game.world.Position;
|
||||
import emu.grasscutter.scripts.ScriptLoader;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.script.*;
|
||||
@ -39,6 +40,7 @@ public final class SceneGroup {
|
||||
private transient boolean loaded;
|
||||
private transient CompiledScript script;
|
||||
private transient Bindings bindings;
|
||||
public String overrideScriptPath;
|
||||
|
||||
public static SceneGroup of(int groupId) {
|
||||
var group = new SceneGroup();
|
||||
@ -86,8 +88,14 @@ public final class SceneGroup {
|
||||
// Create the bindings.
|
||||
this.bindings = ScriptLoader.getEngine().createBindings();
|
||||
|
||||
var cs =
|
||||
ScriptLoader.getScript("Scene/%s/scene%s_group%s.lua".formatted(sceneId, sceneId, this.id));
|
||||
CompiledScript cs;
|
||||
if (overrideScriptPath != null && !overrideScriptPath.equals("")) {
|
||||
cs = ScriptLoader.getScript(overrideScriptPath, true);
|
||||
} else {
|
||||
cs =
|
||||
ScriptLoader.getScript(
|
||||
"Scene/%s/scene%s_group%s.lua".formatted(sceneId, sceneId, this.id));
|
||||
}
|
||||
|
||||
if (cs == null) {
|
||||
return this;
|
||||
|
@ -20,9 +20,11 @@ public final class ScriptMonsterTideService {
|
||||
private final List<Integer> monsterConfigIds;
|
||||
private final OnMonsterCreated onMonsterCreated = new OnMonsterCreated();
|
||||
private final OnMonsterDead onMonsterDead = new OnMonsterDead();
|
||||
private final String source;
|
||||
|
||||
public ScriptMonsterTideService(
|
||||
SceneScriptManager sceneScriptManager,
|
||||
String source,
|
||||
SceneGroup group,
|
||||
int tideCount,
|
||||
int monsterSceneLimit,
|
||||
@ -35,6 +37,7 @@ public final class ScriptMonsterTideService {
|
||||
this.monsterAlive = new AtomicInteger(0);
|
||||
this.monsterConfigOrders = new ConcurrentLinkedQueue<>(List.of(ordersConfigId));
|
||||
this.monsterConfigIds = List.of(ordersConfigId);
|
||||
this.source = source;
|
||||
|
||||
this.sceneScriptManager
|
||||
.getScriptMonsterSpawnService()
|
||||
@ -83,11 +86,11 @@ public final class ScriptMonsterTideService {
|
||||
sceneScriptManager.createMonster(
|
||||
currentGroup.id, currentGroup.block_id, getNextMonster()));
|
||||
}
|
||||
// spawn the last turn of monsters
|
||||
// fix the 5-2
|
||||
sceneScriptManager.callEvent(
|
||||
new ScriptArgs(
|
||||
currentGroup.id, EventType.EVENT_MONSTER_TIDE_DIE, monsterKillCount.get()));
|
||||
// call registered events that may spawn in more monsters
|
||||
var scriptArgs =
|
||||
new ScriptArgs(currentGroup.id, EventType.EVENT_MONSTER_TIDE_DIE, monsterKillCount.get());
|
||||
scriptArgs.setEventSource(source);
|
||||
sceneScriptManager.callEvent(scriptArgs);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,16 @@
|
||||
package emu.grasscutter.server.event.game;
|
||||
|
||||
import emu.grasscutter.scripts.data.SceneBlock;
|
||||
import emu.grasscutter.server.event.types.ServerEvent;
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
public final class SceneBlockLoadedEvent extends ServerEvent {
|
||||
private SceneBlock block;
|
||||
|
||||
public SceneBlockLoadedEvent(SceneBlock block) {
|
||||
super(Type.GAME);
|
||||
|
||||
this.block = block;
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package emu.grasscutter.server.event.game;
|
||||
|
||||
import emu.grasscutter.game.world.Scene;
|
||||
import emu.grasscutter.server.event.types.ServerEvent;
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
public final class SceneMetaLoadEvent extends ServerEvent {
|
||||
private Scene scene;
|
||||
@Setter private boolean override;
|
||||
|
||||
public SceneMetaLoadEvent(Scene scene) {
|
||||
super(Type.GAME);
|
||||
|
||||
this.scene = scene;
|
||||
}
|
||||
}
|
@ -1,49 +1,55 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
import emu.grasscutter.*;
|
||||
import emu.grasscutter.Grasscutter.ServerRunMode;
|
||||
import emu.grasscutter.database.DatabaseHelper;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.game.battlepass.BattlePassSystem;
|
||||
import emu.grasscutter.game.chat.*;
|
||||
import emu.grasscutter.game.chat.ChatSystem;
|
||||
import emu.grasscutter.game.chat.ChatSystemHandler;
|
||||
import emu.grasscutter.game.combine.CombineManger;
|
||||
import emu.grasscutter.game.drop.*;
|
||||
import emu.grasscutter.game.drop.DropSystem;
|
||||
import emu.grasscutter.game.drop.DropSystemLegacy;
|
||||
import emu.grasscutter.game.dungeons.DungeonSystem;
|
||||
import emu.grasscutter.game.expedition.ExpeditionSystem;
|
||||
import emu.grasscutter.game.gacha.GachaSystem;
|
||||
import emu.grasscutter.game.home.*;
|
||||
import emu.grasscutter.game.managers.cooking.*;
|
||||
import emu.grasscutter.game.home.HomeWorld;
|
||||
import emu.grasscutter.game.home.HomeWorldMPSystem;
|
||||
import emu.grasscutter.game.managers.cooking.CookingCompoundManager;
|
||||
import emu.grasscutter.game.managers.cooking.CookingManager;
|
||||
import emu.grasscutter.game.managers.energy.EnergyManager;
|
||||
import emu.grasscutter.game.managers.stamina.StaminaManager;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.game.quest.QuestSystem;
|
||||
import emu.grasscutter.game.shop.ShopSystem;
|
||||
import emu.grasscutter.game.systems.*;
|
||||
import emu.grasscutter.game.systems.AnnouncementSystem;
|
||||
import emu.grasscutter.game.systems.InventorySystem;
|
||||
import emu.grasscutter.game.systems.MultiplayerSystem;
|
||||
import emu.grasscutter.game.talk.TalkSystem;
|
||||
import emu.grasscutter.game.tower.TowerSystem;
|
||||
import emu.grasscutter.game.world.*;
|
||||
import emu.grasscutter.game.world.World;
|
||||
import emu.grasscutter.game.world.WorldDataSystem;
|
||||
import emu.grasscutter.net.packet.PacketHandler;
|
||||
import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail;
|
||||
import emu.grasscutter.server.dispatch.DispatchClient;
|
||||
import emu.grasscutter.server.event.game.ServerTickEvent;
|
||||
import emu.grasscutter.server.event.internal.*;
|
||||
import emu.grasscutter.server.event.internal.ServerStartEvent;
|
||||
import emu.grasscutter.server.event.internal.ServerStopEvent;
|
||||
import emu.grasscutter.server.event.types.ServerEvent;
|
||||
import emu.grasscutter.server.scheduler.ServerTaskScheduler;
|
||||
import emu.grasscutter.task.TaskMap;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
import kcp.highway.*;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
import emu.grasscutter.server.game.session.GameSessionManager;
|
||||
|
||||
import java.net.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
import kcp.highway.*;
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
@Getter
|
||||
public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
@ -54,7 +60,6 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
private final Set<World> worlds;
|
||||
private final Int2ObjectMap<HomeWorld> homeWorlds;
|
||||
|
||||
@Getter private boolean started = false;
|
||||
@Setter private DispatchClient dispatchClient;
|
||||
|
||||
// Server systems
|
||||
@ -135,7 +140,7 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
channelConfig.setUseConvChannel(true);
|
||||
channelConfig.setAckNoDelay(false);
|
||||
|
||||
this.init(GameSessionManager.getInstance(), channelConfig, address);
|
||||
this.init(GameSessionManager.getListener(), channelConfig, address);
|
||||
|
||||
EnergyManager.initialize();
|
||||
StaminaManager.initialize();
|
||||
@ -306,11 +311,6 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
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) {
|
||||
return this.getHomeWorlds()
|
||||
.computeIfAbsent(owner.getUid(), (uid) -> new HomeWorld(this, owner));
|
||||
@ -342,8 +342,6 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
.info(translate("messages.game.address_bind", GAME_INFO.accessAddress, address.getPort()));
|
||||
ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now());
|
||||
event.call();
|
||||
|
||||
this.started = true;
|
||||
}
|
||||
|
||||
public void onServerShutdown() {
|
||||
@ -358,10 +356,10 @@ public final class GameServer extends KcpServer implements Iterable<Player> {
|
||||
this.stop(); // Stop the server.
|
||||
|
||||
try {
|
||||
var threadPool = GameSessionManager.getExecutor();
|
||||
var threadPool = GameSessionManager.getLogicThread();
|
||||
|
||||
// Shutdown network thread.
|
||||
threadPool.shutdown();
|
||||
threadPool.shutdownGracefully();
|
||||
// Wait for the network thread to finish.
|
||||
if (!threadPool.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
Grasscutter.getLogger().error("Logic thread did not terminate!");
|
||||
|
@ -1,5 +1,7 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_INFO;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.Grasscutter.ServerDebugMode;
|
||||
import emu.grasscutter.net.packet.*;
|
||||
@ -7,8 +9,6 @@ import emu.grasscutter.server.event.game.ReceivePacketEvent;
|
||||
import emu.grasscutter.server.game.GameSession.SessionState;
|
||||
import it.unimi.dsi.fastutil.ints.*;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.GAME_INFO;
|
||||
|
||||
public final class GameServerPacketHandler {
|
||||
private final Int2ObjectMap<PacketHandler> handlers;
|
||||
|
||||
@ -76,11 +76,13 @@ public final class GameServerPacketHandler {
|
||||
}
|
||||
|
||||
// Invoke event.
|
||||
var event = new ReceivePacketEvent(session, opcode, payload);
|
||||
if (event.call()) // If event is not canceled, continue.
|
||||
ReceivePacketEvent event = new ReceivePacketEvent(session, opcode, payload);
|
||||
event.call();
|
||||
if (!event.isCanceled()) // If event is not canceled, continue.
|
||||
handler.handle(session, header, event.getPacketData());
|
||||
} catch (Exception ex) {
|
||||
Grasscutter.getLogger().warn("Unable to handle packet.", ex);
|
||||
// TODO Remove this when no more needed
|
||||
ex.printStackTrace();
|
||||
}
|
||||
return; // Packet successfully handled
|
||||
}
|
||||
|
@ -1,26 +1,24 @@
|
||||
package emu.grasscutter.server.game;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.Grasscutter.ServerDebugMode;
|
||||
import emu.grasscutter.game.Account;
|
||||
import emu.grasscutter.game.player.Player;
|
||||
import emu.grasscutter.net.*;
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.server.event.game.SendPacketEvent;
|
||||
import emu.grasscutter.utils.*;
|
||||
import io.netty.buffer.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.file.Path;
|
||||
import lombok.*;
|
||||
|
||||
import static emu.grasscutter.config.Configuration.*;
|
||||
import static emu.grasscutter.utils.lang.Language.translate;
|
||||
|
||||
public class GameSession implements KcpChannel {
|
||||
public class GameSession implements GameSessionManager.KcpChannel {
|
||||
private final GameServer server;
|
||||
private KcpTunnel tunnel;
|
||||
private GameSessionManager.KcpTunnel tunnel;
|
||||
|
||||
@Getter @Setter private Account account;
|
||||
@Getter private Player player;
|
||||
@ -148,7 +146,7 @@ public class GameSession implements KcpChannel {
|
||||
if (packet.shouldEncrypt) {
|
||||
Crypto.xor(bytes, packet.useDispatchKey() ? Crypto.DISPATCH_KEY : this.encryptKey);
|
||||
}
|
||||
this.tunnel.writeData(bytes);
|
||||
tunnel.writeData(bytes);
|
||||
} catch (Exception ignored) {
|
||||
Grasscutter.getLogger().debug("Unable to send packet to client.");
|
||||
}
|
||||
@ -156,13 +154,13 @@ public class GameSession implements KcpChannel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected(KcpTunnel tunnel) {
|
||||
public void onConnected(GameSessionManager.KcpTunnel tunnel) {
|
||||
this.tunnel = tunnel;
|
||||
Grasscutter.getLogger().info(translate("messages.game.connect", this.getAddress().toString()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(byte[] bytes) {
|
||||
public void handleReceive(byte[] bytes) {
|
||||
// Decrypt and turn back into a packet
|
||||
Crypto.xor(bytes, useSecretKey() ? this.encryptKey : Crypto.DISPATCH_KEY);
|
||||
ByteBuf packet = Unpooled.wrappedBuffer(bytes);
|
||||
@ -228,8 +226,8 @@ public class GameSession implements KcpChannel {
|
||||
// Handle
|
||||
getServer().getPacketHandler().handle(this, opcode, header, payload);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Grasscutter.getLogger().warn("Unable to handle packet.", exception);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
// byteBuf.release(); //Needn't
|
||||
packet.release();
|
||||
@ -237,9 +235,8 @@ public class GameSession implements KcpChannel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnected() {
|
||||
public void handleClose() {
|
||||
setState(SessionState.INACTIVE);
|
||||
|
||||
// send disconnection pack in case of reconnection
|
||||
Grasscutter.getLogger()
|
||||
.info(translate("messages.game.disconnect", this.getAddress().toString()));
|
||||
|
@ -0,0 +1,114 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package emu.grasscutter.server.game.session;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.server.game.*;
|
||||
import emu.grasscutter.utils.Utils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kcp.highway.*;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public final class GameSessionManager implements KcpListener {
|
||||
@Getter private static final GameSessionManager instance
|
||||
= new GameSessionManager();
|
||||
@Getter private static final ExecutorService executor
|
||||
= Executors.newWorkStealingPool();
|
||||
@Getter private static final Map<Ukcp, GameSession> sessions
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Waits for the game server to be ready.
|
||||
*
|
||||
* @return The game server.
|
||||
*/
|
||||
private GameServer waitForServer() {
|
||||
var server = Grasscutter.getGameServer();
|
||||
var times = 0; while (server == null || !server.isStarted()) {
|
||||
Utils.sleep(1000); // Wait 1s for the server to start.
|
||||
if (times++ > 5) {
|
||||
Grasscutter.getLogger().error("Game server has not started in a reasonable time.");
|
||||
return null;
|
||||
}
|
||||
|
||||
server = Grasscutter.getGameServer();
|
||||
}
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnected(Ukcp ukcp) {
|
||||
// Fetch the game server.
|
||||
var server = this.waitForServer();
|
||||
if (server == null) {
|
||||
ukcp.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new session.
|
||||
var session = sessions.compute(ukcp, (k, existing) -> {
|
||||
// Close an existing session.
|
||||
if (existing != null) {
|
||||
existing.close();
|
||||
}
|
||||
|
||||
return new GameSession(server);
|
||||
});
|
||||
|
||||
// Connect the session.
|
||||
session.onConnected(new GameSessionHandler(ukcp));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReceive(ByteBuf byteBuf, Ukcp ukcp) {
|
||||
// Get the session.
|
||||
var session = sessions.get(ukcp);
|
||||
if (session == null) {
|
||||
ukcp.close(); return;
|
||||
}
|
||||
|
||||
// Handle the message in a separate thread.
|
||||
var bytes = Utils.byteBufToArray(byteBuf);
|
||||
executor.submit(() -> session.onMessage(bytes));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleException(Throwable throwable, Ukcp ukcp) {
|
||||
Grasscutter.getLogger().error("Exception in game session.", throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleClose(Ukcp ukcp) {
|
||||
var session = sessions.remove(ukcp);
|
||||
if (session != null) {
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
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));
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.net.proto.DungeonDieOptionReqOuterClass.DungeonDieOptionReq;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
|
||||
@Opcodes(PacketOpcodes.DungeonDieOptionReq)
|
||||
public class HandlerDungeonDieOptionReq extends PacketHandler {
|
||||
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
DungeonDieOptionReq req = DungeonDieOptionReq.parseFrom(payload);
|
||||
var dieOption = req.getDieOption();
|
||||
// TODO Handle other die options
|
||||
if (req.getIsQuitImmediately()) {
|
||||
session.getPlayer().getServer().getDungeonSystem().exitDungeon(session.getPlayer());
|
||||
}
|
||||
session.getPlayer().sendPacket(new BasePacket(PacketOpcodes.DungeonDieOptionRsp));
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
|
||||
@Opcodes(PacketOpcodes.DungeonRestartReq)
|
||||
public class HandlerDungeonRestartReq extends PacketHandler {
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
session.getPlayer().getServer().getDungeonSystem().restartDungeon(session.getPlayer());
|
||||
session.getPlayer().sendPacket(new BasePacket(PacketOpcodes.DungeonRestartRsp));
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.net.proto.GetDungeonEntryExploreConditionReqOuterClass.GetDungeonEntryExploreConditionReq;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.PacketDungeonEntryToBeExploreNotify;
|
||||
|
||||
@Opcodes(PacketOpcodes.GetDungeonEntryExploreConditionReq)
|
||||
public class HandlerGetDungeonEntryExploreConditionReq extends PacketHandler {
|
||||
@Override
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
var req = GetDungeonEntryExploreConditionReq.parseFrom(payload);
|
||||
|
||||
// TODO Send GetDungeonEntryExploreConditionRsp if condition
|
||||
// (adventurer rank or quest completion) is not met. Parse
|
||||
// dungeon entry conditions from DungeonEntryExcelConfigData.json.
|
||||
// session.send(new PacketGetDungeonEntryExploreConditionRsp(req.getDungeonEntryConfigId()));
|
||||
|
||||
// For now, just unlock any domain the player touches.
|
||||
session.send(
|
||||
new PacketDungeonEntryToBeExploreNotify(
|
||||
req.getDungeonEntryScenePointId(), req.getSceneId(), req.getDungeonEntryConfigId()));
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package emu.grasscutter.server.packet.recv;
|
||||
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.server.game.GameSession;
|
||||
import emu.grasscutter.server.packet.send.PacketCoopDataNotify;
|
||||
import emu.grasscutter.server.packet.send.PacketPersonalLineAllDataRsp;
|
||||
|
||||
@Opcodes(PacketOpcodes.PersonalLineAllDataReq)
|
||||
@ -12,5 +13,7 @@ public class HandlerPersonalLineAllDataReq extends PacketHandler {
|
||||
session.send(
|
||||
new PacketPersonalLineAllDataRsp(
|
||||
session.getPlayer().getQuestManager().getMainQuests().values()));
|
||||
// TODO: this should maybe be at player login?
|
||||
session.send(new PacketCoopDataNotify());
|
||||
}
|
||||
}
|
||||
|
@ -15,10 +15,11 @@ public class HandlerPersonalSceneJumpReq extends PacketHandler {
|
||||
public void handle(GameSession session, byte[] header, byte[] payload) throws Exception {
|
||||
PersonalSceneJumpReq req = PersonalSceneJumpReq.parseFrom(payload);
|
||||
var player = session.getPlayer();
|
||||
var prevSceneId = player.getSceneId();
|
||||
|
||||
// get the scene point
|
||||
ScenePointEntry scenePointEntry =
|
||||
GameData.getScenePointEntryById(player.getSceneId(), req.getPointId());
|
||||
GameData.getScenePointEntryById(prevSceneId, req.getPointId());
|
||||
|
||||
if (scenePointEntry != null) {
|
||||
Position pos =
|
||||
@ -26,6 +27,7 @@ public class HandlerPersonalSceneJumpReq extends PacketHandler {
|
||||
int sceneId = scenePointEntry.getPointData().getTranSceneId();
|
||||
|
||||
player.getWorld().transferPlayerToScene(player, sceneId, pos);
|
||||
player.getScene().setPrevScene(prevSceneId);
|
||||
session.send(new PacketPersonalSceneJumpRsp(sceneId, pos));
|
||||
}
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ public class HandlerQuestCreateEntityReq extends PacketHandler {
|
||||
val monsterId = entity.getMonsterId();
|
||||
val level = entity.getLevel();
|
||||
MonsterData monsterData = GameData.getMonsterDataMap().get(monsterId);
|
||||
gameEntity = new EntityMonster(scene, monsterData, pos, level);
|
||||
gameEntity = new EntityMonster(scene, monsterData, pos, rot, level);
|
||||
}
|
||||
case NPC_ID -> {}
|
||||
}
|
||||
|
@ -69,7 +69,6 @@ public class HandlerSetPlayerBornDataReq extends PacketHandler {
|
||||
|
||||
// Login done
|
||||
session.getPlayer().onLogin();
|
||||
session.getPlayer().onPlayerBorn();
|
||||
|
||||
// Born resp packet
|
||||
session.send(new BasePacket(PacketOpcodes.SetPlayerBornDataRsp));
|
||||
|
@ -0,0 +1,31 @@
|
||||
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));
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.net.proto.DungeonEntryToBeExploreNotifyOuterClass.DungeonEntryToBeExploreNotify;
|
||||
|
||||
public class PacketDungeonEntryToBeExploreNotify extends BasePacket {
|
||||
|
||||
/**
|
||||
* Marks the dungeon as pending exploration. This creates the "Unknown" text bubble above the
|
||||
* dungeon entry in the world map.
|
||||
*/
|
||||
public PacketDungeonEntryToBeExploreNotify(
|
||||
int dungeonEntryScenePointId, int sceneId, int dungeonEntryConfigId) {
|
||||
super(PacketOpcodes.DungeonEntryToBeExploreNotify);
|
||||
this.setData(
|
||||
DungeonEntryToBeExploreNotify.newBuilder()
|
||||
.setDungeonEntryScenePointId(dungeonEntryScenePointId)
|
||||
.setSceneId(sceneId)
|
||||
.setDungeonEntryConfigId(dungeonEntryConfigId));
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.data.GameData;
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.net.proto.DungeonEntryBlockReasonOuterClass.DungeonEntryBlockReason;
|
||||
import emu.grasscutter.net.proto.DungeonEntryCondOuterClass.DungeonEntryCond;
|
||||
import emu.grasscutter.net.proto.GetDungeonEntryExploreConditionRspOuterClass.GetDungeonEntryExploreConditionRsp;
|
||||
|
||||
public class PacketGetDungeonEntryExploreConditionRsp extends BasePacket {
|
||||
public PacketGetDungeonEntryExploreConditionRsp(int dungeonId) {
|
||||
super(PacketOpcodes.GetDungeonEntryExploreConditionRsp);
|
||||
|
||||
var data =
|
||||
GameData.getDungeonEntryDataMap().values().stream()
|
||||
.filter(d -> d.getId() == dungeonId)
|
||||
.toList()
|
||||
.get(0);
|
||||
|
||||
var level = data.getLevelCondition();
|
||||
var quest = data.getQuestCondition();
|
||||
var proto =
|
||||
GetDungeonEntryExploreConditionRsp.newBuilder()
|
||||
.setRetcode(0)
|
||||
.setDungeonEntryCond(
|
||||
DungeonEntryCond.newBuilder()
|
||||
// There is also a DUNGEON_ENTRY_REASON_MULIPLE but only one param1
|
||||
// field to put values in. Only report the required level for now, then.
|
||||
.setCondReason(DungeonEntryBlockReason.DUNGEON_ENTRY_REASON_LEVEL)
|
||||
.setParam1(level))
|
||||
.build();
|
||||
|
||||
this.setData(proto);
|
||||
}
|
||||
}
|
@ -12,6 +12,11 @@ public class PacketPostEnterSceneRsp extends BasePacket {
|
||||
PostEnterSceneRsp p =
|
||||
PostEnterSceneRsp.newBuilder().setEnterSceneToken(player.getEnterSceneToken()).build();
|
||||
|
||||
//
|
||||
// On moving to new scene:
|
||||
// Unfreeze dungeon entry points that have already been unlocked in this scene.
|
||||
player.unfreezeUnlockedScenePoints();
|
||||
|
||||
this.setData(p);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
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);
|
||||
}
|
||||
}
|
@ -6,27 +6,29 @@ import emu.grasscutter.net.proto.TowerLevelStarCondNotifyOuterClass.TowerLevelSt
|
||||
|
||||
public class PacketTowerLevelStarCondNotify extends BasePacket {
|
||||
|
||||
public PacketTowerLevelStarCondNotify(int floorId, int levelIndex) {
|
||||
public PacketTowerLevelStarCondNotify(int floorId, int levelIndex, int lostStar) {
|
||||
super(PacketOpcodes.TowerLevelStarCondNotify);
|
||||
|
||||
TowerLevelStarCondNotify proto =
|
||||
TowerLevelStarCondNotify.newBuilder()
|
||||
.setFloorId(floorId)
|
||||
.setLevelIndex(levelIndex)
|
||||
.addCondDataList(
|
||||
TowerLevelStarCondData.newBuilder()
|
||||
// .setCondValue(1)
|
||||
.build())
|
||||
.addCondDataList(
|
||||
TowerLevelStarCondData.newBuilder()
|
||||
// .setCondValue(2)
|
||||
.build())
|
||||
.addCondDataList(
|
||||
TowerLevelStarCondData.newBuilder()
|
||||
// .setCondValue(3)
|
||||
.build())
|
||||
.build();
|
||||
var proto = TowerLevelStarCondNotify.newBuilder().setFloorId(floorId).setLevelIndex(levelIndex);
|
||||
|
||||
this.setData(proto);
|
||||
if (1 <= lostStar && lostStar <= 3) {
|
||||
proto.addCondDataList(
|
||||
TowerLevelStarCondData.newBuilder()
|
||||
// If these are still obfuscated in the next client version,
|
||||
// just set all int fields to the star (1 <= star <= 3)
|
||||
// that failed and set all boolean fields to true.
|
||||
.setNGHNFHCLFBH(lostStar)
|
||||
.setIBGHBFANCBK(true)
|
||||
.setOILLLBMMABH(true)
|
||||
.setOMOECEGOALC(lostStar)
|
||||
.build());
|
||||
} else {
|
||||
proto
|
||||
.addCondDataList(TowerLevelStarCondData.newBuilder().build())
|
||||
.addCondDataList(TowerLevelStarCondData.newBuilder().build())
|
||||
.addCondDataList(TowerLevelStarCondData.newBuilder().build());
|
||||
}
|
||||
|
||||
this.setData(proto.build());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
package emu.grasscutter.server.packet.send;
|
||||
|
||||
import emu.grasscutter.net.packet.*;
|
||||
import emu.grasscutter.net.proto.UnfreezeGroupLimitNotifyOuterClass.UnfreezeGroupLimitNotify;
|
||||
|
||||
public class PacketUnfreezeGroupLimitNotify extends BasePacket {
|
||||
public PacketUnfreezeGroupLimitNotify(int pointId, int sceneId) {
|
||||
super(PacketOpcodes.UnfreezeGroupLimitNotify);
|
||||
this.setData(UnfreezeGroupLimitNotify.newBuilder().setPointId(pointId).setSceneId(sceneId));
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import emu.grasscutter.data.common.ItemUseData;
|
||||
import emu.grasscutter.data.excels.*;
|
||||
import emu.grasscutter.data.excels.achievement.AchievementData;
|
||||
import emu.grasscutter.data.excels.avatar.AvatarData;
|
||||
import emu.grasscutter.server.http.handlers.GachaHandler;
|
||||
import emu.grasscutter.utils.*;
|
||||
import emu.grasscutter.utils.lang.Language;
|
||||
import emu.grasscutter.utils.lang.Language.TextStrings;
|
||||
@ -311,8 +312,19 @@ public final class Tools {
|
||||
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 {
|
||||
ResourceLoader.loadResources();
|
||||
List<String> jsons = createGachaMappingJsons();
|
||||
var usedLocales = new HashSet<String>();
|
||||
StringBuilder sb = new StringBuilder("mappings = {\n");
|
||||
|
@ -1,42 +0,0 @@
|
||||
package emu.grasscutter.utils.objects;
|
||||
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.Grasscutter.ServerRunMode;
|
||||
import emu.grasscutter.database.*;
|
||||
|
||||
public interface DatabaseObject<T> {
|
||||
/**
|
||||
* @return Does this object belong in the game database?
|
||||
*/
|
||||
default boolean isGameObject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Should this object be saved immediately?
|
||||
*/
|
||||
default boolean saveImmediately() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a deferred save.
|
||||
* This object will save as a group with other objects.
|
||||
*/
|
||||
default void deferSave() {
|
||||
Database.save(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to save this object to the database.
|
||||
*/
|
||||
default void save() {
|
||||
if (this.isGameObject()) {
|
||||
DatabaseManager.getGameDatastore().save(this);
|
||||
} else if (Grasscutter.getRunMode() != ServerRunMode.GAME_ONLY) {
|
||||
DatabaseManager.getAccountDatastore().save(this);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Unable to store an account object while in game-only mode.");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,60 @@
|
||||
package io.grasscutter;
|
||||
|
||||
import io.grasscutter.virtual.*;
|
||||
import lombok.*;
|
||||
import com.mchange.util.AssertException;
|
||||
import emu.grasscutter.Grasscutter;
|
||||
import emu.grasscutter.config.Configuration;
|
||||
import java.io.IOException;
|
||||
import lombok.Getter;
|
||||
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;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/** Testing entrypoint for {@link Grasscutter}. */
|
||||
public final class GrasscutterTest {
|
||||
@Getter
|
||||
public static final OkHttpClient httpClient = new OkHttpClient();
|
||||
@Getter public static final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
@Getter private static final OkHttpClient httpClient = new OkHttpClient();
|
||||
|
||||
@Getter public static VirtualAccount account;
|
||||
@Setter
|
||||
@Getter public static VirtualPlayer player;
|
||||
@Getter public static VirtualGameSession gameSession;
|
||||
@Getter private static int httpPort = -1;
|
||||
@Getter private static int gamePort = -1;
|
||||
|
||||
/**
|
||||
* Creates an HTTP URL.
|
||||
*
|
||||
* @param route The route to use.
|
||||
* @return The URL.
|
||||
*/
|
||||
public static String http(String route) {
|
||||
return "http://127.0.0.1:" + GrasscutterTest.httpPort + "/" + route;
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void entry() {
|
||||
try {
|
||||
// Start Grasscutter.
|
||||
Grasscutter.main(new String[] {"-test"});
|
||||
} catch (Exception ignored) {
|
||||
throw new AssertException("Grasscutter failed to start.");
|
||||
}
|
||||
|
||||
// Set the ports.
|
||||
GrasscutterTest.httpPort = Configuration.SERVER.http.bindPort;
|
||||
GrasscutterTest.gamePort = Configuration.SERVER.game.bindPort;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("HTTP server check")
|
||||
public void checkHttpServer() {
|
||||
// Create a request.
|
||||
var request = new Request.Builder().url(GrasscutterTest.http("")).build();
|
||||
|
||||
// Perform the request.
|
||||
try (var response = GrasscutterTest.httpClient.newCall(request).execute()) {
|
||||
// Check the response.
|
||||
Assertions.assertTrue(response.isSuccessful());
|
||||
} catch (IOException exception) {
|
||||
throw new AssertionError(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user