Merge branch 'development' into Weather

This commit is contained in:
Luke H-W 2022-06-24 00:47:14 +09:30 committed by GitHub
commit f04035da34
23 changed files with 1196 additions and 665 deletions

View File

@ -43,7 +43,7 @@ sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
group = 'xyz.grasscutters'
version = '1.2.1-dev'
version = '1.2.2-dev'
sourceCompatibility = 17
@ -171,7 +171,16 @@ publishing {
}
repositories {
maven {
// change URLs to point to your repos, e.g. http://my.org/repo
if(version.endsWith('-dev')) {
println ("Publishing to 4benj-maven")
url 'https://repo.4benj.com/releases'
name '4benj-maven'
credentials {
username System.getenv('benj_maven_username')
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
@ -181,6 +190,7 @@ publishing {
}
}
}
}
clean {
delete protobuf.generatedFilesBaseDir
@ -225,8 +235,10 @@ eclipse {
}
signing {
if(!version.endsWith('-dev')) {
sign publishing.publications.mavenJava
}
}
javadoc {
options.encoding = 'UTF-8'

View File

@ -9,7 +9,11 @@
"pattern": "^[A-Za-z\\d_.-]+$"
}
},
"required": [ "name", "description", "mainClass" ],
"required": [
"name",
"description",
"mainClass"
],
"properties": {
"name": {
"description": "The unique name of plugin.",
@ -22,7 +26,10 @@
},
"version": {
"description": "A plugin revision identifier.",
"type": [ "string", "number" ]
"type": [
"string",
"number"
]
},
"description": {
"description": "Human readable plugin summary.",
@ -44,6 +51,13 @@
"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"
}
}
}
}

View File

@ -1,25 +1,34 @@
package emu.grasscutter;
import java.io.*;
import java.util.Calendar;
import ch.qos.logback.classic.Logger;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import emu.grasscutter.auth.AuthenticationSystem;
import emu.grasscutter.auth.DefaultAuthentication;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.command.DefaultPermissionHandler;
import emu.grasscutter.command.PermissionHandler;
import emu.grasscutter.game.dungeons.challenge.DungeonChallenge;
import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.database.DatabaseManager;
import emu.grasscutter.game.managers.energy.EnergyManager;
import emu.grasscutter.game.managers.stamina.StaminaManager;
import emu.grasscutter.plugin.PluginManager;
import emu.grasscutter.plugin.api.ServerHook;
import emu.grasscutter.scripts.ScriptLoader;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.http.HttpServer;
import emu.grasscutter.server.http.dispatch.DispatchHandler;
import emu.grasscutter.server.http.handlers.*;
import emu.grasscutter.server.http.dispatch.RegionHandler;
import emu.grasscutter.server.http.documentation.DocumentationServerHandler;
import emu.grasscutter.server.http.handlers.AnnouncementsHandler;
import emu.grasscutter.server.http.handlers.GachaHandler;
import emu.grasscutter.server.http.handlers.GenericHandler;
import emu.grasscutter.server.http.handlers.LogHandler;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.ConfigContainer;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.Language;
import emu.grasscutter.utils.Utils;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
@ -30,21 +39,13 @@ import org.jline.terminal.TerminalBuilder;
import org.reflections.Reflections;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import ch.qos.logback.classic.Logger;
import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.database.DatabaseManager;
import emu.grasscutter.utils.Language;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.Crypto;
import javax.annotation.Nullable;
import java.io.*;
import java.util.Calendar;
import static emu.grasscutter.Configuration.DATA;
import static emu.grasscutter.Configuration.SERVER;
import static emu.grasscutter.utils.Language.translate;
import static emu.grasscutter.Configuration.*;
public final class Grasscutter {
private static final Logger log = (Logger) LoggerFactory.getLogger(Grasscutter.class);
@ -90,13 +91,16 @@ public final class Grasscutter {
for (String arg : args) {
switch (arg.toLowerCase()) {
case "-handbook" -> {
Tools.createGmHandbook(); exitEarly = true;
Tools.createGmHandbook();
exitEarly = true;
}
case "-gachamap" -> {
Tools.createGachaMapping(DATA("gacha_mappings.js")); exitEarly = true;
Tools.createGachaMapping(DATA("gacha_mappings.js"));
exitEarly = true;
}
case "-version" -> {
System.out.println("Grasscutter version: " + BuildConfig.VERSION + "-" + BuildConfig.GIT_HASH); exitEarly = true;
System.out.println("Grasscutter version: " + BuildConfig.VERSION + "-" + BuildConfig.GIT_HASH);
exitEarly = true;
}
}
}
@ -113,8 +117,6 @@ public final class Grasscutter {
Grasscutter.updateDayOfWeek();
ResourceLoader.loadAll();
ScriptLoader.init();
EnergyManager.initialize();
DungeonChallenge.initialize();
// Initialize database.
DatabaseManager.initialize();
@ -142,9 +144,6 @@ public final class Grasscutter {
httpServer.addRouter(GachaHandler.class);
httpServer.addRouter(DocumentationServerHandler.class);
// TODO: find a better place?
StaminaManager.initialize();
// Start servers.
var runMode = SERVER.runMode;
if (runMode == ServerRunMode.HYBRID) {
@ -176,6 +175,7 @@ public final class Grasscutter {
*/
private static void onShutdown() {
// Disable all plugins.
if(pluginManager != null)
pluginManager.disablePlugins();
}
@ -215,6 +215,7 @@ public final class Grasscutter {
/**
* Saves the provided server configuration.
*
* @param config The configuration to save, or null for a new one.
*/
public static void saveConfig(@Nullable ConfigContainer config) {
@ -350,6 +351,7 @@ public final class Grasscutter {
/**
* Sets the authentication system for the server.
*
* @param authenticationSystem The authentication system to use.
*/
public static void setAuthenticationSystem(AuthenticationSystem authenticationSystem) {
@ -358,6 +360,7 @@ public final class Grasscutter {
/**
* Sets the permission handler for the server.
*
* @param permissionHandler The permission handler to use.
*/
public static void setPermissionHandler(PermissionHandler permissionHandler) {

View File

@ -17,8 +17,8 @@ public interface ExternalAuthenticator {
* Called when an external account creation request is made.
* @param request The authentication request.
*
* For developers: Use {@link AuthenticationRequest#getRequest()} to get the request body.
* Use {@link AuthenticationRequest#getResponse()} to get the response body.
* For developers: Use AuthenticationRequest#getRequest() to get the request body.
* Use AuthenticationRequest#getResponse() to get the response body.
*/
void handleAccountCreation(AuthenticationRequest request);
@ -26,8 +26,8 @@ public interface ExternalAuthenticator {
* Called when an external password reset request is made.
* @param request The authentication request.
*
* For developers: Use {@link AuthenticationRequest#getRequest()} to get the request body.
* Use {@link AuthenticationRequest#getResponse()} to get the response body.
* For developers: Use AuthenticationRequest#getRequest() to get the request body.
* Use AuthenticationRequest#getResponse() to get the response body.
*/
void handlePasswordReset(AuthenticationRequest request);
}

View File

@ -1,9 +1,7 @@
package emu.grasscutter.command;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player;
import org.reflections.Reflections;
import java.util.*;
@ -11,6 +9,7 @@ import java.util.*;
@SuppressWarnings({"UnusedReturnValue", "unused"})
public final class CommandMap {
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, Integer> targetPlayerIds = new HashMap<>();
private static final String consoleId = "console";
@ -45,7 +44,7 @@ public final class CommandMap {
// Register aliases.
if (annotation.aliases().length > 0) {
for (String alias : annotation.aliases()) {
this.commands.put(alias, command);
this.aliases.put(alias, command);
this.annotations.put(alias, annotation);
}
}
@ -60,6 +59,7 @@ public final class CommandMap {
*/
public CommandMap unregisterCommand(String label) {
Grasscutter.getLogger().debug("Unregistered command: " + label);
CommandHandler handler = this.commands.get(label);
if (handler == null) return this;
@ -70,7 +70,7 @@ public final class CommandMap {
// Unregister aliases.
if (annotation.aliases().length > 0) {
for (String alias : annotation.aliases()) {
this.commands.remove(alias);
this.aliases.remove(alias);
this.annotations.remove(alias);
}
}
@ -78,7 +78,9 @@ public final class CommandMap {
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() {
return new LinkedHashMap<>(this.annotations);
@ -142,7 +144,7 @@ public final class CommandMap {
}
if (targetUidStr != null) {
if (targetUidStr.equals("")) { // Clears the default targetPlayer.
targetPlayerIds.remove(playerId);
this.targetPlayerIds.remove(playerId);
CommandHandler.sendTranslatedMessage(player, "commands.execution.clear_target");
} else { // Sets default targetPlayer to the UID provided.
try {
@ -151,7 +153,7 @@ public final class CommandMap {
if (targetPlayer == null) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
} else {
targetPlayerIds.put(playerId, uid);
this.targetPlayerIds.put(playerId, uid);
CommandHandler.sendTranslatedMessage(player, "commands.execution.set_target", targetUidStr);
CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline() ? "commands.execution.set_target_online" : "commands.execution.set_target_offline", targetUidStr);
}
@ -164,11 +166,19 @@ public final class CommandMap {
// Get command handler.
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) {
CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label);
return;
}
// Get the command's annotation.
Command annotation = this.annotations.get(label);
// If any @UID argument is present, override targetPlayer with it.
for (int i = 0; i < args.size(); i++) {
String arg = args.get(i);
@ -191,8 +201,8 @@ public final class CommandMap {
// If there's still no targetPlayer at this point, use previously-set target
if (targetPlayer == null) {
if (targetPlayerIds.containsKey(playerId)) {
targetPlayer = Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.get(playerId), true); // We check every time in case the target is deleted after being targeted
if (this.targetPlayerIds.containsKey(playerId)) {
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) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
return;
@ -204,32 +214,36 @@ public final class CommandMap {
}
// 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;
}
// 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 (targetPlayer == null) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target");
CommandHandler.sendTranslatedMessage(null, "commands.execution.need_target");
return;
}
if ((targetRequirement == Command.TargetRequirement.ONLINE) && !targetPlayer.isOnline()) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_online");
return;
}
if ((targetRequirement == Command.TargetRequirement.OFFLINE) && targetPlayer.isOnline()) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_offline");
return;
}
}
// Invoke execute method for handler.
boolean threading = this.annotations.get(label).threading();
// Copy player and handler to final properties.
final Player targetPlayerF = targetPlayer; // Is there a better way to do this?
Runnable runnable = () -> handler.execute(player, targetPlayerF, args);
if(threading) {
final CommandHandler handlerF = handler; // Is there a better way to do this?
// Invoke execute method for handler.
Runnable runnable = () -> handlerF.execute(player, targetPlayerF, args);
if (annotation.threading()) {
new Thread(runnable).start();
} else {
runnable.run();
@ -242,10 +256,11 @@ public final class CommandMap {
private void scan() {
Reflections reflector = Grasscutter.reflector;
Set<Class<?>> classes = reflector.getTypesAnnotatedWith(Command.class);
classes.forEach(annotated -> {
try {
Command cmdData = annotated.getAnnotation(Command.class);
Object object = annotated.newInstance();
Object object = annotated.getDeclaredConstructor().newInstance();
if (object instanceof CommandHandler)
this.registerCommand(cmdData.label(), (CommandHandler) object);
else Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a CommandHandler!");

View File

@ -6,11 +6,11 @@ import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import java.io.*;
import java.nio.file.FileSystems;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.List;
import java.util.regex.Pattern;
import static emu.grasscutter.Configuration.DATA;
@ -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
* @see #load(String, boolean)
*
* @param resourcePath The path to the data file to be loaded.
* @return InputStream of the data file.
* @throws FileNotFoundException
* @see #load(String, boolean)
*/
public static InputStream load(String resourcePath) throws FileNotFoundException {
return load(resourcePath, true);
@ -29,6 +30,7 @@ public class DataLoader {
/**
* Load a data file by its name.
*
* @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?
* @return InputStream of the data file.
@ -53,9 +55,7 @@ public class DataLoader {
if (filenames == null) {
Grasscutter.getLogger().error("We were unable to locate your default data files.");
}
for (Path file : filenames) {
} else for (Path file : filenames) {
String relativePath = String.valueOf(file).split("defaults[\\\\\\/]data[\\\\\\/]")[1];
CheckAndCopyData(relativePath);

View File

@ -154,10 +154,15 @@ public final class DatabaseHelper {
return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", id)).first();
}
@Deprecated
public static Player getPlayerByAccount(Account account) {
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) {
return DatabaseManager.getGameDatastore().find(Player.class).filter(Filters.eq("_id", uid)).count() > 0;
}

View File

@ -0,0 +1,74 @@
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;
}
}

View File

@ -1017,7 +1017,7 @@ public class Player {
}
public void interactWith(int gadgetEntityId, GadgetInteractReq req) {
public void interactWith(int gadgetEntityId, GadgetInteractReq opType) {
GameEntity entity = getScene().getEntityById(gadgetEntityId);
if (entity == null) {
return;
@ -1050,7 +1050,7 @@ public class Player {
return;
}
boolean shouldDelete = gadget.getContent().onInteract(this, req);
boolean shouldDelete = gadget.getContent().onInteract(this, opType);
if (shouldDelete) {
entity.getScene().removeEntity(entity);

View File

@ -5,6 +5,7 @@ public enum EntityIdType {
MONSTER (0x02),
NPC (0x03),
GADGET (0x04),
REGION (0x05),
WEAPON (0x06),
TEAM (0x09),
MPLEVEL (0x0b);

View File

@ -30,14 +30,15 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.danilopianini.util.SpatialIndex;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
public class Scene {
private final World world;
private final SceneData sceneData;
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> deadSpawnedEntities;
private final Set<SceneBlock> loadedBlocks;
@ -55,15 +56,15 @@ public class Scene {
public Scene(World world, SceneData sceneData) {
this.world = world;
this.sceneData = sceneData;
this.players = Collections.synchronizedList(new ArrayList<>());
this.entities = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>());
this.players = new CopyOnWriteArrayList<>();
this.entities = new ConcurrentHashMap<>();
this.time = 8 * 60;
this.prevScene = 3;
this.spawnedEntities = new HashSet<>();
this.deadSpawnedEntities = new HashSet<>();
this.loadedBlocks = new HashSet<>();
this.spawnedEntities = ConcurrentHashMap.newKeySet();
this.deadSpawnedEntities = ConcurrentHashMap.newKeySet();
this.loadedBlocks = ConcurrentHashMap.newKeySet();
this.scriptManager = new SceneScriptManager(this);
}
@ -91,7 +92,7 @@ public class Scene {
return this.getPlayers().size();
}
public Int2ObjectMap<GameEntity> getEntities() {
public Map<Integer, GameEntity> getEntities() {
return entities;
}
@ -610,15 +611,10 @@ public class Scene {
var suiteData = group.getSuiteByIndex(suite);
suiteData.sceneTriggers.forEach(getScriptManager()::registerTrigger);
entities.addAll(suiteData.sceneGadgets.stream()
.map(g -> scriptManager.createGadget(group.id, group.block_id, g))
.filter(Objects::nonNull)
.toList());
entities.addAll(suiteData.sceneMonsters.stream()
.map(mob -> scriptManager.createMonster(group.id, group.block_id, mob))
.filter(Objects::nonNull)
.toList());
entities.addAll(scriptManager.getGadgetsInGroupSuite(group, suiteData));
entities.addAll(scriptManager.getMonstersInGroupSuite(group, suiteData));
scriptManager.registerRegionInGroupSuite(group, suiteData);
}
scriptManager.meetEntities(entities);
@ -641,13 +637,12 @@ public class Scene {
group.triggers.values().forEach(getScriptManager()::deregisterTrigger);
}
if(group.regions != null){
group.regions.forEach(getScriptManager()::deregisterRegion);
group.regions.values().forEach(getScriptManager()::deregisterRegion);
}
}
scriptManager.getLoadedGroupSetPerBlock().remove(block.id);
Grasscutter.getLogger().info("Scene {} Block {} is unloaded.", this.getId(), block.id);
}
// Gadgets
public void onPlayerCreateGadget(EntityClientGadget gadget) {

View File

@ -7,11 +7,13 @@ public final class PluginConfig {
public String name, description, version;
public String mainClass;
public String[] authors;
public String[] loadAfter;
/**
* Attempts to validate this config instance.
* @return True if the config is valid, false otherwise.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean validate() {
return name != null && description != null && mainClass != null;
}

View File

@ -1,35 +1,41 @@
package emu.grasscutter.plugin;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.server.event.Event;
import emu.grasscutter.server.event.EventHandler;
import emu.grasscutter.server.event.HandlerPriority;
import emu.grasscutter.server.event.*;
import emu.grasscutter.utils.Utils;
import lombok.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import javax.annotation.Nullable;
import java.io.*;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.*;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.*;
import static emu.grasscutter.Configuration.*;
import static emu.grasscutter.Configuration.PLUGIN;
/**
* Manages the server's plugins and the event system.
*/
public final class PluginManager {
private final Map<String, Plugin> plugins = new HashMap<>();
private final List<EventHandler<? extends Event>> listeners = new LinkedList<>();
/* All loaded plugins. */
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() {
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.
*/
@ -59,36 +65,59 @@ public final class PluginManager {
}
});
// Create a class loader for the plugins.
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 {
URL url = plugin.toURI().toURL();
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());
// Create a plugin config instance from the config file.
PluginConfig pluginConfig = Grasscutter.getGsonFactory().fromJson(fileReader, PluginConfig.class);
// Check if the plugin config is valid.
if (!pluginConfig.validate()) {
Utils.logObject(pluginConfig);
Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid config file.");
return;
}
// Create a JAR file instance from the plugin's URL.
JarFile jarFile = new JarFile(plugin);
// Load all class files from the JAR file.
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
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("/", ".");
classLoader.loadClass(className); // Use the same class loader for ALL plugins.
}
// Create a plugin instance.
Class<?> pluginClass = classLoader.loadClass(pluginConfig.mainClass);
Plugin pluginInstance = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
this.loadPlugin(pluginInstance, PluginIdentifier.fromPluginConfig(pluginConfig), loader);
// Close the file reader.
fileReader.close();
fileReader.close(); // Close the file reader.
// 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);
} catch (ClassNotFoundException ignored) {
Grasscutter.getLogger().warn("Plugin " + plugin.getName() + " has an invalid main class.");
} catch (FileNotFoundException ignored) {
@ -97,11 +126,41 @@ public final class PluginManager {
} catch (Exception 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.
*
* @param plugin The plugin instance.
*/
private void loadPlugin(Plugin plugin, PluginIdentifier identifier, URLClassLoader classLoader) {
@ -111,15 +170,24 @@ public final class PluginManager {
try {
Class<Plugin> pluginClass = Plugin.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) {
Grasscutter.getLogger().warn("Failed to add plugin identifier: " + identifier.name);
}
// Add the plugin to the list of loaded plugins.
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.
try {
plugin.onLoad();
} catch (Throwable exception) {
Grasscutter.getLogger().error("Failed to load plugin: " + identifier.name, exception);
}
}
/**
@ -128,7 +196,11 @@ public final class PluginManager {
public void enablePlugins() {
this.plugins.forEach((name, plugin) -> {
Grasscutter.getLogger().info("Enabling plugin: " + name);
try {
plugin.onEnable();
} catch (Throwable exception) {
Grasscutter.getLogger().error("Failed to enable plugin: " + name, exception);
}
});
}
@ -138,20 +210,27 @@ public final class PluginManager {
public void disablePlugins() {
this.plugins.forEach((name, plugin) -> {
Grasscutter.getLogger().info("Disabling plugin: " + name);
try {
plugin.onDisable();
} catch (Throwable exception) {
Grasscutter.getLogger().error("Failed to disable plugin: " + name, exception);
}
});
}
/**
* Registers a plugin's event listener.
*
* @param plugin The plugin registering the listener.
* @param listener The event listener.
*/
public void registerListener(EventHandler<? extends Event> listener) {
this.listeners.add(listener);
public void registerListener(Plugin plugin, EventHandler<? extends Event> listener) {
this.listeners.get(plugin).add(listener);
}
/**
* Invoke the provided event on all registered event listeners.
*
* @param event The event to invoke.
*/
public void invokeEvent(Event event) {
@ -161,22 +240,70 @@ public final class PluginManager {
/**
* Check an event to handlers for the priority.
*
* @param event The event being called.
* @param priority The priority to call for.
*/
private void checkAndFilter(Event event, HandlerPriority priority) {
this.listeners.stream()
// Create a collection of listeners.
List<EventHandler<? extends Event>> listeners = new LinkedList<>();
// 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) {
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.
*
* @param event The event passed through to the handler.
* @param handler The handler to invoke.
*/

View File

@ -6,10 +6,7 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.data.excels.WorldLevelData;
import emu.grasscutter.game.entity.EntityGadget;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.game.entity.EntityNPC;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.net.proto.VisionTypeOuterClass;
import emu.grasscutter.scripts.constants.EventType;
@ -17,17 +14,12 @@ import emu.grasscutter.scripts.data.*;
import emu.grasscutter.scripts.service.ScriptMonsterSpawnService;
import emu.grasscutter.scripts.service.ScriptMonsterTideService;
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.LuaValue;
import org.luaj.vm2.lib.jse.CoerceJavaToLua;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class SceneScriptManager {
@ -38,15 +30,15 @@ public class SceneScriptManager {
/**
* current triggers controlled by RefreshGroup
*/
private final Int2ObjectOpenHashMap<Set<SceneTrigger>> currentTriggers;
private final Int2ObjectOpenHashMap<SceneRegion> regions;
private Map<Integer,SceneGroup> sceneGroups;
private final Map<Integer, Set<SceneTrigger>> currentTriggers;
private final Map<Integer, EntityRegion> regions; // <EntityId-Region>
private final Map<Integer,SceneGroup> sceneGroups;
private ScriptMonsterTideService scriptMonsterTideService;
private ScriptMonsterSpawnService scriptMonsterSpawnService;
/**
* blockid - loaded groupSet
*/
private Int2ObjectMap<Set<SceneGroup>> loadedGroupSetPerBlock;
private final Map<Integer, Set<SceneGroup>> loadedGroupSetPerBlock;
public static final ExecutorService eventExecutor;
static {
eventExecutor = new ThreadPoolExecutor(4, 4,
@ -55,13 +47,13 @@ public class SceneScriptManager {
}
public SceneScriptManager(Scene scene) {
this.scene = scene;
this.currentTriggers = new Int2ObjectOpenHashMap<>();
this.currentTriggers = new ConcurrentHashMap<>();
this.regions = new Int2ObjectOpenHashMap<>();
this.variables = new HashMap<>();
this.sceneGroups = new HashMap<>();
this.regions = new ConcurrentHashMap<>();
this.variables = new ConcurrentHashMap<>();
this.sceneGroups = new ConcurrentHashMap<>();
this.scriptMonsterSpawnService = new ScriptMonsterSpawnService(this);
this.loadedGroupSetPerBlock = new Int2ObjectOpenHashMap<>();
this.loadedGroupSetPerBlock = new ConcurrentHashMap<>();
// TEMPORARY
if (this.getScene().getId() < 10 && !Grasscutter.getConfig().server.game.enableScriptInBigWorld) {
@ -123,19 +115,25 @@ public class SceneScriptManager {
spawnMonstersInGroup(group, suite);
spawnGadgetsInGroup(group, suite);
}
public SceneRegion getRegionById(int id) {
public EntityRegion getRegionById(int id) {
return regions.get(id);
}
public void registerRegion(SceneRegion region) {
regions.put(region.config_id, region);
public void registerRegion(EntityRegion region) {
regions.put(region.getId(), region);
}
public void registerRegionInGroupSuite(SceneGroup group, SceneSuite suite){
suite.sceneRegions.stream().map(region -> new EntityRegion(this.getScene(), region))
.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 void deregisterRegion(SceneRegion region) {
regions.remove(region.config_id);
}
public Int2ObjectMap<Set<SceneGroup>> getLoadedGroupSetPerBlock() {
public Map<Integer, Set<SceneGroup>> getLoadedGroupSetPerBlock() {
return loadedGroupSetPerBlock;
}
@ -182,10 +180,6 @@ public class SceneScriptManager {
}
this.sceneGroups.put(group.id, group);
if(group.regions != null){
group.regions.forEach(this::registerRegion);
}
}
public void checkRegions() {
@ -193,37 +187,49 @@ public class SceneScriptManager {
return;
}
for (SceneRegion region : this.regions.values()) {
for (var region : this.regions.values()) {
getScene().getEntities().values()
.stream()
.filter(e -> e.getEntityType() <= 2 && region.contains(e.getPosition()))
.filter(e -> e.getEntityType() <= 2 && region.getMetaRegion().contains(e.getPosition()))
.forEach(region::addEntity);
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.config_id).setSourceEntityId(region.config_id));
callEvent(EventType.EVENT_ENTER_REGION, new ScriptArgs(region.getConfigId()).setSourceEntityId(region.getId()));
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){
spawnMonstersInGroup(group, suite);
spawnGadgetsInGroup(group, suite);
// we added trigger first
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){
deregisterTrigger(suite.sceneTriggers);
removeMonstersInGroup(group, suite);
removeGadgetsInGroup(group, suite);
deregisterTrigger(suite.sceneTriggers);
}
public void spawnGadgetsInGroup(SceneGroup group, int suiteIndex) {
spawnGadgetsInGroup(group, group.getSuiteByIndex(suiteIndex));
}
public void spawnGadgetsInGroup(SceneGroup group) {
spawnGadgetsInGroup(group, null);
suite.sceneRegions.forEach(this::deregisterRegion);
}
public void spawnGadgetsInGroup(SceneGroup group, SceneSuite suite) {
@ -240,13 +246,6 @@ public class SceneScriptManager {
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) {
if(suite == null || suite.sceneMonsters.size() <= 0){
return;
@ -255,11 +254,6 @@ public class SceneScriptManager {
.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) {
this.scriptMonsterTideService =
new ScriptMonsterTideService(this, group, tideCount, sceneLimit, ordersConfigId);
@ -351,6 +345,16 @@ public class SceneScriptManager {
}
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);
if (entity.getGadgetData() == null){

View File

@ -316,13 +316,13 @@ public class ScriptLib {
int regionId = table.get("region_eid").toint();
int entityType = table.get("entity_type").toint();
SceneRegion region = this.getSceneScriptManager().getRegionById(regionId);
var region = this.getSceneScriptManager().getRegionById(regionId);
if (region == null) {
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) {

View File

@ -11,4 +11,9 @@ public class SceneGadget extends SceneObject{
public int point_type;
public SceneBossChest boss_chest;
public int interact_id;
public boolean isOneoff;
public void setIsOneoff(boolean isOneoff){
this.isOneoff = isOneoff;
}
}

View File

@ -34,7 +34,7 @@ public class SceneGroup {
public Map<Integer, SceneGadget> gadgets; // <ConfigId, Gadgets>
public Map<String, SceneTrigger> triggers;
public Map<Integer, SceneNPC> npc; // <NpcId, NPC>
public List<SceneRegion> regions;
public Map<Integer, SceneRegion> regions;
public List<SceneSuite> suites;
public List<SceneVar> variables;
@ -115,7 +115,10 @@ public class SceneGroup {
triggers.values().forEach(t -> t.currentGroup = this);
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"));
// Garbages TODO fix properly later
@ -157,6 +160,13 @@ public class SceneGroup {
.map(triggers::get)
.toList()
);
suite.sceneRegions = new ArrayList<>(
suite.regions.stream()
.filter(regions::containsKey)
.map(regions::get)
.toList()
);
}
} catch (ScriptException e) {

View File

@ -1,62 +1,34 @@
package emu.grasscutter.scripts.data;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.scripts.constants.ScriptRegionShape;
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.ToString;
@ToString
@Setter
public class SceneRegion {
public int config_id;
public int shape;
public Position pos;
// for CUBIC
public Position size;
// for SPHERE
public int radius;
private boolean hasNewEntities;
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) {
if (this.getEntities().contains(entity.getId())) {
return;
}
this.getEntities().add(entity.getId());
this.hasNewEntities = true;
}
public void removeEntity(GameEntity entity) {
this.getEntities().remove(entity.getId());
}
public boolean contains(Position p) {
public transient SceneGroup group;
public boolean contains(Position position) {
switch (shape) {
case ScriptRegionShape.CUBIC:
return (Math.abs(pos.getX() - p.getX()) <= size.getX()) &&
(Math.abs(pos.getZ() - p.getZ()) <= size.getZ());
return (Math.abs(pos.getX() - position.getX()) <= size.getX()) &&
(Math.abs(pos.getY() - position.getY()) <= size.getY()) &&
(Math.abs(pos.getZ() - position.getZ()) <= size.getZ());
case ScriptRegionShape.SPHERE:
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;
}
}

View File

@ -11,9 +11,12 @@ public class SceneSuite {
public List<Integer> monsters;
public List<Integer> gadgets;
public List<String> triggers;
public List<Integer> regions;
public int rand_weight;
public transient List<SceneMonster> sceneMonsters;
public transient List<SceneGadget> sceneGadgets;
public transient List<SceneTrigger> sceneTriggers;
public transient List<SceneRegion> sceneRegions;
}

View File

@ -2,7 +2,6 @@ package emu.grasscutter.scripts.serializer;
import com.esotericsoftware.reflectasm.ConstructorAccess;
import com.esotericsoftware.reflectasm.MethodAccess;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.scripts.ScriptUtils;
import lombok.AccessLevel;
@ -12,8 +11,6 @@ import lombok.experimental.FieldDefaults;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@ -56,6 +53,8 @@ public class LuaSerializer implements Serializer {
object = (T) (Float) keyValue.tofloat(); // terrible...
} else if (keyValue.isstring()) {
object = (T) keyValue.tojstring();
} else if (keyValue.isboolean()) {
object = (T) (Boolean) keyValue.toboolean();
} else {
object = (T) keyValue;
}
@ -118,6 +117,8 @@ public class LuaSerializer implements Serializer {
methodAccess.invoke(object, fieldMeta.index, keyValue.toint());
} else if (fieldMeta.getType().equals(String.class)) {
methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring());
} else if (fieldMeta.getType().equals(boolean.class)) {
methodAccess.invoke(object, fieldMeta.index, keyValue.toboolean());
} else {
methodAccess.invoke(object, fieldMeta.index, keyValue.tojstring());
}

View File

@ -1,6 +1,7 @@
package emu.grasscutter.server.event;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.plugin.Plugin;
import emu.grasscutter.utils.EventConsumer;
public final class EventHandler<T extends Event> {
@ -75,7 +76,7 @@ public final class EventHandler<T extends Event> {
/**
* Registers the handler into the PluginManager.
*/
public void register() {
Grasscutter.getPluginManager().registerListener(this);
public void register(Plugin plugin) {
Grasscutter.getPluginManager().registerListener(plugin, this);
}
}

View File

@ -11,7 +11,6 @@ import emu.grasscutter.net.packet.PacketOpcodes;
import emu.grasscutter.net.proto.GetPlayerTokenReqOuterClass.GetPlayerTokenReq;
import emu.grasscutter.net.packet.PacketHandler;
import emu.grasscutter.server.event.game.PlayerCreationEvent;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.game.GameSession;
import emu.grasscutter.server.game.GameSession.SessionState;
import emu.grasscutter.server.packet.send.PacketGetPlayerTokenRsp;
@ -58,15 +57,15 @@ public class HandlerGetPlayerTokenReq extends PacketHandler {
}
}
// Get player
Player player = DatabaseHelper.getPlayerByAccount(account);
// Call creation event.
PlayerCreationEvent event = new PlayerCreationEvent(session, Player.class); event.call();
// Get player.
Player player = DatabaseHelper.getPlayerByAccount(account, event.getPlayerClass());
if (player == null) {
int nextPlayerUid = DatabaseHelper.getNextPlayerId(session.getAccount().getReservedPlayerUid());
// Call creation event.
PlayerCreationEvent event = new PlayerCreationEvent(session, Player.class); event.call();
// Create player instance from event.
player = event.getPlayerClass().getDeclaredConstructor(GameSession.class).newInstance(session);

View File

@ -131,5 +131,293 @@
"count": 1
}
]
},
{
"objNames": [
"SceneObj_Chest_Default_Lv3",
"SceneObj_Chest_Locked_Lv3",
"SceneObj_Chest_Bramble_Lv3",
"SceneObj_Chest_Frozen_Lv3"
],
"advExp": 200,
"resin": 20,
"mora": 8888,
"sigil": 20,
"content": [
{
"itemId": 104012,
"count": 3
},
{
"itemId": 223,
"count": 10
},
{
"itemId": 104002,
"count": 1
}
],
"randomCount": 4,
"randomContent": [
{
"itemId": 11509,
"count": 1
},
{
"itemId": 13501,
"count": 1
},
{
"itemId": 12201,
"count": 1
},
{
"itemId": 12301,
"count": 1
},
{
"itemId": 13201,
"count": 1
},
{
"itemId": 13301,
"count": 1
},
{
"itemId": 14201,
"count": 1
},
{
"itemId": 14301,
"count": 1
},
{
"itemId": 15201,
"count": 1
},
{
"itemId": 15301,
"count": 1
}
]
},
{
"objNames": [
"SceneObj_Chest_Default_Lv4",
"SceneObj_Chest_Locked_Lv4",
"SceneObj_Chest_Bramble_Lv4",
"SceneObj_Chest_Frozen_Lv4"
],
"advExp": 20000,
"resin": 2,
"mora": 88888,
"sigil": 2,
"content": [
{
"itemId": 104012,
"count": 3
},
{
"itemId": 223,
"count": 50
},
{
"itemId": 104002,
"count": 1
}
],
"randomCount": 4,
"randomContent": [
{
"itemId": 13501,
"count": 1
},
{
"itemId": 11301,
"count": 1
},
{
"itemId": 12201,
"count": 1
},
{
"itemId": 12301,
"count": 1
},
{
"itemId": 13201,
"count": 1
},
{
"itemId": 13301,
"count": 1
},
{
"itemId": 14201,
"count": 1
},
{
"itemId": 14301,
"count": 1
},
{
"itemId": 15201,
"count": 1
},
{
"itemId": 15301,
"count": 1
}
]
},
{
"objNames": [
"SceneObj_Chest_Default_Lv5",
"SceneObj_Chest_Locked_Lv5",
"SceneObj_Chest_Bramble_Lv5",
"SceneObj_Chest_Frozen_Lv5"
],
"advExp": 20000,
"resin": 2,
"mora": 88888,
"sigil": 2,
"content": [
{
"itemId": 104012,
"count": 3
},
{
"itemId": 223,
"count": 100
},
{
"itemId": 104002,
"count": 1
}
],
"randomCount": 5,
"randomContent": [
{
"itemId": 13501,
"count": 1
},
{
"itemId": 11301,
"count": 1
},
{
"itemId": 13509,
"count": 1
},
{
"itemId": 12301,
"count": 1
},
{
"itemId": 14509,
"count": 1
},
{
"itemId": 15507,
"count": 1
},
{
"itemId": 15509,
"count": 1
},
{
"itemId": 11509,
"count": 1
},
{
"itemId": 11503,
"count": 1
},
{
"itemId": 15301,
"count": 1
}
]
},
{
"objNames": [
"SceneObj_Area_Common_Property_Ani_Prop_MoonlitBox_01"
],
"advExp": 20000,
"resin": 2,
"mora": 88888,
"sigil": 2,
"content": [
{
"itemId": 104012,
"count": 3
},
{
"itemId": 223,
"count": 100
},
{
"itemId": 104002,
"count": 1
}
],
"randomCount": 5,
"randomContent": [
{
"itemId": 13501,
"count": 1
},
{
"itemId": 11301,
"count": 1
},
{
"itemId": 13509,
"count": 1
},
{
"itemId": 12301,
"count": 1
},
{
"itemId": 14509,
"count": 1
},
{
"itemId": 15507,
"count": 1
},
{
"itemId": 15509,
"count": 1
},
{
"itemId": 11509,
"count": 1
},
{
"itemId": 11503,
"count": 1
},
{
"itemId": 15301,
"count": 1
}
]
},
{
"objNames": [
"SceneObj_Area_Dq_Property_Ani_Prop_JunkChest_01",
"SceneObj_Area_Common_Property_Ani_Prop_JunkChest_02",
"SearchPoint",
"SearchPoint_OnWater"
],
"mora": 1,
"content": [
{
"itemId": 201,
"count": 114
}
]
}
]