Commit f04035da authored by Luke H-W's avatar Luke H-W Committed by GitHub
Browse files

Merge branch 'development' into Weather

parents b2e62055 1c9a9599
...@@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17 ...@@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17
group = 'xyz.grasscutters' group = 'xyz.grasscutters'
version = '1.2.1-dev' version = '1.2.2-dev'
sourceCompatibility = 17 sourceCompatibility = 17
...@@ -116,7 +116,7 @@ jar { ...@@ -116,7 +116,7 @@ jar {
from { from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
} }
duplicatesStrategy = DuplicatesStrategy.INCLUDE duplicatesStrategy = DuplicatesStrategy.INCLUDE
from('src/main/java') { from('src/main/java') {
...@@ -171,13 +171,23 @@ publishing { ...@@ -171,13 +171,23 @@ publishing {
} }
repositories { repositories {
maven { maven {
// change URLs to point to your repos, e.g. http://my.org/repo if(version.endsWith('-dev')) {
def releasesRepoUrl = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/' println ("Publishing to 4benj-maven")
def snapshotsRepoUrl = 'https://s01.oss.sonatype.org/content/repositories/snapshots/' url 'https://repo.4benj.com/releases'
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl name '4benj-maven'
credentials {
name = 'sonatype' username System.getenv('benj_maven_username')
credentials(PasswordCredentials) password System.getenv('benj_maven_token')
}
} else {
println ("Publishing to sonatype")
def releasesRepoUrl = 'https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/'
def snapshotsRepoUrl = 'https://s01.oss.sonatype.org/content/repositories/snapshots/'
url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
name = 'sonatype'
credentials(PasswordCredentials)
}
} }
} }
} }
...@@ -225,7 +235,9 @@ eclipse { ...@@ -225,7 +235,9 @@ eclipse {
} }
signing { signing {
sign publishing.publications.mavenJava if(!version.endsWith('-dev')) {
sign publishing.publications.mavenJava
}
} }
javadoc { javadoc {
......
{ {
"$schema": "http://json-schema.org/draft-07/schema#", "$schema": "http://json-schema.org/draft-07/schema#",
"title": "JSON schema for a Grasscutter Plugin", "title": "JSON schema for a Grasscutter Plugin",
"type": "object", "type": "object",
"additionalProperties": true, "additionalProperties": true,
"definitions": { "definitions": {
"plugin-name": { "plugin-name": {
"type": "string", "type": "string",
"pattern": "^[A-Za-z\\d_.-]+$" "pattern": "^[A-Za-z\\d_.-]+$"
} }
},
"required": [ "name", "description", "mainClass" ],
"properties": {
"name": {
"description": "The unique name of plugin.",
"$ref": "#/definitions/plugin-name"
},
"mainClass": {
"description": "The plugin's initial class file.",
"type": "string",
"pattern": "^(?!org\\.bukkit\\.)([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*$"
},
"version": {
"description": "A plugin revision identifier.",
"type": [ "string", "number" ]
},
"description": {
"description": "Human readable plugin summary.",
"type": "string"
},
"author": {
"description": "The plugin author.",
"type": "string"
},
"authors": {
"description": "The plugin contributors.",
"type": "array",
"items": {
"type": "string"
}
}, },
"website": { "required": [
"title": "Website", "name",
"description": "The URL to the plugin's site", "description",
"type": "string", "mainClass"
"format": "uri" ],
"properties": {
"name": {
"description": "The unique name of plugin.",
"$ref": "#/definitions/plugin-name"
},
"mainClass": {
"description": "The plugin's initial class file.",
"type": "string",
"pattern": "^(?!org\\.bukkit\\.)([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*$"
},
"version": {
"description": "A plugin revision identifier.",
"type": [
"string",
"number"
]
},
"description": {
"description": "Human readable plugin summary.",
"type": "string"
},
"author": {
"description": "The plugin author.",
"type": "string"
},
"authors": {
"description": "The plugin contributors.",
"type": "array",
"items": {
"type": "string"
}
},
"website": {
"title": "Website",
"description": "The URL to the plugin's site",
"type": "string",
"format": "uri"
},
"loadAfter": {
"description": "Plugins to load before this plugin.",
"type": "array",
"items": {
"type": "string"
}
}
} }
}
} }
\ No newline at end of file
...@@ -16,18 +16,18 @@ public interface ExternalAuthenticator { ...@@ -16,18 +16,18 @@ public interface ExternalAuthenticator {
/** /**
* Called when an external account creation request is made. * Called when an external account creation request is made.
* @param request The authentication request. * @param request The authentication request.
* *
* For developers: Use {@link AuthenticationRequest#getRequest()} to get the request body. * For developers: Use AuthenticationRequest#getRequest() to get the request body.
* Use {@link AuthenticationRequest#getResponse()} to get the response body. * Use AuthenticationRequest#getResponse() to get the response body.
*/ */
void handleAccountCreation(AuthenticationRequest request); void handleAccountCreation(AuthenticationRequest request);
/** /**
* Called when an external password reset request is made. * Called when an external password reset request is made.
* @param request The authentication request. * @param request The authentication request.
* *
* For developers: Use {@link AuthenticationRequest#getRequest()} to get the request body. * For developers: Use AuthenticationRequest#getRequest() to get the request body.
* Use {@link AuthenticationRequest#getResponse()} to get the response body. * Use AuthenticationRequest#getResponse() to get the response body.
*/ */
void handlePasswordReset(AuthenticationRequest request); void handlePasswordReset(AuthenticationRequest request);
} }
package emu.grasscutter.command; package emu.grasscutter.command;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.Player;
import org.reflections.Reflections; import org.reflections.Reflections;
import java.util.*; import java.util.*;
...@@ -11,6 +9,7 @@ import java.util.*; ...@@ -11,6 +9,7 @@ import java.util.*;
@SuppressWarnings({"UnusedReturnValue", "unused"}) @SuppressWarnings({"UnusedReturnValue", "unused"})
public final class CommandMap { public final class CommandMap {
private final Map<String, CommandHandler> commands = new HashMap<>(); private final Map<String, CommandHandler> commands = new HashMap<>();
private final Map<String, CommandHandler> aliases = new HashMap<>();
private final Map<String, Command> annotations = new HashMap<>(); private final Map<String, Command> annotations = new HashMap<>();
private final Map<String, Integer> targetPlayerIds = new HashMap<>(); private final Map<String, Integer> targetPlayerIds = new HashMap<>();
private static final String consoleId = "console"; private static final String consoleId = "console";
...@@ -45,7 +44,7 @@ public final class CommandMap { ...@@ -45,7 +44,7 @@ public final class CommandMap {
// Register aliases. // Register aliases.
if (annotation.aliases().length > 0) { if (annotation.aliases().length > 0) {
for (String alias : annotation.aliases()) { for (String alias : annotation.aliases()) {
this.commands.put(alias, command); this.aliases.put(alias, command);
this.annotations.put(alias, annotation); this.annotations.put(alias, annotation);
} }
} }
...@@ -60,6 +59,7 @@ public final class CommandMap { ...@@ -60,6 +59,7 @@ public final class CommandMap {
*/ */
public CommandMap unregisterCommand(String label) { public CommandMap unregisterCommand(String label) {
Grasscutter.getLogger().debug("Unregistered command: " + label); Grasscutter.getLogger().debug("Unregistered command: " + label);
CommandHandler handler = this.commands.get(label); CommandHandler handler = this.commands.get(label);
if (handler == null) return this; if (handler == null) return this;
...@@ -70,7 +70,7 @@ public final class CommandMap { ...@@ -70,7 +70,7 @@ public final class CommandMap {
// Unregister aliases. // Unregister aliases.
if (annotation.aliases().length > 0) { if (annotation.aliases().length > 0) {
for (String alias : annotation.aliases()) { for (String alias : annotation.aliases()) {
this.commands.remove(alias); this.aliases.remove(alias);
this.annotations.remove(alias); this.annotations.remove(alias);
} }
} }
...@@ -78,7 +78,9 @@ public final class CommandMap { ...@@ -78,7 +78,9 @@ public final class CommandMap {
return this; return this;
} }
public List<Command> getAnnotationsAsList() { return new LinkedList<>(this.annotations.values()); } public List<Command> getAnnotationsAsList() {
return new LinkedList<>(this.annotations.values());
}
public HashMap<String, Command> getAnnotations() { public HashMap<String, Command> getAnnotations() {
return new LinkedHashMap<>(this.annotations); return new LinkedHashMap<>(this.annotations);
...@@ -125,7 +127,7 @@ public final class CommandMap { ...@@ -125,7 +127,7 @@ public final class CommandMap {
List<String> args = new LinkedList<>(Arrays.asList(split)); List<String> args = new LinkedList<>(Arrays.asList(split));
String label = args.remove(0); String label = args.remove(0);
String playerId = (player == null) ? consoleId : player.getAccount().getId(); String playerId = (player == null) ? consoleId : player.getAccount().getId();
// Check for special cases - currently only target command. // Check for special cases - currently only target command.
String targetUidStr = null; String targetUidStr = null;
if (label.startsWith("@")) { // @[UID] if (label.startsWith("@")) { // @[UID]
...@@ -142,7 +144,7 @@ public final class CommandMap { ...@@ -142,7 +144,7 @@ public final class CommandMap {
} }
if (targetUidStr != null) { if (targetUidStr != null) {
if (targetUidStr.equals("")) { // Clears the default targetPlayer. if (targetUidStr.equals("")) { // Clears the default targetPlayer.
targetPlayerIds.remove(playerId); this.targetPlayerIds.remove(playerId);
CommandHandler.sendTranslatedMessage(player, "commands.execution.clear_target"); CommandHandler.sendTranslatedMessage(player, "commands.execution.clear_target");
} else { // Sets default targetPlayer to the UID provided. } else { // Sets default targetPlayer to the UID provided.
try { try {
...@@ -151,9 +153,9 @@ public final class CommandMap { ...@@ -151,9 +153,9 @@ public final class CommandMap {
if (targetPlayer == null) { if (targetPlayer == null) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error"); CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
} else { } else {
targetPlayerIds.put(playerId, uid); this.targetPlayerIds.put(playerId, uid);
CommandHandler.sendTranslatedMessage(player, "commands.execution.set_target", targetUidStr); CommandHandler.sendTranslatedMessage(player, "commands.execution.set_target", targetUidStr);
CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline()? "commands.execution.set_target_online" : "commands.execution.set_target_offline", targetUidStr); CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline() ? "commands.execution.set_target_online" : "commands.execution.set_target_offline", targetUidStr);
} }
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
CommandHandler.sendTranslatedMessage(player, "commands.generic.invalid.uid"); CommandHandler.sendTranslatedMessage(player, "commands.generic.invalid.uid");
...@@ -164,11 +166,19 @@ public final class CommandMap { ...@@ -164,11 +166,19 @@ public final class CommandMap {
// Get command handler. // Get command handler.
CommandHandler handler = this.commands.get(label); CommandHandler handler = this.commands.get(label);
if(handler == null)
// Try to get the handler by alias.
handler = this.aliases.get(label);
// Check if the handler is still null.
if (handler == null) { if (handler == null) {
CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label); CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label);
return; return;
} }
// Get the command's annotation.
Command annotation = this.annotations.get(label);
// If any @UID argument is present, override targetPlayer with it. // If any @UID argument is present, override targetPlayer with it.
for (int i = 0; i < args.size(); i++) { for (int i = 0; i < args.size(); i++) {
String arg = args.get(i); String arg = args.get(i);
...@@ -188,11 +198,11 @@ public final class CommandMap { ...@@ -188,11 +198,11 @@ public final class CommandMap {
} }
} }
} }
// If there's still no targetPlayer at this point, use previously-set target // If there's still no targetPlayer at this point, use previously-set target
if (targetPlayer == null) { if (targetPlayer == null) {
if (targetPlayerIds.containsKey(playerId)) { if (this.targetPlayerIds.containsKey(playerId)) {
targetPlayer = Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.get(playerId), true); // We check every time in case the target is deleted after being targeted targetPlayer = Grasscutter.getGameServer().getPlayerByUid(this.targetPlayerIds.get(playerId), true); // We check every time in case the target is deleted after being targeted
if (targetPlayer == null) { if (targetPlayer == null) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error"); CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
return; return;
...@@ -204,32 +214,36 @@ public final class CommandMap { ...@@ -204,32 +214,36 @@ public final class CommandMap {
} }
// Check for permissions. // Check for permissions.
if (!Grasscutter.getPermissionHandler().checkPermission(player, targetPlayer, this.annotations.get(label).permission(), this.annotations.get(label).permissionTargeted())) { if (!Grasscutter.getPermissionHandler().checkPermission(player, targetPlayer, annotation.permission(), this.annotations.get(label).permissionTargeted())) {
return; return;
} }
// Check if command has unfulfilled constraints on targetPlayer // Check if command has unfulfilled constraints on targetPlayer
Command.TargetRequirement targetRequirement = this.annotations.get(label).targetRequirement(); Command.TargetRequirement targetRequirement = annotation.targetRequirement();
if (targetRequirement != Command.TargetRequirement.NONE) { if (targetRequirement != Command.TargetRequirement.NONE) {
if (targetPlayer == null) { if (targetPlayer == null) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target"); CommandHandler.sendTranslatedMessage(null, "commands.execution.need_target");
return; return;
} }
if ((targetRequirement == Command.TargetRequirement.ONLINE) && !targetPlayer.isOnline()) { if ((targetRequirement == Command.TargetRequirement.ONLINE) && !targetPlayer.isOnline()) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_online"); CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_online");
return; return;
} }
if ((targetRequirement == Command.TargetRequirement.OFFLINE) && targetPlayer.isOnline()) { if ((targetRequirement == Command.TargetRequirement.OFFLINE) && targetPlayer.isOnline()) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_offline"); CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_offline");
return; return;
} }
} }
// Copy player and handler to final properties.
final Player targetPlayerF = targetPlayer; // Is there a better way to do this?
final CommandHandler handlerF = handler; // Is there a better way to do this?
// Invoke execute method for handler. // Invoke execute method for handler.
boolean threading = this.annotations.get(label).threading(); Runnable runnable = () -> handlerF.execute(player, targetPlayerF, args);
final Player targetPlayerF = targetPlayer; // Is there a better way to do this? if (annotation.threading()) {
Runnable runnable = () -> handler.execute(player, targetPlayerF, args);
if(threading) {
new Thread(runnable).start(); new Thread(runnable).start();
} else { } else {
runnable.run(); runnable.run();
...@@ -242,10 +256,11 @@ public final class CommandMap { ...@@ -242,10 +256,11 @@ public final class CommandMap {
private void scan() { private void scan() {
Reflections reflector = Grasscutter.reflector; Reflections reflector = Grasscutter.reflector;
Set<Class<?>> classes = reflector.getTypesAnnotatedWith(Command.class); Set<Class<?>> classes = reflector.getTypesAnnotatedWith(Command.class);
classes.forEach(annotated -> { classes.forEach(annotated -> {
try { try {
Command cmdData = annotated.getAnnotation(Command.class); Command cmdData = annotated.getAnnotation(Command.class);
Object object = annotated.newInstance(); Object object = annotated.getDeclaredConstructor().newInstance();
if (object instanceof CommandHandler) if (object instanceof CommandHandler)
this.registerCommand(cmdData.label(), (CommandHandler) object); this.registerCommand(cmdData.label(), (CommandHandler) object);
else Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a CommandHandler!"); else Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a CommandHandler!");
......
...@@ -6,11 +6,11 @@ import emu.grasscutter.tools.Tools; ...@@ -6,11 +6,11 @@ import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.FileUtils; import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import java.io.*; import java.io.FileInputStream;
import java.nio.file.FileSystems; import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
import static emu.grasscutter.Configuration.DATA; import static emu.grasscutter.Configuration.DATA;
...@@ -18,10 +18,11 @@ public class DataLoader { ...@@ -18,10 +18,11 @@ public class DataLoader {
/** /**
* Load a data file by its name. If the file isn't found within the /data directory then it will fallback to the default within the jar resources * Load a data file by its name. If the file isn't found within the /data directory then it will fallback to the default within the jar resources
* @see #load(String, boolean) *
* @param resourcePath The path to the data file to be loaded. * @param resourcePath The path to the data file to be loaded.
* @return InputStream of the data file. * @return InputStream of the data file.
* @throws FileNotFoundException * @throws FileNotFoundException
* @see #load(String, boolean)
*/ */
public static InputStream load(String resourcePath) throws FileNotFoundException { public static InputStream load(String resourcePath) throws FileNotFoundException {
return load(resourcePath, true); return load(resourcePath, true);
...@@ -29,17 +30,18 @@ public class DataLoader { ...@@ -29,17 +30,18 @@ public class DataLoader {
/** /**
* Load a data file by its name. * Load a data file by its name.
*
* @param resourcePath The path to the data file to be loaded. * @param resourcePath The path to the data file to be loaded.
* @param useFallback If the file does not exist in the /data directory, should it use the default file in the jar? * @param useFallback If the file does not exist in the /data directory, should it use the default file in the jar?
* @return InputStream of the data file. * @return InputStream of the data file.
* @throws FileNotFoundException * @throws FileNotFoundException
*/ */
public static InputStream load(String resourcePath, boolean useFallback) throws FileNotFoundException { public static InputStream load(String resourcePath, boolean useFallback) throws FileNotFoundException {
if(Utils.fileExists(DATA(resourcePath))) { if (Utils.fileExists(DATA(resourcePath))) {
// Data is in the resource directory // Data is in the resource directory
return new FileInputStream(DATA(resourcePath)); return new FileInputStream(DATA(resourcePath));
} else { } else {
if(useFallback) { if (useFallback) {
return FileUtils.readResourceAsStream("/defaults/data/" + resourcePath); return FileUtils.readResourceAsStream("/defaults/data/" + resourcePath);
} }
} }
...@@ -50,12 +52,10 @@ public class DataLoader { ...@@ -50,12 +52,10 @@ public class DataLoader {
public static void CheckAllFiles() { public static void CheckAllFiles() {
try { try {
List<Path> filenames = FileUtils.getPathsFromResource("/defaults/data/"); List<Path> filenames = FileUtils.getPathsFromResource("/defaults/data/");
if (filenames == null) {
Grasscutter.getLogger().error("We were unable to locate your default data files.");
}
for (Path file : filenames) { if (filenames == null) {
Grasscutter.getLogger().error("We were unable to locate your default data files.");
} else for (Path file : filenames) {
String relativePath = String.valueOf(file).split("defaults[\\\\\\/]data[\\\\\\/]")[1]; String relativePath = String.valueOf(file).split("defaults[\\\\\\/]data[\\\\\\/]")[1];
CheckAndCopyData(relativePath); CheckAndCopyData(relativePath);
...@@ -76,12 +76,12 @@ public class DataLoader { ...@@ -76,12 +76,12 @@ public class DataLoader {
String[] path = name.split("/"); String[] path = name.split("/");
String folder = ""; String folder = "";
for(int i = 0; i < (path.length - 1); i++) { for (int i = 0; i < (path.length - 1); i++) {
folder += path[i] + "/"; folder += path[i] + "/";
// Make sure the current folder exists // Make sure the current folder exists
String folderToCreate = Utils.toFilePath(DATA(folder)); String folderToCreate = Utils.toFilePath(DATA(folder));
if(!Utils.fileExists(folderToCreate)) { if (!Utils.fileExists(folderToCreate)) {
Grasscutter.getLogger().info("Creating data folder '" + folder + "'"); Grasscutter.getLogger().info("Creating data folder '" + folder + "'");
Utils.createFolder(folderToCreate); Utils.createFolder(folderToCreate);
} }
......
...@@ -39,11 +39,11 @@ public final class DatabaseHelper { ...@@ -39,11 +39,11 @@ public final class DatabaseHelper {
if (reservedUid == GameConstants.SERVER_CONSOLE_UID) { if (reservedUid == GameConstants.SERVER_CONSOLE_UID) {
return null; return null;
} }
if (DatabaseHelper.checkIfAccountExists(reservedUid)) { if (DatabaseHelper.checkIfAccountExists(reservedUid)) {
return null; return null;
} }
// Make sure no existing player already has this id. // Make sure no existing player already has this id.
if (DatabaseHelper.checkIfPlayerExists(reservedUid)) { if (DatabaseHelper.checkIfPlayerExists(reservedUid)) {
return null; return null;
...@@ -105,11 +105,11 @@ public final class DatabaseHelper { ...@@ -105,11 +105,11 @@ public final class DatabaseHelper {
public static Account getAccountByPlayerId(int playerId) { public static Account getAccountByPlayerId(int playerId) {
return DatabaseManager.getAccountDatastore().find(Account.class).filter(Filters.eq("reservedPlayerId", playerId)).first(); return DatabaseManager.getAccountDatastore().find(Account.class).filter(Filters.eq("reservedPlayerId", playerId)).first();
} }
public static boolean checkIfAccountExists(String name) { public static boolean checkIfAccountExists(String name) {
return DatabaseManager.getAccountDatastore().find(Account.class).filter(Filters.eq("username", name)).count() > 0; return DatabaseManager.getAccountDatastore().find(Account.class).filter(Filters.eq("username", name)).count() > 0;
} }
public static boolean checkIfAccountExists(int reservedUid) { public static boolean checkIfAccountExists(int reservedUid) {
return DatabaseManager.getAccountDatastore().find(Account.class).filter(Filters.eq("reservedPlayerId", reservedUid)).count() > 0; return DatabaseManager.getAccountDatastore().find(Account.class).filter(Filters.eq("reservedPlayerId", reservedUid)).count() > 0;
} }
...@@ -120,11 +120,11 @@ public final class DatabaseHelper { ...@@ -120,11 +120,11 @@ public final class DatabaseHelper {
// database in an inconsistent state, but unfortunately Mongo only supports that when we have a replica set ... // database in an inconsistent state, but unfortunately Mongo only supports that when we have a replica set ...
Player player = Grasscutter.getGameServer().getPlayerByAccountId(target.getId()); Player player = Grasscutter.getGameServer().getPlayerByAccountId(target.getId());
if (player != null) { if (player != null) {
// Close session first // Close session first
player.getSession().close(); player.getSession().close();
// Delete data from collections // Delete data from collections
DatabaseManager.getGameDatabase().getCollection("mail").deleteMany(eq("ownerUid", player.getUid())); DatabaseManager.getGameDatabase().getCollection("mail").deleteMany(eq("ownerUid", player.getUid()));
DatabaseManager.getGameDatabase().getCollection("avatars").deleteMany(eq("ownerId", player.getUid())); DatabaseManager.getGameDatabase().getCollection("avatars").deleteMany(eq("ownerId", player.getUid()));
...@@ -153,11 +153,16 @@ public final class DatabaseHelper { ...@@ -153,11 +153,16 @@ public final class DatabaseHelper {
public static Player getPlayerByUid(int id) { public static Player getPlayerByUid(int id) {
return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", id)).first(); return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", id)).first();
} }
@Deprecated
public static Player getPlayerByAccount(Account account) { public static Player getPlayerByAccount(Account account) {
return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("accountId", account.getId())).first(); return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("accountId", account.getId())).first();
} }
public static Player getPlayerByAccount(Account account, Class<? extends Player> playerClass) {
return DatabaseManager.getGameDatastore().find(playerClass).filter(Filters.eq("accountId", account.getId())).first();
}
public static boolean checkIfPlayerExists(int uid) { public static boolean checkIfPlayerExists(int uid) {
return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", uid)).count() > 0; return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", uid)).count() > 0;
} }
...@@ -218,7 +223,7 @@ public final class DatabaseHelper { ...@@ -218,7 +223,7 @@ public final class DatabaseHelper {
public static List<GameItem> getInventoryItems(Player player) { public static List<GameItem> getInventoryItems(Player player) {
return DatabaseManager.getGameDatastore().find(GameItem.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(GameItem.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList();
} }
public static List<Friendship> getFriends(Player player) { public static List<Friendship> getFriends(Player player) {
return DatabaseManager.getGameDatastore().find(Friendship.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(Friendship.class).filter(Filters.eq("ownerId", player.getUid())).stream().toList();
} }
...@@ -272,40 +277,40 @@ public final class DatabaseHelper { ...@@ -272,40 +277,40 @@ public final class DatabaseHelper {
public static void saveGachaRecord(GachaRecord gachaRecord){ public static void saveGachaRecord(GachaRecord gachaRecord){
DatabaseManager.getGameDatastore().save(gachaRecord); DatabaseManager.getGameDatastore().save(gachaRecord);
} }
public static List<Mail> getAllMail(Player player) { public static List<Mail> getAllMail(Player player) {
return DatabaseManager.getGameDatastore().find(Mail.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(Mail.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList();
} }
public static void saveMail(Mail mail) { public static void saveMail(Mail mail) {
DatabaseManager.getGameDatastore().save(mail); DatabaseManager.getGameDatastore().save(mail);
} }
public static boolean deleteMail(Mail mail) { public static boolean deleteMail(Mail mail) {
DeleteResult result = DatabaseManager.getGameDatastore().delete(mail); DeleteResult result = DatabaseManager.getGameDatastore().delete(mail);
return result.wasAcknowledged(); return result.wasAcknowledged();
} }
public static List<GameMainQuest> getAllQuests(Player player) { public static List<GameMainQuest> getAllQuests(Player player) {
return DatabaseManager.getGameDatastore().find(GameMainQuest.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList(); return DatabaseManager.getGameDatastore().find(GameMainQuest.class).filter(Filters.eq("ownerUid", player.getUid())).stream().toList();
} }
public static void saveQuest(GameMainQuest quest) { public static void saveQuest(GameMainQuest quest) {
DatabaseManager.getGameDatastore().save(quest); DatabaseManager.getGameDatastore().save(quest);
} }
public static boolean deleteQuest(GameMainQuest quest) { public static boolean deleteQuest(GameMainQuest quest) {
return DatabaseManager.getGameDatastore().delete(quest).wasAcknowledged(); return DatabaseManager.getGameDatastore().delete(quest).wasAcknowledged();
} }
public static GameHome getHomeByUid(int id) { public static GameHome getHomeByUid(int id) {
return DatabaseManager.getGameDatastore().find(GameHome.class).filter(Filters.eq("ownerUid", id)).first(); return DatabaseManager.getGameDatastore().find(GameHome.class).filter(Filters.eq("ownerUid", id)).first();
} }
public static void saveHome(GameHome gameHome) { public static void saveHome(GameHome gameHome) {
DatabaseManager.getGameDatastore().save(gameHome); DatabaseManager.getGameDatastore().save(gameHome);
} }
public static BattlePassManager loadBattlePass(Player player) { public static BattlePassManager loadBattlePass(Player player) {
BattlePassManager manager = DatabaseManager.getGameDatastore().find(BattlePassManager.class).filter(Filters.eq("ownerUid", player.getUid())).first(); BattlePassManager manager = DatabaseManager.getGameDatastore().find(BattlePassManager.class).filter(Filters.eq("ownerUid", player.getUid())).first();
if (manager == null) { if (manager == null) {
...@@ -316,7 +321,7 @@ public final class DatabaseHelper { ...@@ -316,7 +321,7 @@ public final class DatabaseHelper {
} }
return manager; return manager;
} }
public static void saveBattlePass(BattlePassManager manager) { public static void saveBattlePass(BattlePassManager manager) {
DatabaseManager.getGameDatastore().save(manager); DatabaseManager.getGameDatastore().save(manager);
} }
......
package emu.grasscutter.game.entity;
import emu.grasscutter.game.props.EntityIdType;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.proto.SceneEntityInfoOuterClass;
import emu.grasscutter.scripts.data.SceneRegion;
import emu.grasscutter.utils.Position;
import it.unimi.dsi.fastutil.ints.Int2FloatOpenHashMap;
import lombok.Getter;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Getter
public class EntityRegion extends GameEntity{
private final Position position;
private boolean hasNewEntities;
private final Set<Integer> entities; // Ids of entities inside this region
private final SceneRegion metaRegion;
public EntityRegion(Scene scene, SceneRegion region) {
super(scene);
this.id = getScene().getWorld().getNextEntityId(EntityIdType.REGION);
setGroupId(region.group.id);
setBlockId(region.group.block_id);
setConfigId(region.config_id);
this.position = region.pos.clone();
this.entities = ConcurrentHashMap.newKeySet();
this.metaRegion = region;
}
public void addEntity(GameEntity entity) {
if (this.getEntities().contains(entity.getId())) {
return;
}
this.getEntities().add(entity.getId());
this.hasNewEntities = true;
}
public boolean hasNewEntities() {
return hasNewEntities;
}
public void resetNewEntities() {
hasNewEntities = false;
}
public void removeEntity(GameEntity entity) {
this.getEntities().remove(entity.getId());
}
@Override
public Int2FloatOpenHashMap getFightProperties() {
return null;
}
@Override
public Position getPosition() {
return position;
}
@Override
public Position getRotation() {
return null;
}
@Override
public SceneEntityInfoOuterClass.SceneEntityInfo toProto() {
/**
* The Region Entity would not be sent to client.
*/
return null;
}
}
...@@ -125,7 +125,7 @@ public class Player { ...@@ -125,7 +125,7 @@ public class Player {
@Transient private MessageHandler messageHandler; @Transient private MessageHandler messageHandler;
@Transient private AbilityManager abilityManager; @Transient private AbilityManager abilityManager;
@Transient private QuestManager questManager; @Transient private QuestManager questManager;
@Transient private SotSManager sotsManager; @Transient private SotSManager sotsManager;
@Transient private InsectCaptureManager insectCaptureManager; @Transient private InsectCaptureManager insectCaptureManager;
...@@ -436,7 +436,7 @@ public class Player { ...@@ -436,7 +436,7 @@ public class Player {
public int getWorldLevel() { public int getWorldLevel() {
return this.getProperty(PlayerProperty.PROP_PLAYER_WORLD_LEVEL); return this.getProperty(PlayerProperty.PROP_PLAYER_WORLD_LEVEL);
} }
public void setWorldLevel(int level) { public void setWorldLevel(int level) {
this.setProperty(PlayerProperty.PROP_PLAYER_WORLD_LEVEL, level); this.setProperty(PlayerProperty.PROP_PLAYER_WORLD_LEVEL, level);
this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_WORLD_LEVEL)); this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_WORLD_LEVEL));
...@@ -459,7 +459,7 @@ public class Player { ...@@ -459,7 +459,7 @@ public class Player {
this.setProperty(PlayerProperty.PROP_PLAYER_SCOIN, mora); this.setProperty(PlayerProperty.PROP_PLAYER_SCOIN, mora);
this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_SCOIN)); this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_SCOIN));
} }
public int getCrystals() { public int getCrystals() {
return this.getProperty(PlayerProperty.PROP_PLAYER_MCOIN); return this.getProperty(PlayerProperty.PROP_PLAYER_MCOIN);
} }
...@@ -534,11 +534,11 @@ public class Player { ...@@ -534,11 +534,11 @@ public class Player {
public TeamManager getTeamManager() { public TeamManager getTeamManager() {
return this.teamManager; return this.teamManager;
} }
public TowerManager getTowerManager() { public TowerManager getTowerManager() {
return towerManager; return towerManager;
} }
public TowerData getTowerData() { public TowerData getTowerData() {
if(towerData==null){ if(towerData==null){
// because of mistake, null may be saved as storage at some machine, this if can be removed in future // because of mistake, null may be saved as storage at some machine, this if can be removed in future
...@@ -546,7 +546,7 @@ public class Player { ...@@ -546,7 +546,7 @@ public class Player {
} }
return towerData; return towerData;
} }
public QuestManager getQuestManager() { public QuestManager getQuestManager() {
return questManager; return questManager;
} }
...@@ -615,7 +615,7 @@ public class Player { ...@@ -615,7 +615,7 @@ public class Player {
public MpSettingType getMpSetting() { public MpSettingType getMpSetting() {
return MpSettingType.MP_SETTING_TYPE_ENTER_AFTER_APPLY; // TEMP return MpSettingType.MP_SETTING_TYPE_ENTER_AFTER_APPLY; // TEMP
} }
public Queue<AttackResult> getAttackResults() { public Queue<AttackResult> getAttackResults() {
return this.attackResults; return this.attackResults;
} }
...@@ -810,7 +810,7 @@ public class Player { ...@@ -810,7 +810,7 @@ public class Player {
remainCalendar.add(Calendar.DATE, moonCardDuration); remainCalendar.add(Calendar.DATE, moonCardDuration);
Date theLastDay = remainCalendar.getTime(); Date theLastDay = remainCalendar.getTime();
Date now = DateHelper.onlyYearMonthDay(new Date()); Date now = DateHelper.onlyYearMonthDay(new Date());
return (int) ((theLastDay.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)); // By copilot return (int) ((theLastDay.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)); // By copilot
} }
public void rechargeMoonCard() { public void rechargeMoonCard() {
...@@ -1007,7 +1007,7 @@ public class Player { ...@@ -1007,7 +1007,7 @@ public class Player {
} }
public Mail getMail(int index) { return this.getMailHandler().getMailById(index); } public Mail getMail(int index) { return this.getMailHandler().getMailById(index); }
public int getMailId(Mail message) { public int getMailId(Mail message) {
return this.getMailHandler().getMailIndex(message); return this.getMailHandler().getMailIndex(message);
} }
...@@ -1015,9 +1015,9 @@ public class Player { ...@@ -1015,9 +1015,9 @@ public class Player {
public boolean replaceMailByIndex(int index, Mail message) { public boolean replaceMailByIndex(int index, Mail message) {
return this.getMailHandler().replaceMailByIndex(index, message); return this.getMailHandler().replaceMailByIndex(index, message);
} }
public void interactWith(int gadgetEntityId, GadgetInteractReq req) {
public void interactWith(int gadgetEntityId, GadgetInteractReq opType) {
GameEntity entity = getScene().getEntityById(gadgetEntityId); GameEntity entity = getScene().getEntityById(gadgetEntityId);
if (entity == null) { if (entity == null) {
return; return;
...@@ -1045,13 +1045,13 @@ public class Player { ...@@ -1045,13 +1045,13 @@ public class Player {
} }
} }
} else if (entity instanceof EntityGadget gadget) { } else if (entity instanceof EntityGadget gadget) {
if (gadget.getContent() == null) { if (gadget.getContent() == null) {
return; return;
} }
boolean shouldDelete = gadget.getContent().onInteract(this, req); boolean shouldDelete = gadget.getContent().onInteract(this, opType);
if (shouldDelete) { if (shouldDelete) {
entity.getScene().removeEntity(entity); entity.getScene().removeEntity(entity);
} }
...@@ -1195,7 +1195,7 @@ public class Player { ...@@ -1195,7 +1195,7 @@ public class Player {
} }
return showAvatarInfoList; return showAvatarInfoList;
} }
public PlayerWorldLocationInfoOuterClass.PlayerWorldLocationInfo getWorldPlayerLocationInfo() { public PlayerWorldLocationInfoOuterClass.PlayerWorldLocationInfo getWorldPlayerLocationInfo() {
return PlayerWorldLocationInfoOuterClass.PlayerWorldLocationInfo.newBuilder() return PlayerWorldLocationInfoOuterClass.PlayerWorldLocationInfo.newBuilder()
.setSceneId(this.getSceneId()) .setSceneId(this.getSceneId())
...@@ -1238,7 +1238,7 @@ public class Player { ...@@ -1238,7 +1238,7 @@ public class Player {
public BattlePassManager getBattlePassManager(){ public BattlePassManager getBattlePassManager(){
return battlePassManager; return battlePassManager;
} }
public void loadBattlePassManager() { public void loadBattlePassManager() {
if (this.battlePassManager != null) return; if (this.battlePassManager != null) return;
this.battlePassManager = DatabaseHelper.loadBattlePass(this); this.battlePassManager = DatabaseHelper.loadBattlePass(this);
...@@ -1328,7 +1328,7 @@ public class Player { ...@@ -1328,7 +1328,7 @@ public class Player {
public void save() { public void save() {
DatabaseHelper.savePlayer(this); DatabaseHelper.savePlayer(this);
} }
// Called from tokenrsp // Called from tokenrsp
public void loadFromDatabase() { public void loadFromDatabase() {
// Make sure these exist // Make sure these exist
...@@ -1346,7 +1346,7 @@ public class Player { ...@@ -1346,7 +1346,7 @@ public class Player {
} }
//Make sure towerManager's player is online player //Make sure towerManager's player is online player
this.getTowerManager().setPlayer(this); this.getTowerManager().setPlayer(this);
// Load from db // Load from db
this.getAvatars().loadFromDatabase(); this.getAvatars().loadFromDatabase();
this.getInventory().loadFromDatabase(); this.getInventory().loadFromDatabase();
...@@ -1355,7 +1355,7 @@ public class Player { ...@@ -1355,7 +1355,7 @@ public class Player {
this.getFriendsList().loadFromDatabase(); this.getFriendsList().loadFromDatabase();
this.getMailHandler().loadFromDatabase(); this.getMailHandler().loadFromDatabase();
this.getQuestManager().loadFromDatabase(); this.getQuestManager().loadFromDatabase();
this.loadBattlePassManager(); this.loadBattlePassManager();
} }
...@@ -1368,12 +1368,12 @@ public class Player { ...@@ -1368,12 +1368,12 @@ public class Player {
quest.finish(); quest.finish();
} }
getQuestManager().addQuest(35101); getQuestManager().addQuest(35101);
this.setSceneId(3); this.setSceneId(3);
this.getPos().set(GameConstants.START_POSITION); this.getPos().set(GameConstants.START_POSITION);
} }
*/ */
// Create world // Create world
World world = new World(this); World world = new World(this);
world.addPlayer(this); world.addPlayer(this);
...@@ -1410,7 +1410,7 @@ public class Player { ...@@ -1410,7 +1410,7 @@ public class Player {
// First notify packets sent // First notify packets sent
this.setHasSentAvatarDataNotify(true); this.setHasSentAvatarDataNotify(true);
// Set session state // Set session state
session.setState(SessionState.ACTIVE); session.setState(SessionState.ACTIVE);
...@@ -1420,7 +1420,7 @@ public class Player { ...@@ -1420,7 +1420,7 @@ public class Player {
session.close(); session.close();
return; return;
} }
// register // register
getServer().registerPlayer(this); getServer().registerPlayer(this);
getProfile().setPlayer(this); // Set online getProfile().setPlayer(this); // Set online
......
...@@ -5,10 +5,11 @@ public enum EntityIdType { ...@@ -5,10 +5,11 @@ public enum EntityIdType {
MONSTER (0x02), MONSTER (0x02),
NPC (0x03), NPC (0x03),
GADGET (0x04), GADGET (0x04),
WEAPON (0x06), REGION (0x05),
WEAPON (0x06),
TEAM (0x09), TEAM (0x09),
MPLEVEL (0x0b); MPLEVEL (0x0b);
private final int id; private final int id;
private EntityIdType(int id) { private EntityIdType(int id) {
......
...@@ -30,22 +30,23 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; ...@@ -30,22 +30,23 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.danilopianini.util.SpatialIndex; import org.danilopianini.util.SpatialIndex;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class Scene { public class Scene {
private final World world; private final World world;
private final SceneData sceneData; private final SceneData sceneData;
private final List<Player> players; private final List<Player> players;
private final Int2ObjectMap<GameEntity> entities; private final Map<Integer, GameEntity> entities;
private final Set<SpawnDataEntry> spawnedEntities; private final Set<SpawnDataEntry> spawnedEntities;
private final Set<SpawnDataEntry> deadSpawnedEntities; private final Set<SpawnDataEntry> deadSpawnedEntities;
private final Set<SceneBlock> loadedBlocks; private final Set<SceneBlock> loadedBlocks;
private boolean dontDestroyWhenEmpty; private boolean dontDestroyWhenEmpty;
private int autoCloseTime; private int autoCloseTime;
private int time; private int time;
private SceneScriptManager scriptManager; private SceneScriptManager scriptManager;
private WorldChallenge challenge; private WorldChallenge challenge;
private List<DungeonSettleListener> dungeonSettleListeners; private List<DungeonSettleListener> dungeonSettleListeners;
...@@ -55,18 +56,18 @@ public class Scene { ...@@ -55,18 +56,18 @@ public class Scene {
public Scene(World world, SceneData sceneData) { public Scene(World world, SceneData sceneData) {
this.world = world; this.world = world;
this.sceneData = sceneData; this.sceneData = sceneData;
this.players = Collections.synchronizedList(new ArrayList<>()); this.players = new CopyOnWriteArrayList<>();
this.entities = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); this.entities = new ConcurrentHashMap<>();
this.time = 8 * 60; this.time = 8 * 60;
this.prevScene = 3; this.prevScene = 3;
this.spawnedEntities = new HashSet<>(); this.spawnedEntities = ConcurrentHashMap.newKeySet();
this.deadSpawnedEntities = new HashSet<>(); this.deadSpawnedEntities = ConcurrentHashMap.newKeySet();
this.loadedBlocks = new HashSet<>(); this.loadedBlocks = ConcurrentHashMap.newKeySet();
this.scriptManager = new SceneScriptManager(this); this.scriptManager = new SceneScriptManager(this);
} }
public int getId() { public int getId() {
return sceneData.getId(); return sceneData.getId();
} }
...@@ -86,15 +87,15 @@ public class Scene { ...@@ -86,15 +87,15 @@ public class Scene {
public List<Player> getPlayers() { public List<Player> getPlayers() {
return players; return players;
} }
public int getPlayerCount() { public int getPlayerCount() {
return this.getPlayers().size(); return this.getPlayers().size();
} }
public Int2ObjectMap<GameEntity> getEntities() { public Map<Integer, GameEntity> getEntities() {
return entities; return entities;
} }
public GameEntity getEntityById(int id) { public GameEntity getEntityById(int id) {
return this.entities.get(id); return this.entities.get(id);
} }
...@@ -610,15 +611,10 @@ public class Scene { ...@@ -610,15 +611,10 @@ public class Scene {
var suiteData = group.getSuiteByIndex(suite); var suiteData = group.getSuiteByIndex(suite);
suiteData.sceneTriggers.forEach(getScriptManager()::registerTrigger); suiteData.sceneTriggers.forEach(getScriptManager()::registerTrigger);
entities.addAll(suiteData.sceneGadgets.stream() entities.addAll(scriptManager.getGadgetsInGroupSuite(group, suiteData));
.map(g -> scriptManager.createGadget(group.id, group.block_id, g)) entities.addAll(scriptManager.getMonstersInGroupSuite(group, suiteData));
.filter(Objects::nonNull)
.toList());
entities.addAll(suiteData.sceneMonsters.stream()
.map(mob -> scriptManager.createMonster(group.id, group.block_id, mob))
.filter(Objects::nonNull)
.toList());
scriptManager.registerRegionInGroupSuite(group, suiteData);
} }
scriptManager.meetEntities(entities); scriptManager.meetEntities(entities);
...@@ -635,19 +631,18 @@ public class Scene { ...@@ -635,19 +631,18 @@ public class Scene {
toRemove.forEach(this::removeEntityDirectly); toRemove.forEach(this::removeEntityDirectly);
this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE)); this.broadcastPacket(new PacketSceneEntityDisappearNotify(toRemove, VisionType.VISION_TYPE_REMOVE));
} }
for (SceneGroup group : block.groups.values()) { for (SceneGroup group : block.groups.values()) {
if(group.triggers != null){ if(group.triggers != null){
group.triggers.values().forEach(getScriptManager()::deregisterTrigger); group.triggers.values().forEach(getScriptManager()::deregisterTrigger);
} }
if(group.regions != null){ if(group.regions != null){
group.regions.forEach(getScriptManager()::deregisterRegion); group.regions.values().forEach(getScriptManager()::deregisterRegion);
} }
} }
scriptManager.getLoadedGroupSetPerBlock().remove(block.id); scriptManager.getLoadedGroupSetPerBlock().remove(block.id);
Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id); Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id);
} }
// Gadgets // Gadgets
public void onPlayerCreateGadget(EntityClientGadget gadget) { public void onPlayerCreateGadget(EntityClientGadget gadget) {
......
...@@ -7,11 +7,13 @@ public final class PluginConfig { ...@@ -7,11 +7,13 @@ public final class PluginConfig {
public String name, description, version; public String name, description, version;
public String mainClass; public String mainClass;
public String[] authors; public String[] authors;
public String[] loadAfter;
/** /**
* Attempts to validate this config instance. * Attempts to validate this config instance.
* @return True if the config is valid, false otherwise. * @return True if the config is valid, false otherwise.
*/ */
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean validate() { public boolean validate() {
return name != null && description != null && mainClass != null; return name != null && description != null && mainClass != null;
} }
......
package emu.grasscutter.plugin; package emu.grasscutter.plugin;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.event.Event; import emu.grasscutter.server.event.*;
import emu.grasscutter.server.event.EventHandler;
import emu.grasscutter.server.event.HandlerPriority;
import emu.grasscutter.utils.Utils; import emu.grasscutter.utils.Utils;
import lombok.*;
import java.io.File; import javax.annotation.Nullable;
import java.io.FileNotFoundException; import java.io.*;
import java.io.InputStreamReader;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.MalformedURLException; import java.net.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*; import java.util.*;
import java.util.jar.JarEntry; import java.util.jar.*;
import java.util.jar.JarFile;
import static emu.grasscutter.Configuration.*; import static emu.grasscutter.Configuration.PLUGIN;
/** /**
* Manages the server's plugins and the event system. * Manages the server's plugins and the event system.
*/ */
public final class PluginManager { public final class PluginManager {
private final Map<String, Plugin> plugins = new HashMap<>(); /* All loaded plugins. */
private final List<EventHandler<? extends Event>> listeners = new LinkedList<>(); private final Map<String, Plugin> plugins = new LinkedHashMap<>();
/* All currently registered listeners per plugin. */
private final Map<Plugin, List<EventHandler<? extends Event>>> listeners = new LinkedHashMap<>();
public PluginManager() { public PluginManager() {
this.loadPlugins(); // Load all plugins from the plugins directory. this.loadPlugins(); // Load all plugins from the plugins directory.
} }
/* Data about an unloaded plugin. */
@AllArgsConstructor @Getter
static class PluginData {
private Plugin plugin;
private PluginIdentifier identifier;
private URLClassLoader classLoader;
private String[] dependencies;
}
/** /**
* Loads plugins from the config-specified directory. * Loads plugins from the config-specified directory.
*/ */
private void loadPlugins() { private void loadPlugins() {
File pluginsDir = new File(Utils.toFilePath(PLUGIN())); File pluginsDir = new File(Utils.toFilePath(PLUGIN()));
if(!pluginsDir.exists() && !pluginsDir.mkdirs()) { if (!pluginsDir.exists() && !pluginsDir.mkdirs()) {
Grasscutter.getLogger().error("Failed to create plugins directory: " + pluginsDir.getAbsolutePath()); Grasscutter.getLogger().error("Failed to create plugins directory: " + pluginsDir.getAbsolutePath());
return; return;
} }
File[] files = pluginsDir.listFiles(); File[] files = pluginsDir.listFiles();
if(files == null) { if (files == null) {
// The directory is empty, there aren't any plugins to load. // The directory is empty, there aren't any plugins to load.
return; return;
} }
List<File> plugins = Arrays.stream(files) List<File> plugins = Arrays.stream(files)
.filter(file -> file.getName().endsWith(".jar")) .filter(file -> file.getName().endsWith(".jar"))
.toList(); .toList();
URL[] pluginNames = new URL[plugins.size()]; URL[] pluginNames = new URL[plugins.size()];
plugins.forEach(plugin -> { plugins.forEach(plugin -> {
...@@ -59,36 +65,59 @@ public final class PluginManager { ...@@ -59,36 +65,59 @@ public final class PluginManager {
} }
}); });
// Create a class loader for the plugins.
URLClassLoader classLoader = new URLClassLoader(pluginNames); URLClassLoader classLoader = new URLClassLoader(pluginNames);
// Create a list of plugins that require dependencies.
List<PluginData> dependencies = new ArrayList<>();
plugins.forEach(plugin -> { // Initialize all plugins.
for(var plugin : plugins) {
try { try {
URL url = plugin.toURI().toURL(); URL url = plugin.toURI().toURL();
try (URLClassLoader loader = new URLClassLoader(new URL[]{url})) { try (URLClassLoader loader = new URLClassLoader(new URL[]{url})) {
URL configFile = loader.findResource("plugin.json"); // Find the plugin.json file for each plugin. // Find the plugin.json file for each plugin.
URL configFile = loader.findResource("plugin.json");
// Open the config file for reading.
InputStreamReader fileReader = new InputStreamReader(configFile.openStream()); InputStreamReader fileReader = new InputStreamReader(configFile.openStream());
// Create a plugin config instance from the config file.
PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class); PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class);
if(!pluginConfig.validate()) { // Check if the plugin config is valid.
if (!pluginConfig.validate()) {
Utils.logObject(pluginConfig); Utils.logObject(pluginConfig);
Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid config file."); Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid config file.");
return; return;
} }
// Create a JAR file instance from the plugin's URL.
JarFile jarFile = new JarFile(plugin); JarFile jarFile = new JarFile(plugin);
// Load all class files from the JAR file.
Enumeration<JarEntry> entries = jarFile.entries(); Enumeration<JarEntry> entries = jarFile.entries();
while(entries.hasMoreElements()) { while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement(); JarEntry entry = entries.nextElement();
if(entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info")) continue; if (entry.isDirectory() || !entry.getName().endsWith(".class") || entry.getName().contains("module-info"))
continue;
String className = entry.getName().replace(".class", "").replace("/", "."); String className = entry.getName().replace(".class", "").replace("/", ".");
classLoader.loadClass(className); // Use the same class loader for ALL plugins. classLoader.loadClass(className); // Use the same class loader for ALL plugins.
} }
// Create a plugin instance.
Class<?> pluginClass = classLoader.loadClass(pluginConfig.mainClass); Class<?> pluginClass = classLoader.loadClass(pluginConfig.mainClass);
Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance(); Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
// Close the file reader.
fileReader.close();
// Check if the plugin has alternate dependencies.
if(pluginConfig.loadAfter != null && pluginConfig.loadAfter.length > 0) {
// Add the plugin to a "load later" list.
dependencies.add(new PluginData(
pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig),
loader, pluginConfig.loadAfter));
continue;
}
// Load the plugin.
this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader); this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader);
fileReader.close(); // Close the file reader.
} catch (ClassNotFoundException ignored) { } catch (ClassNotFoundException ignored) {
Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class."); Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class.");
} catch (FileNotFoundException ignored) { } catch (FileNotFoundException ignored) {
...@@ -97,29 +126,68 @@ public final class PluginManager { ...@@ -97,29 +126,68 @@ public final class PluginManager {
} catch (Exception exception) { } catch (Exception exception) {
Grasscutter.getLogger().error("Failed to load plugin: " + plugin.getName(), exception); Grasscutter.getLogger().error("Failed to load plugin: " + plugin.getName(), exception);
} }
}); }
// Load plugins with dependencies.
int depth = 0; final int maxDepth = 30;
while(!dependencies.isEmpty()) {
// Check if the depth is too high.
if(depth >= maxDepth) {
Grasscutter.getLogger().error("Failed to load plugins with dependencies.");
break;
}
try {
// Get the next plugin to load.
var pluginData = dependencies.get(0);
// Check if the plugin's dependencies are loaded.
if(!this.plugins.keySet().containsAll(List.of(pluginData.getDependencies()))) {
depth++; // Increase depth counter.
continue; // Continue to next plugin.
}
// Remove the plugin from the list of dependencies.
dependencies.remove(pluginData);
// Load the plugin.
this.loadPlugin(pluginData.getPlugin(), pluginData.getIdentifier(), pluginData.getClassLoader());
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to load a plugin.", exception); depth++;
}
}
} }
/** /**
* Load the specified plugin. * Load the specified plugin.
*
* @param plugin The plugin instance. * @param plugin The plugin instance.
*/ */
private void loadPlugin(Plugin plugin, PluginIdentifier identifier, URLClassLoader classLoader) { private void loadPlugin(Plugin plugin, PluginIdentifier identifier, URLClassLoader classLoader) {
Grasscutter.getLogger().info("Loading plugin: " + identifier.name); Grasscutter.getLogger().info("Loading plugin: " + identifier.name);
// Add the plugin's identifier. // Add the plugin's identifier.
try { try {
Class<Plugin> pluginClass = Plugin.class; Class<Plugin> pluginClass = Plugin.class;
Method method = pluginClass.getDeclaredMethod("initializePlugin", PluginIdentifier.class, URLClassLoader.class); Method method = pluginClass.getDeclaredMethod("initializePlugin", PluginIdentifier.class, URLClassLoader.class);
method.setAccessible(true); method.invoke(plugin, identifier, classLoader); method.setAccessible(false); method.setAccessible(true);
method.invoke(plugin, identifier, classLoader);
method.setAccessible(false);
} catch (Exception ignored) { } catch (Exception ignored) {
Grasscutter.getLogger().warn("Failed to add plugin identifier: " + identifier.name); Grasscutter.getLogger().warn("Failed to add plugin identifier: " + identifier.name);
} }
// Add the plugin to the list of loaded plugins. // Add the plugin to the list of loaded plugins.
this.plugins.put(identifier.name, plugin); this.plugins.put(identifier.name, plugin);
// Create a collection for the plugin's listeners.
this.listeners.put(plugin, new LinkedList<>());
// Call the plugin's onLoad method. // Call the plugin's onLoad method.
plugin.onLoad(); try {
plugin.onLoad();
} catch (Throwable exception) {
Grasscutter.getLogger().error("Failed to load plugin: " + identifier.name, exception);
}
} }
/** /**
...@@ -128,62 +196,121 @@ public final class PluginManager { ...@@ -128,62 +196,121 @@ public final class PluginManager {
public void enablePlugins() { public void enablePlugins() {
this.plugins.forEach((name, plugin) -> { this.plugins.forEach((name, plugin) -> {
Grasscutter.getLogger().info("Enabling plugin: " + name); Grasscutter.getLogger().info("Enabling plugin: " + name);
plugin.onEnable(); try {
plugin.onEnable();
} catch (Throwable exception) {
Grasscutter.getLogger().error("Failed to enable plugin: " + name, exception);
}
}); });
} }
/** /**
* Disables all registered plugins. * Disables all registered plugins.
*/ */
public void disablePlugins() { public void disablePlugins() {
this.plugins.forEach((name, plugin) -> { this.plugins.forEach((name, plugin) -> {
Grasscutter.getLogger().info("Disabling plugin: " + name); Grasscutter.getLogger().info("Disabling plugin: " + name);
plugin.onDisable(); try {
plugin.onDisable();
} catch (Throwable exception) {
Grasscutter.getLogger().error("Failed to disable plugin: " + name, exception);
}
}); });
} }
/** /**
* Registers a plugin's event listener. * Registers a plugin's event listener.
*
* @param plugin The plugin registering the listener.
* @param listener The event listener. * @param listener The event listener.
*/ */
public void registerListener(EventHandler<? extends Event> listener) { public void registerListener(Plugin plugin, EventHandler<? extends Event> listener) {
this.listeners.add(listener); this.listeners.get(plugin).add(listener);
} }
/** /**
* Invoke the provided event on all registered event listeners. * Invoke the provided event on all registered event listeners.
*
* @param event The event to invoke. * @param event The event to invoke.
*/ */
public void invokeEvent(Event event) { public void invokeEvent(Event event) {
EnumSet.allOf(HandlerPriority.class) EnumSet.allOf(HandlerPriority.class)
.forEach(priority -> this.checkAndFilter(event, priority)); .forEach(priority -> this.checkAndFilter(event, priority));
} }
/** /**
* Check an event to handlers for the priority. * Check an event to handlers for the priority.
* @param event The event being called. *
* @param event The event being called.
* @param priority The priority to call for. * @param priority The priority to call for.
*/ */
private void checkAndFilter(Event event, HandlerPriority priority) { private void checkAndFilter(Event event, HandlerPriority priority) {
this.listeners.stream() // Create a collection of listeners.
.filter(handler -> handler.handles().isInstance(event)) List<EventHandler<? extends Event>> listeners = new LinkedList<>();
.filter(handler -> handler.getPriority() == priority)
.toList().forEach(handler -> this.invokeHandler(event, handler)); // Add all listeners from every plugin.
this.listeners.values().forEach(listeners::addAll);
listeners.stream()
// Filter the listeners by priority.
.filter(handler -> handler.handles().isInstance(event))
.filter(handler -> handler.getPriority() == priority)
// Invoke the event.
.toList().forEach(handler -> this.invokeHandler(event, handler));
} }
/**
* Gets a plugin's instance by its name.
*
* @param name The name of the plugin.
* @return Either null, or the plugin's instance.
*/
@Nullable
public Plugin getPlugin(String name) { public Plugin getPlugin(String name) {
return this.plugins.get(name); return this.plugins.get(name);
} }
/**
* Enables a plugin.
*
* @param plugin The plugin to enable.
*/
public void enablePlugin(Plugin plugin) {
try {
// Call the plugin's onEnable method.
plugin.onEnable();
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to enable plugin: " + plugin.getName(), exception);
}
}
/**
* Disables a plugin.
*
* @param plugin The plugin to disable.
*/
public void disablePlugin(Plugin plugin) {
try {
// Call the plugin's onDisable method.
plugin.onDisable();
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to disable plugin: " + plugin.getName(), exception);
}
// Un-register all listeners.
this.listeners.remove(plugin);
}
/** /**
* Performs logic checks then invokes the provided event handler. * Performs logic checks then invokes the provided event handler.
* @param event The event passed through to the handler. *
* @param event The event passed through to the handler.
* @param handler The handler to invoke. * @param handler The handler to invoke.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T extends Event> void invokeHandler(Event event, EventHandler<T> handler) { private <T extends Event> void invokeHandler(Event event, EventHandler<T> handler) {
if(!event.isCanceled() || if (!event.isCanceled() ||
(event.isCanceled() && handler.ignoresCanceled()) (event.isCanceled() && handler.ignoresCanceled())
) handler.getCallback().consume((T) event); ) handler.getCallback().consume((T) event);
} }
} }
...@@ -6,10 +6,7 @@ import emu.grasscutter.Grasscutter; ...@@ -6,10 +6,7 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.MonsterData; import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.data.excels.WorldLevelData; import emu.grasscutter.data.excels.WorldLevelData;
import emu.grasscutter.game.entity.EntityGadget; import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.game.entity.EntityNPC;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.proto.VisionTypeOuterClass; import emu.grasscutter.net.proto.VisionTypeOuterClass;
import emu.grasscutter.scripts.constants.EventType; import emu.grasscutter.scripts.constants.EventType;
...@@ -17,17 +14,12 @@ import emu.grasscutter.scripts.data.*; ...@@ -17,17 +14,12 @@ import emu.grasscutter.scripts.data.*;
import emu.grasscutter.scripts.service.ScriptMonsterSpawnService; import emu.grasscutter.scripts.service.ScriptMonsterSpawnService;
import emu.grasscutter.scripts.service.ScriptMonsterTideService; import emu.grasscutter.scripts.service.ScriptMonsterTideService;
import io.netty.util.concurrent.FastThreadLocalThread; import io.netty.util.concurrent.FastThreadLocalThread;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.luaj.vm2.LuaError; import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaValue; import org.luaj.vm2.LuaValue;
import org.luaj.vm2.lib.jse.CoerceJavaToLua; import org.luaj.vm2.lib.jse.CoerceJavaToLua;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutorService; import java.util.concurrent.*;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class SceneScriptManager { public class SceneScriptManager {
...@@ -38,15 +30,15 @@ public class SceneScriptManager { ...@@ -38,15 +30,15 @@ public class SceneScriptManager {
/** /**
* current triggers controlled by RefreshGroup * current triggers controlled by RefreshGroup
*/ */
private final Int2ObjectOpenHashMap<Set<SceneTrigger>> currentTriggers; private final Map<Integer, Set<SceneTrigger>> currentTriggers;
private final Int2ObjectOpenHashMap<SceneRegion> regions; private final Map<Integer, EntityRegion> regions; // <EntityId-Region>
private Map<Integer,SceneGroup> sceneGroups; private final Map<Integer,SceneGroup> sceneGroups;
private ScriptMonsterTideService scriptMonsterTideService; private ScriptMonsterTideService scriptMonsterTideService;
private ScriptMonsterSpawnService scriptMonsterSpawnService; private ScriptMonsterSpawnService scriptMonsterSpawnService;
/** /**
* blockid - loaded groupSet * blockid - loaded groupSet
*/ */
private Int2ObjectMap<Set<SceneGroup>> loadedGroupSetPerBlock; private final Map<Integer, Set<SceneGroup>> loadedGroupSetPerBlock;
public static final ExecutorService eventExecutor; public static final ExecutorService eventExecutor;
static { static {
eventExecutor = new ThreadPoolExecutor(4, 4, eventExecutor = new ThreadPoolExecutor(4, 4,
...@@ -55,23 +47,23 @@ public class SceneScriptManager { ...@@ -55,23 +47,23 @@ public class SceneScriptManager {
} }
public SceneScriptManager(Scene scene) { public SceneScriptManager(Scene scene) {
this.scene = scene; this.scene = scene;
this.currentTriggers = new Int2ObjectOpenHashMap<>(); this.currentTriggers = new ConcurrentHashMap<>();
this.regions = new Int2ObjectOpenHashMap<>(); this.regions = new ConcurrentHashMap<>();
this.variables = new HashMap<>(); this.variables = new ConcurrentHashMap<>();
this.sceneGroups = new HashMap<>(); this.sceneGroups = new ConcurrentHashMap<>();
this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this); this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this);
this.loadedGroupSetPerBlock = new Int2ObjectOpenHashMap<>(); this.loadedGroupSetPerBlock = new ConcurrentHashMap<>();
// TEMPORARY // TEMPORARY
if (this.getScene().getId() < 10 && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) { if (this.getScene().getId() < 10 && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) {
return; return;
} }
// Create // Create
this.init(); this.init();
} }
public Scene getScene() { public Scene getScene() {
return scene; return scene;
} }
...@@ -123,19 +115,25 @@ public class SceneScriptManager { ...@@ -123,19 +115,25 @@ public class SceneScriptManager {
spawnMonstersInGroup(group, suite); spawnMonstersInGroup(group, suite);
spawnGadgetsInGroup(group, suite); spawnGadgetsInGroup(group, suite);
} }
public SceneRegion getRegionById(int id) { public EntityRegion getRegionById(int id) {
return regions.get(id); return regions.get(id);
} }
public void registerRegion(SceneRegion region) { public void registerRegion(EntityRegion region) {
regions.put(region.config_id, region); regions.put(region.getId(), region);
} }
public void registerRegionInGroupSuite(SceneGroup group, SceneSuite suite){
public void deregisterRegion(SceneRegion region) { suite.sceneRegions.stream().map(region -> new EntityRegion(this.getScene(), region))
regions.remove(region.config_id); .forEach(this::registerRegion);
}
public synchronized void deregisterRegion(SceneRegion region) {
var instance = regions.values().stream()
.filter(r -> r.getConfigId() == region.config_id)
.findFirst();
instance.ifPresent(entityRegion -> regions.remove(entityRegion.getId()));
} }
public Int2ObjectMap<Set<SceneGroup>> getLoadedGroupSetPerBlock() { public Map<Integer, Set<SceneGroup>> getLoadedGroupSetPerBlock() {
return loadedGroupSetPerBlock; return loadedGroupSetPerBlock;
} }
...@@ -182,53 +180,61 @@ public class SceneScriptManager { ...@@ -182,53 +180,61 @@ public class SceneScriptManager {
} }
this.sceneGroups.put(group.id, group); this.sceneGroups.put(group.id, group);
if(group.regions != null){
group.regions.forEach(this::registerRegion);
}
} }
public void checkRegions() { public void checkRegions() {
if (this.regions.size() == 0) { if (this.regions.size() == 0) {
return; return;
} }
for (SceneRegion region : this.regions.values()) { for (var region : this.regions.values()) {
getScene().getEntities().values() getScene().getEntities().values()
.stream() .stream()
.filter(e -> e.getEntityType() <= 2 && region.contains(e.getPosition())) .filter(e -> e.getEntityType() <= 2 && region.getMetaRegion().contains(e.getPosition()))
.forEach(region::addEntity); .forEach(region::addEntity);
if (region.hasNewEntities()) { if (region.hasNewEntities()) {
// This is not how it works, source_eid should be region entity id, but we dont have an entity for regions yet callEvent(EventType.EVENT_ENTER_REGION, new ScriptArgs(region.getConfigId()).setSourceEntityId(region.getId()));
callEvent(EventType.EVENT_ENTER_REGION, new ScriptArgs(region.config_id).setSourceEntityId(region.config_id));
region.resetNewEntities(); region.resetNewEntities();
} }
} }
} }
public List<EntityGadget> getGadgetsInGroupSuite(SceneGroup group, SceneSuite suite){
return suite.sceneGadgets.stream()
.map(g -> createGadget(group.id, group.block_id, g))
.filter(Objects::nonNull)
.toList();
}
public List<EntityMonster> getMonstersInGroupSuite(SceneGroup group, SceneSuite suite){
return suite.sceneMonsters.stream()
.map(mob -> createMonster(group.id, group.block_id, mob))
.filter(Objects::nonNull)
.toList();
}
public void addGroupSuite(SceneGroup group, SceneSuite suite){ public void addGroupSuite(SceneGroup group, SceneSuite suite){
spawnMonstersInGroup(group, suite); // we added trigger first
spawnGadgetsInGroup(group, suite); registerTrigger(suite.sceneTriggers);
registerTrigger(suite.sceneTriggers);
var toCreate = new ArrayList<GameEntity>();
toCreate.addAll(getGadgetsInGroupSuite(group, suite));
toCreate.addAll(getMonstersInGroupSuite(group, suite));
addEntities(toCreate);
registerRegionInGroupSuite(group, suite);
} }
public void removeGroupSuite(SceneGroup group, SceneSuite suite){ public void removeGroupSuite(SceneGroup group, SceneSuite suite){
deregisterTrigger(suite.sceneTriggers);
removeMonstersInGroup(group, suite); removeMonstersInGroup(group, suite);
removeGadgetsInGroup(group, suite); removeGadgetsInGroup(group, suite);
deregisterTrigger(suite.sceneTriggers);
} suite.sceneRegions.forEach(this::deregisterRegion);
public void spawnGadgetsInGroup(SceneGroup group, int suiteIndex) {
spawnGadgetsInGroup(group, group.getSuiteByIndex(suiteIndex));
}
public void spawnGadgetsInGroup(SceneGroup group) {
spawnGadgetsInGroup(group, null);
} }
public void spawnGadgetsInGroup(SceneGroup group, SceneSuite suite) { public void spawnGadgetsInGroup(SceneGroup group, SceneSuite suite) {
var gadgets = group.gadgets.values(); var gadgets = group.gadgets.values();
if (suite != null) { if (suite != null) {
gadgets = suite.sceneGadgets; gadgets = suite.sceneGadgets;
} }
...@@ -240,13 +246,6 @@ public class SceneScriptManager { ...@@ -240,13 +246,6 @@ public class SceneScriptManager {
this.addEntities(toCreate); this.addEntities(toCreate);
} }
public void spawnMonstersInGroup(SceneGroup group, int suiteIndex) {
var suite = group.getSuiteByIndex(suiteIndex);
if(suite == null){
return;
}
spawnMonstersInGroup(group, suite);
}
public void spawnMonstersInGroup(SceneGroup group, SceneSuite suite) { public void spawnMonstersInGroup(SceneGroup group, SceneSuite suite) {
if(suite == null || suite.sceneMonsters.size() <= 0){ if(suite == null || suite.sceneMonsters.size() <= 0){
return; return;
...@@ -254,11 +253,6 @@ public class SceneScriptManager { ...@@ -254,11 +253,6 @@ public class SceneScriptManager {
this.addEntities(suite.sceneMonsters.stream() this.addEntities(suite.sceneMonsters.stream()
.map(mob -> createMonster(group.id, group.block_id, mob)).toList()); .map(mob -> createMonster(group.id, group.block_id, mob)).toList());
} }
public void spawnMonstersInGroup(SceneGroup group) {
this.addEntities(group.monsters.values().stream()
.map(mob -> createMonster(group.id, group.block_id, mob)).toList());
}
public void startMonsterTideInGroup(SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) { public void startMonsterTideInGroup(SceneGroup group, Integer[] ordersConfigId, int tideCount, int sceneLimit) {
this.scriptMonsterTideService = this.scriptMonsterTideService =
...@@ -351,6 +345,16 @@ public class SceneScriptManager { ...@@ -351,6 +345,16 @@ public class SceneScriptManager {
} }
public EntityGadget createGadget(int groupId, int blockId, SceneGadget g) { public EntityGadget createGadget(int groupId, int blockId, SceneGadget g) {
if(g.isOneoff){
var hasEntity = getScene().getEntities().values().stream()
.filter(e -> e instanceof EntityGadget)
.filter(e -> e.getGroupId() == g.group.id)
.filter(e -> e.getConfigId() == g.config_id)
.findFirst();
if(hasEntity.isPresent()){
return null;
}
}
EntityGadget entity = new EntityGadget(getScene(), g.gadget_id, g.pos); EntityGadget entity = new EntityGadget(getScene(), g.gadget_id, g.pos);
if (entity.getGadgetData() == null){ if (entity.getGadgetData() == null){
......
...@@ -309,22 +309,22 @@ public class ScriptLib { ...@@ -309,22 +309,22 @@ public class ScriptLib {
return 0; return 0;
} }
public int GetRegionEntityCount(LuaTable table) { public int GetRegionEntityCount(LuaTable table) {
logger.debug("[LUA] Call GetRegionEntityCount with {}", logger.debug("[LUA] Call GetRegionEntityCount with {}",
printTable(table)); printTable(table));
int regionId = table.get("region_eid").toint(); int regionId = table.get("region_eid").toint();
int entityType = table.get("entity_type").toint(); int entityType = table.get("entity_type").toint();
SceneRegion region = this.getSceneScriptManager().getRegionById(regionId); var region = this.getSceneScriptManager().getRegionById(regionId);
if (region == null) { if (region == null) {
return 0; return 0;
} }
return (int) region.getEntities().intStream().filter(e -> e >> 24 == entityType).count(); return (int) region.getEntities().stream().filter(e -> e >> 24 == entityType).count();
} }
public void PrintContextLog(String msg) { public void PrintContextLog(String msg) {
logger.info("[LUA] " + msg); logger.info("[LUA] " + msg);
} }
......
...@@ -11,4 +11,9 @@ public class SceneGadget extends SceneObject{ ...@@ -11,4 +11,9 @@ public class SceneGadget extends SceneObject{
public int point_type; public int point_type;
public SceneBossChest boss_chest; public SceneBossChest boss_chest;
public int interact_id; public int interact_id;
public boolean isOneoff;
public void setIsOneoff(boolean isOneoff){
this.isOneoff = isOneoff;
}
} }
...@@ -34,10 +34,10 @@ public class SceneGroup { ...@@ -34,10 +34,10 @@ public class SceneGroup {
public Map<Integer, SceneGadget> gadgets; // <ConfigId, Gadgets> public Map<Integer, SceneGadget> gadgets; // <ConfigId, Gadgets>
public Map<String, SceneTrigger> triggers; public Map<String, SceneTrigger> triggers;
public Map<Integer, SceneNPC> npc; // <NpcId, NPC> public Map<Integer, SceneNPC> npc; // <NpcId, NPC>
public List<SceneRegion> regions; public Map<Integer, SceneRegion> regions;
public List<SceneSuite> suites; public List<SceneSuite> suites;
public List<SceneVar> variables; public List<SceneVar> variables;
public SceneBusiness business; public SceneBusiness business;
public SceneGarbage garbages; public SceneGarbage garbages;
public SceneInitConfig init_config; public SceneInitConfig init_config;
...@@ -115,9 +115,12 @@ public class SceneGroup { ...@@ -115,9 +115,12 @@ public class SceneGroup {
triggers.values().forEach(t -> t.currentGroup = this); triggers.values().forEach(t -> t.currentGroup = this);
suites = ScriptLoader.getSerializer().toList(SceneSuite.class, bindings.get("suites")); suites = ScriptLoader.getSerializer().toList(SceneSuite.class, bindings.get("suites"));
regions = ScriptLoader.getSerializer().toList(SceneRegion.class, bindings.get("regions")); regions = ScriptLoader.getSerializer().toList(SceneRegion.class, bindings.get("regions")).stream()
.collect(Collectors.toMap(x -> x.config_id, y -> y));
regions.values().forEach(m -> m.group = this);
init_config = ScriptLoader.getSerializer().toObject(SceneInitConfig.class, bindings.get("init_config")); init_config = ScriptLoader.getSerializer().toObject(SceneInitConfig.class, bindings.get("init_config"));
// Garbages TODO fix properly later // Garbages TODO fix properly later
Object garbagesValue = bindings.get("garbages"); Object garbagesValue = bindings.get("garbages");
if (garbagesValue != null && garbagesValue instanceof LuaValue garbagesTable) { if (garbagesValue != null && garbagesValue instanceof LuaValue garbagesTable) {
...@@ -157,12 +160,19 @@ public class SceneGroup { ...@@ -157,12 +160,19 @@ public class SceneGroup {
.map(triggers::get) .map(triggers::get)
.toList() .toList()
); );
suite.sceneRegions = new ArrayList<>(
suite.regions.stream()
.filter(regions::containsKey)
.map(regions::get)
.toList()
);
} }
} catch (ScriptException e) { } catch (ScriptException e) {
Grasscutter.getLogger().error("Error loading group " + id + " in scene " + sceneId, e); Grasscutter.getLogger().error("Error loading group " + id + " in scene " + sceneId, e);
} }
Grasscutter.getLogger().info("group {} in scene {} is loaded successfully.", id, sceneId); Grasscutter.getLogger().info("group {} in scene {} is loaded successfully.", id, sceneId);
return this; return this;
} }
......
package emu.grasscutter.scripts.data; package emu.grasscutter.scripts.data;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.scripts.constants.ScriptRegionShape; import emu.grasscutter.scripts.constants.ScriptRegionShape;
import emu.grasscutter.utils.Position; import emu.grasscutter.utils.Position;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import lombok.Data;
import lombok.Setter; import lombok.Setter;
import lombok.ToString;
@ToString
@Setter @Setter
public class SceneRegion { public class SceneRegion {
public int config_id; public int config_id;
public int shape; public int shape;
public Position pos; public Position pos;
// for CUBIC
public Position size; public Position size;
// for SPHERE
private boolean hasNewEntities; public int radius;
private final IntSet entities; // Ids of entities inside this region
public SceneRegion() {
this.entities = new IntOpenHashSet();
}
public IntSet getEntities() {
return entities;
}
public void addEntity(GameEntity entity) { public transient SceneGroup group;
if (this.getEntities().contains(entity.getId())) { public boolean contains(Position position) {
return;
}
this.getEntities().add(entity.getId());
this.hasNewEntities = true;
}
public void removeEntity(GameEntity entity) {
this.getEntities().remove(entity.getId());
}
public boolean contains(Position p) {
switch (shape) { switch (shape) {
case ScriptRegionShape.CUBIC: case ScriptRegionShape.CUBIC:
return (Math.abs(pos.getX() - p.getX()) <= size.getX()) && return (Math.abs(pos.getX() - position.getX()) <= size.getX()) &&
(Math.abs(pos.getZ() - p.getZ()) <= size.getZ()); (Math.abs(pos.getY() - position.getY()) <= size.getY()) &&
(Math.abs(pos.getZ() - position.getZ()) <= size.getZ());
case ScriptRegionShape.SPHERE: case ScriptRegionShape.SPHERE:
return false; var x = Math.pow(pos.getX() - position.getX(), 2);
var y = Math.pow(pos.getY() - position.getY(), 2);
var z = Math.pow(pos.getZ() - position.getZ(), 2);
return x + y + z <= (radius ^ 2);
} }
return false; return false;
} }
public boolean hasNewEntities() {
return hasNewEntities;
}
public void resetNewEntities() {
hasNewEntities = false;
}
} }
...@@ -11,9 +11,12 @@ public class SceneSuite { ...@@ -11,9 +11,12 @@ public class SceneSuite {
public List<Integer> monsters; public List<Integer> monsters;
public List<Integer> gadgets; public List<Integer> gadgets;
public List<String> triggers; public List<String> triggers;
public int rand_weight; public List<Integer> regions;
public int rand_weight;
public transient List<SceneMonster> sceneMonsters; public transient List<SceneMonster> sceneMonsters;
public transient List<SceneGadget> sceneGadgets; public transient List<SceneGadget> sceneGadgets;
public transient List<SceneTrigger> sceneTriggers; public transient List<SceneTrigger> sceneTriggers;
public transient List<SceneRegion> sceneRegions;
} }
package emu.grasscutter.scripts.serializer; package emu.grasscutter.scripts.serializer;
import com.esotericsoftware.reflectasm.ConstructorAccess; import com.esotericsoftware.reflectasm.ConstructorAccess;
import com.esotericsoftware.reflectasm.MethodAccess; import com.esotericsoftware.reflectasm.MethodAccess;
import emu.grasscutter.Grasscutter; import emu.grasscutter.Grasscutter;
import emu.grasscutter.scripts.ScriptUtils; import emu.grasscutter.scripts.ScriptUtils;
import lombok.AccessLevel; import lombok.AccessLevel;
...@@ -12,8 +11,6 @@ import lombok.experimental.FieldDefaults; ...@@ -12,8 +11,6 @@ import lombok.experimental.FieldDefaults;
import org.luaj.vm2.LuaTable; import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue; import org.luaj.vm2.LuaValue;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
...@@ -56,10 +53,12 @@ public class LuaSerializer implements Serializer { ...@@ -56,10 +53,12 @@ public class LuaSerializer implements Serializer {
object = (T) (Float) keyValue.tofloat(); // terrible... object = (T) (Float) keyValue.tofloat(); // terrible...
} else if (keyValue.isstring()) { } else if (keyValue.isstring()) {
object = (T) keyValue.tojstring(); object = (T) keyValue.tojstring();
} else { } else if (keyValue.isboolean()) {
object = (T) (Boolean) keyValue.toboolean();
} else {
object = (T) keyValue; object = (T) keyValue;
} }
if (object != null) { if (object != null) {
list.add(object); list.add(object);
} }
...@@ -118,7 +117,9 @@ public class LuaSerializer implements Serializer { ...@@ -118,7 +117,9 @@ public class LuaSerializer implements Serializer {
methodAccess.invoke(object, fieldMeta.index, keyValue.toint()); methodAccess.invoke(object, fieldMeta.index, keyValue.toint());
} else if (fieldMeta.getType().equals(String.class)) { } else if (fieldMeta.getType().equals(String.class)) {
methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring()); methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring());
} else { } else if (fieldMeta.getType().equals(boolean.class)) {
methodAccess.invoke(object, fieldMeta.index, keyValue.toboolean());
} else {
methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring()); methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring());
} }
} catch (Exception ex) { } catch (Exception ex) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment