Merge unstable into development (#2173)

* Remove more scene synchronized

* Fix worktop options not appearing

* Format code [skip actions]

* Fix delay with server tasks

* Format code [skip actions]

* Fully fix fairy clock (#2146)

* Fix scene transition

* fully fix fairy clock

* Re-add call to `Player#updatePlayerGameTime`

* Format code [skip actions]

* Initialize the script loader in `ResourceLoader#loadAll`

* Fix region removal checking

* Format code [skip actions]

* Use Lombok's `EqualsAndHashCode` for comparing scene regions

* Format code [skip actions]

* Move 'invalid gather object' to `trace`

* Add more information to the 'unknown condition handler' message

* Move invalid ability action to trace

* Make `KcpTunnel` public

* Validate the NPC being talked to

* Format code [skip actions]

* NPCs are not spawned server side; change logic to handle it

* Format code [skip actions]

* unload scene when there are no players (#2147)

* unload scene when there are no players

* Update src/main/java/emu/grasscutter/game/world/Scene.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Check if a command should be copied or HTTP should be used

* Lint Code [skip actions]

* Fix character names rendering incorrectly

* Add basic troubleshooting command

* Implement handbook teleporting

also a few formatting changes and sort data by logical sense

* Fix listener `ConcurrentModificationException` issue

* Add color change to `Join the Community!`

* Lint Code [skip actions]

* Make clickable buttons appear clickable

* Remove 'Mechanicus' entities from the list of entities

* Format code [skip actions]

* Fix going back returning a blank screen

* Implement entity spawning

* Add setting level to entity card

* Add support for 'plain text' mode

* Make descriptions of objects scrollable

* Lint Code [skip actions]

* Format code [skip actions]

* Change the way existing hooks work

* Format code [skip actions]

* Upgrade Javalin to 5.5.0 & Fix project warnings

* Upgrade logging libraries

* Fix gacha mappings static file issue

* Add temporary backwards compatability for `ServerHelper`

* Format code [skip actions]

* Remove artifact signatures from VCS

* Fix forge queue data protocol definition

* Run `spotlessApply`

* Format code [skip actions]

* Download data required for building artifacts

* Add call for Facebook logins

* Add the wiki page as a submodule

* Format code [skip actions]

* Update translation (#2150)

* Update translation

* Update translation

* Separate the dispatch and game servers (pt. 1)

gacha is still broken, handbook still needs to be done

* Format code [skip actions]

* Separate the dispatch and game servers (pt. 2)

this commit fixes the gacha page

* Add description for '/troubleshoot'

* Set default avatar talent level to 10

* Separate the dispatch and game servers (pt. 3)

implement handbook across servers!

* Format code [skip actions]

* Update GitHub Actions to use 'download-file' over 'wget'

* Gm handbook lmao (#2149)

* Fix font issue

* Fix avatars

* Fix text overflow in commands

* Fix virtualized lists and items page 😭😭

* magix why 💀

* use hover style in all minicards

* button

* remove console.log

* lint

* Add icons

* magix asked

* Fix overflow padding issue

* Fix achievement text overflow

* remove icons from repo

* Change command icon

* Add the wiki page as a submodule

* total magix moment

* fix text overflow in commands

* Fix discord button

* Make text scale on Minicard

* import icons and font from another source

* Add hover effects to siebar buttons

* move font and readme to submodule repo

* Make data folder a submodule

* import icons and font from data submodule

* Update README.md

* total magix moment

* magix moment v2

* submodule change

* Import `.webp` files

* Resize `HomeButton`

* Fix 'Copy Command' reappearing after changing pages

---------

Co-authored-by: KingRainbow44 <kobedo11@gmail.com>

* Lint Code [skip actions]

* Download data for the build, not for the lint

* format imports

this is really just to see if build handbook works kek

* Implement proper handbook authentication (pt. 1)

* Implement proper handbook authentication (pt. 2)

* Format code [skip actions]

* Add quest data dumping for the handbook

* Change colors to fit _something suitable_

* Format code [skip actions]

* Fix force pushing to branches after linting

* Fix logic of `SetPlayerPropReq`

* Move more group loading to `trace`

* Add handbook IP authentication in hybrid mode

* Fix player level up not displaying on the client properly

* Format code [skip actions]

* Fix game time locking

* Format code [skip actions]

* Update player properties

* Format code [skip actions]

* Move `warn`s for groups to `debug`

* Fix player pausing

* Move more logs to `trace`

* Use `removeItemById` for deleting items via quests

* Clean up logger more

* Pause in-game time when the world is paused

* Format code [skip actions]

* More player property documentation

* Multi-threaded resource loading

* Format code [skip actions]

* Add quest widgets

* Add quests page (basic impl.)

* Add/fix colors

also fix tailwind

* Remove banned packets

client modifications already perform the job of blocking malicious packets from being executed, no point in having this if self-windy is wanted

* Re-add `BeginCameraSceneLookNotify`

* Fix being unable to attack (#2157)

* Add `PlayerOpenChestEvent`

* Add methods to get players from the server

* Add static methods to register an event handler

* Add `PlayerEnterDungeonEvent`

* Remove legacy documentation from `PlayerMoveEvent`

* Add `PlayerChatEvent`

* Add defaults to `Position`

* Clean up `.utils`

* Revert `Multi-threaded resource loading`

* Fix changing target UID when talking to the server

* Lint Code [skip actions]

* Format code [skip actions]

* fix NPC talk triggering main quest in 46101 (#2158)

Make it so that only talks where the param matches the talkId are checked.

* Format code [skip actions]

* Partially fix Chasing Shadows (#2159)

* Partially fix Chasing Shadows

* Go ahead and move it before the return before Magix tells me to.

* Format code [skip actions]

* Bring back period lol (#2160)

* Disable SNI for the HTTPS server

* Add `EntityCreationEvent`

* Add initial startup message

this is so the server appears like its preparing to start

* Format code [skip actions]

* Enable debug mode for plugin loggers if enabled for the primary logger

* Add documentation about `WorldAreaConfigData`

* Make more fields in excels accessible

* Remove deprecated fields from `GetShopRsp`

* Run `spotlessApply` on definitions

* Add `PlayerEnterAreaEvent`

* Optimize event calls

* Fix event invokes

* Format code [skip actions]

* Remove manual autofinish for main quests. (#2162)

* Add world areas to the textmap cache

* Format code [skip actions]

* Don't overdefine variables in extended classes (#2163)

* Add dumper for world areas

* Format code [skip actions]

* instantiate personalLineList (#2165)

* Fix protocol definitions

thank you Nazrin! (+ hiro for raw definitions)

* Fix the background color leaking from the character widget

* Change HTML spacing to 2 spaces

* Implement hiding widgets

* Change scrollbar to a vibrant color

* Add _some_ scaling to the home buttons and its text

* Build the handbook with Gradle

* Fix the 'finer details' with the handbook UI

* Lint Code [skip actions]

* Fix target destination for the Gradle-built handbook

* Implement fetching a player across servers & Add a chainable JsonObject

useful for plugins! might be used in grasscutter eventually

* Fix GitHub actions

* Fix event calling & canceling

* Run `spotlessApply`

* Rename fields (might be wrong)

* Add/update all/more protocol definitions

* Add/update all/more protocol definitions

* Remove outdated packet

* Fix protocol definitions

* Format code [skip actions]

* Implement some lua variables for less console spam (#2172)

* Implement some lua variables for less console spam

* Add GetHostQuestState

This fixes some chapter 3 stuff.

* Format code [skip actions]

* Fix merge import

* Format code [skip actions]

* Fully fix fairy clock for real this time (#2167)

* Fully fix fairy clock For real this time

* Make it so relogging keeps the time lock state.

* Refactor out questLockTime

* Per Hartie, the client packet needs to be changed too

* Update src/main/java/emu/grasscutter/game/world/World.java

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Update src/main/java/emu/grasscutter/server/packet/recv/HandlerClientLockGameTimeNotify.java

* Remove all code not needed to get clock working

---------

Co-authored-by: Magix <27646710+KingRainbow44@users.noreply.github.com>

* Implement a proper ability system (#2166)

* Apply fix `21dec2fe`

* Apply fix `89d01d5f`

* Apply fix `d900f154`

this one was already implemented; updated to use call from previous commit

* Ability changing commit

TODO: change info to debug

* Remove use of deprecated methods/fields

* Temp commit v2
(Adding LoseHP and some fixes)

* Oopsie

* Probably fix monster battle

* Fix issue with reflecting into fields

* Fix some things

* Fix ability names for 3.6 resources

* Improve logging

---------

Co-authored-by: StartForKiller <jesussanz2003@gmail.com>

* Format code [skip actions]

* Add system for sending messages between servers

* Format some code

* Remove protocol definitions from Spotless

* Default debug to false; enable with `-debug`

* Implement completely useless global value copying

* HACK: Return the avatar which holds the weapon when the weapon is referred to by ID

* Add properties to `AbilityModifier`

* Change the way HTML is served after authentication

* Use thread executors to speed up the database loading process

* Format code [skip actions]

* Add system for setting handbook address and port

* Lint Code [skip actions]

* Format code [skip actions]

* Fix game-related data not saving

* Format code [skip actions]

* Fix handbook server details

* Lint Code [skip actions]

* Format code [skip actions]

* Use the headers provided by a context to get the IP address

should acknowledge #1975

* Format code [skip actions]

* Move more logs to `trace`

* Format code [skip actions]

* more trace

* Fix something and implement weapon entities

* Format code [skip actions]

* Fix `EntityWeapon`

* Remove deprecated API & Fix resource checking

* Fix unnecessary warning for first-time setup

* Implement handbook request limiting

* Format code [skip actions]

* Fix new avatar weapons being null

* Format code [skip actions]

* Fix issue with 35303 being un-completable & Try to fix fulfilled quest conditions being met

* Load activity config on server startup

* Require plugins to specify an API version and match with the server

* Add default open state ignore list

* Format code [skip actions]

* Quick fix for questing, needs more investigation
This would make the questing work again

* Remove existing hack for 35303

* Fix ignored open states from being set

* Format code [skip actions]

* fix the stupidest bug ive ever seen

* Optimize player kicking on server close

* Format code [skip actions]

* Re-add hack to fix 35303

* Update GitHub actions

* Format code [skip actions]

* Potentially fix issues with regions

* Download additional handbook data

* Revert "Potentially fix issues with regions"

This reverts commit 84e3823695.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: scooterboo <lewasite@yahoo.com>
Co-authored-by: Tesutarin <105267106+Tesutarin@users.noreply.github.com>
Co-authored-by: Scald <104459145+Arikatsu@users.noreply.github.com>
Co-authored-by: StartForKiller <jesussanz2003@gmail.com>
This commit is contained in:
Magix
2023-05-31 20:48:16 -07:00
committed by GitHub
parent f46fd372d2
commit 9e5b57a043
3839 changed files with 1841548 additions and 37533 deletions

View File

@@ -1,35 +1,50 @@
package emu.grasscutter;
import emu.grasscutter.utils.Position;
import emu.grasscutter.game.world.Position;
import emu.grasscutter.utils.objects.SparseSet;
import emu.grasscutter.utils.Utils;
import java.util.Arrays;
public final class GameConstants {
public static String VERSION = "3.7.0";
public static boolean DEBUG = false;
public static final int DEFAULT_TEAMS = 4;
public static final int MAX_TEAMS = 10;
public static final int MAIN_CHARACTER_MALE = 10000005;
public static final int MAIN_CHARACTER_FEMALE = 10000007;
public static final Position START_POSITION = new Position(2747, 194, -1719);
public static final int MAX_FRIENDS = 60;
public static final int MAX_FRIEND_REQUESTS = 50;
public static final int SERVER_CONSOLE_UID = 99; // The UID of the server console's "player".
public static final int BATTLE_PASS_MAX_LEVEL = 50;
public static final int BATTLE_PASS_POINT_PER_LEVEL = 1000;
public static final int BATTLE_PASS_POINT_PER_WEEK = 10000;
public static final int BATTLE_PASS_LEVEL_PRICE = 150;
public static final int BATTLE_PASS_CURRENT_INDEX = 2;
// Default entity ability hashes.
public static final String[] DEFAULT_ABILITY_STRINGS = {
"Avatar_DefaultAbility_VisionReplaceDieInvincible", "Avatar_DefaultAbility_AvartarInShaderChange", "Avatar_SprintBS_Invincible",
"Avatar_Freeze_Duration_Reducer", "Avatar_Attack_ReviveEnergy", "Avatar_Component_Initializer", "Avatar_FallAnthem_Achievement_Listener"
"Avatar_DefaultAbility_VisionReplaceDieInvincible",
"Avatar_DefaultAbility_AvartarInShaderChange",
"Avatar_SprintBS_Invincible",
"Avatar_Freeze_Duration_Reducer",
"Avatar_Attack_ReviveEnergy",
"Avatar_Component_Initializer",
"Avatar_FallAnthem_Achievement_Listener"
};
public static final int[] DEFAULT_ABILITY_HASHES = Arrays.stream(DEFAULT_ABILITY_STRINGS).mapToInt(Utils::abilityHash).toArray();
public static final SparseSet ILLEGAL_WEAPONS = new SparseSet("""
10000-10008, 11411, 11506-11508, 12505, 12506, 12508, 12509,
13503, 13506, 14411, 14503, 14505, 14508, 15504-15506
""");
public static final SparseSet ILLEGAL_RELICS = new SparseSet("""
20001, 23300-23340, 23383-23385, 78310-78554, 99310-99554
""");
public static final SparseSet ILLEGAL_ITEMS = new SparseSet("""
100086, 100087, 100100-101000, 101106-101110, 101306, 101500-104000,
105001, 105004, 106000-107000, 107011, 108000, 109000-110000,
115000-130000, 200200-200899, 220050, 220054
""");
public static final int[] DEFAULT_ABILITY_HASHES =
Arrays.stream(DEFAULT_ABILITY_STRINGS).mapToInt(Utils::abilityHash).toArray();
public static final int DEFAULT_ABILITY_NAME = Utils.abilityHash("Default");
}

View File

@@ -1,74 +1,71 @@
package emu.grasscutter;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import static emu.grasscutter.config.Configuration.SERVER;
import static emu.grasscutter.utils.lang.Language.translate;
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 ch.qos.logback.classic.*;
import emu.grasscutter.auth.*;
import emu.grasscutter.command.*;
import emu.grasscutter.config.ConfigContainer;
import emu.grasscutter.data.ResourceLoader;
import emu.grasscutter.database.DatabaseManager;
import emu.grasscutter.plugin.PluginManager;
import emu.grasscutter.plugin.api.ServerHook;
import emu.grasscutter.scripts.ScriptLoader;
import emu.grasscutter.plugin.api.ServerHelper;
import emu.grasscutter.server.dispatch.DispatchServer;
import emu.grasscutter.server.game.GameServer;
import emu.grasscutter.server.http.HttpServer;
import emu.grasscutter.server.http.dispatch.DispatchHandler;
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.server.http.dispatch.*;
import emu.grasscutter.server.http.documentation.*;
import emu.grasscutter.server.http.handlers.*;
import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.Crypto;
import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.Language;
import emu.grasscutter.utils.StartupArguments;
import emu.grasscutter.utils.Utils;
import lombok.Getter;
import lombok.Setter;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.UserInterruptException;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import emu.grasscutter.utils.*;
import emu.grasscutter.utils.lang.Language;
import io.netty.util.concurrent.FastThreadLocalThread;
import java.io.*;
import java.util.Calendar;
import java.util.concurrent.*;
import javax.annotation.Nullable;
import lombok.*;
import org.jline.reader.*;
import org.jline.terminal.*;
import org.reflections.Reflections;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.*;
import java.util.Calendar;
import static emu.grasscutter.config.Configuration.SERVER;
import static emu.grasscutter.utils.Language.translate;
public final class Grasscutter {
public static final File configFile = new File("./config.json");
public static final Reflections reflector = new Reflections("emu.grasscutter");
@Getter private static final Logger logger = (Logger) LoggerFactory.getLogger(Grasscutter.class);
private static LineReader consoleLineReader = null;
@Getter public static ConfigContainer config;
@Getter @Setter private static Language language;
public static final File configFile = new File("./config.json");
@Setter private static ServerRunMode runModeOverride = null; // Config override for run mode
@Getter @Setter private static String preferredLanguage;
@Getter private static int currentDayOfWeek;
@Getter @Setter private static String preferredLanguage;
@Setter private static ServerRunMode runModeOverride = null; // Config override for run mode
@Setter private static boolean noConsole = false;
@Getter private static HttpServer httpServer;
@Getter private static GameServer gameServer;
@Getter private static DispatchServer dispatchServer;
@Getter private static PluginManager pluginManager;
@Getter private static CommandMap commandMap;
@Getter @Setter private static AuthenticationSystem authenticationSystem;
@Getter @Setter private static PermissionHandler permissionHandler;
public static final Reflections reflector = new Reflections("emu.grasscutter");
@Getter public static ConfigContainer config;
private static LineReader consoleLineReader = null;
@Getter
private static final ExecutorService threadPool =
new ThreadPoolExecutor(
6,
6,
60,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(),
FastThreadLocalThread::new,
new ThreadPoolExecutor.AbortPolicy());
static {
// Declare logback configuration.
@@ -83,6 +80,8 @@ public final class Grasscutter {
// Attempt to update configuration.
ConfigContainer.updateConfig();
Grasscutter.getLogger().info("Loading Grasscutter...");
// Load translation files.
Grasscutter.loadLanguage();
@@ -98,21 +97,25 @@ public final class Grasscutter {
System.exit(0); // Exit early.
}
// Get the server run mode.
var runMode = Grasscutter.getRunMode();
// Create command map.
commandMap = new CommandMap(true);
// Initialize server.
Grasscutter.getLogger().info(translate("messages.status.starting"));
Grasscutter.getLogger().info(translate("messages.status.game_version", GameConstants.VERSION));
Grasscutter.getLogger().info(translate("messages.status.version", BuildConfig.VERSION, BuildConfig.GIT_HASH));
logger.info(translate("messages.status.starting"));
logger.info(translate("messages.status.game_version", GameConstants.VERSION));
logger.info(translate("messages.status.version", BuildConfig.VERSION, BuildConfig.GIT_HASH));
// Load all resources.
Grasscutter.updateDayOfWeek();
ResourceLoader.loadAll();
ScriptLoader.init();
if (runMode != ServerRunMode.DISPATCH_ONLY) {
// Load all resources.
Grasscutter.updateDayOfWeek();
ResourceLoader.loadAll();
// Generate handbooks.
Tools.createGmHandbooks(false);
// Generate handbooks.
Tools.createGmHandbooks(false);
}
// Initialize database.
DatabaseManager.initialize();
@@ -122,37 +125,47 @@ public final class Grasscutter {
permissionHandler = new DefaultPermissionHandler();
// Create server instances.
httpServer = new HttpServer();
gameServer = new GameServer();
if (runMode == ServerRunMode.HYBRID || runMode == ServerRunMode.GAME_ONLY)
Grasscutter.gameServer = new GameServer();
if (runMode == ServerRunMode.HYBRID || runMode == ServerRunMode.DISPATCH_ONLY)
Grasscutter.httpServer = new HttpServer();
// Create a server hook instance with both servers.
new ServerHook(gameServer, httpServer);
new ServerHelper(gameServer, httpServer);
// Create plugin manager instance.
pluginManager = new PluginManager();
// Add HTTP routes after loading plugins.
httpServer.addRouter(HttpServer.UnhandledRequestRouter.class);
httpServer.addRouter(HttpServer.DefaultRequestRouter.class);
httpServer.addRouter(RegionHandler.class);
httpServer.addRouter(LogHandler.class);
httpServer.addRouter(GenericHandler.class);
httpServer.addRouter(AnnouncementsHandler.class);
httpServer.addRouter(DispatchHandler.class);
httpServer.addRouter(GachaHandler.class);
httpServer.addRouter(DocumentationServerHandler.class);
if (runMode != ServerRunMode.GAME_ONLY) {
// Add HTTP routes after loading plugins.
httpServer.addRouter(HttpServer.UnhandledRequestRouter.class);
httpServer.addRouter(HttpServer.DefaultRequestRouter.class);
httpServer.addRouter(RegionHandler.class);
httpServer.addRouter(LogHandler.class);
httpServer.addRouter(GenericHandler.class);
httpServer.addRouter(AnnouncementsHandler.class);
httpServer.addRouter(AuthenticationHandler.class);
httpServer.addRouter(GachaHandler.class);
httpServer.addRouter(DocumentationServerHandler.class);
httpServer.addRouter(HandbookHandler.class);
}
// Start servers.
var runMode = Grasscutter.getRunMode();
if (runMode == ServerRunMode.HYBRID) {
httpServer.start();
gameServer.start();
} else if (runMode == ServerRunMode.DISPATCH_ONLY) {
httpServer.start();
// Start dispatch server.
dispatchServer = new DispatchServer("0.0.0.0", 1111);
dispatchServer.start();
} else if (runMode == ServerRunMode.GAME_ONLY) {
gameServer.start();
} else {
getLogger().error(translate("messages.status.run_mode_error", runMode));
getLogger().error(translate("messages.status.run_mode_help"));
getLogger().error(translate("messages.status.shutdown"));
logger.error(translate("messages.status.run_mode_error", runMode));
logger.error(translate("messages.status.run_mode_help"));
logger.error(translate("messages.status.shutdown"));
System.exit(1);
}
@@ -163,16 +176,13 @@ public final class Grasscutter {
Runtime.getRuntime().addShutdownHook(new Thread(Grasscutter::onShutdown));
// Open console.
startConsole();
Grasscutter.startConsole();
}
/**
* Server shutdown event.
*/
/** Server shutdown event. */
private static void onShutdown() {
// Disable all plugins.
if (pluginManager != null)
pluginManager.disablePlugins();
if (pluginManager != null) pluginManager.disablePlugins();
}
/*
@@ -188,9 +198,7 @@ public final class Grasscutter {
* Methods for the configuration system component.
*/
/**
* Attempts to load the configuration from a file.
*/
/** Attempts to load the configuration from a file. */
public static void loadConfig() {
// Check if config.json exists. If not, we generate a new config.
if (!configFile.exists()) {
@@ -204,7 +212,9 @@ public final class Grasscutter {
try {
config = JsonUtils.loadToClass(configFile.toPath(), ConfigContainer.class);
} catch (Exception exception) {
getLogger().error("There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json.");
getLogger()
.error(
"There was an error while trying to load the configuration from config.json. Please make sure that there are no syntax errors. If you want to start with a default configuration, delete your existing config.json.");
System.exit(1);
}
}
@@ -220,9 +230,9 @@ public final class Grasscutter {
try (FileWriter file = new FileWriter(configFile)) {
file.write(JsonUtils.encode(config));
} catch (IOException ignored) {
Grasscutter.getLogger().error("Unable to write to config file.");
logger.error("Unable to write to config file.");
} catch (Exception e) {
Grasscutter.getLogger().error("Unable to save config file.", e);
logger.error("Unable to save config file.", e);
}
}
@@ -251,10 +261,10 @@ public final class Grasscutter {
// When dumb is true, build() never throws.
}
}
consoleLineReader = LineReaderBuilder.builder()
.terminal(terminal)
.build();
consoleLineReader = LineReaderBuilder.builder().terminal(terminal).build();
}
return consoleLineReader;
}
@@ -265,43 +275,45 @@ public final class Grasscutter {
public static void updateDayOfWeek() {
Calendar calendar = Calendar.getInstance();
Grasscutter.currentDayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
Grasscutter.getLogger().debug("Set day of week to "+currentDayOfWeek);
logger.debug("Set day of week to " + currentDayOfWeek);
}
public static void startConsole() {
// Console should not start in dispatch only mode.
if (SERVER.runMode == ServerRunMode.DISPATCH_ONLY) {
getLogger().info(translate("messages.dispatch.no_commands_error"));
if (Grasscutter.getRunMode() == ServerRunMode.DISPATCH_ONLY && Grasscutter.noConsole) {
logger.info(translate("messages.dispatch.no_commands_error"));
return;
} else {
logger.info(translate("messages.status.done"));
}
getLogger().info(translate("messages.status.done"));
String input = null;
boolean isLastInterrupted = false;
var isLastInterrupted = false;
while (config.server.game.enableConsole) {
try {
input = consoleLineReader.readLine("> ");
} catch (UserInterruptException e) {
if (!isLastInterrupted) {
isLastInterrupted = true;
Grasscutter.getLogger().info("Press Ctrl-C again to shutdown.");
logger.info("Press Ctrl-C again to shutdown.");
continue;
} else {
Runtime.getRuntime().exit(0);
}
} catch (EndOfFileException e) {
Grasscutter.getLogger().info("EOF detected.");
logger.info("EOF detected.");
continue;
} catch (IOError e) {
Grasscutter.getLogger().error("An IO error occurred while trying to read from console.", e);
logger.error("An IO error occurred while trying to read from console.", e);
return;
}
isLastInterrupted = false;
try {
CommandMap.getInstance().invoke(null, null, input);
commandMap.invoke(null, null, input);
} catch (Exception e) {
Grasscutter.getLogger().error(translate("messages.game.command_error"), e);
logger.error(translate("messages.game.command_error"), e);
}
}
}
@@ -311,10 +323,16 @@ public final class Grasscutter {
*/
public enum ServerRunMode {
HYBRID, DISPATCH_ONLY, GAME_ONLY
HYBRID,
DISPATCH_ONLY,
GAME_ONLY
}
public enum ServerDebugMode {
ALL, MISSING, WHITELIST, BLACKLIST, NONE
ALL,
MISSING,
WHITELIST,
BLACKLIST,
NONE
}
}

View File

@@ -2,116 +2,47 @@ package emu.grasscutter.auth;
import emu.grasscutter.game.Account;
import emu.grasscutter.server.http.objects.*;
import emu.grasscutter.utils.DispatchUtils;
import io.javalin.http.Context;
import javax.annotation.Nullable;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import javax.annotation.Nullable;
/**
* Defines an authenticator for the server.
* Can be changed by plugins.
*/
/** Defines an authenticator for the server. Can be changed by plugins. */
public interface AuthenticationSystem {
/**
* Called when a user requests to make an account.
* @param username The provided username.
* @param password The provided password. (SHA-256'ed)
*/
void createAccount(String username, String password);
/**
* Called when a user requests to reset their password.
* @param username The username of the account to reset.
*/
void resetPassword(String username);
/**
* Called by plugins to internally verify a user's identity.
* @param details A unique identifier to identify the user. (For example: a JWT token)
* @return The user's account if the verification was successful, null if the user was unable to be verified.
*/
Account verifyUser(String details);
/**
* This is the authenticator used for password authentication.
* @return An authenticator.
*/
Authenticator<LoginResultJson> getPasswordAuthenticator();
/**
* This is the authenticator used for token authentication.
* @return An authenticator.
*/
Authenticator<LoginResultJson> getTokenAuthenticator();
/**
* This is the authenticator used for session authentication.
* @return An authenticator.
*/
Authenticator<ComboTokenResJson> getSessionKeyAuthenticator();
/**
* This is the authenticator used for handling external authentication requests.
* @return An authenticator.
*/
ExternalAuthenticator getExternalAuthenticator();
/**
* This is the authenticator used for handling OAuth authentication requests.
* @return An authenticator.
*/
OAuthAuthenticator getOAuthAuthenticator();
/**
* A data container that holds relevant data for authenticating a client.
*/
@Builder @AllArgsConstructor @Getter
class AuthenticationRequest {
private final Context context;
@Nullable private final LoginAccountRequestJson passwordRequest;
@Nullable private final LoginTokenRequestJson tokenRequest;
@Nullable private final ComboTokenReqJson sessionKeyRequest;
@Nullable private final ComboTokenReqJson.LoginTokenData sessionKeyData;
}
/**
* Generates an authentication request from a {@link LoginAccountRequestJson} object.
*
* @param ctx The Javalin context.
* @param jsonData The JSON data.
* @return An authentication request.
*/
static AuthenticationRequest fromPasswordRequest(Context ctx, LoginAccountRequestJson jsonData) {
return AuthenticationRequest.builder()
.context(ctx)
.passwordRequest(jsonData)
.build();
return AuthenticationRequest.builder().context(ctx).passwordRequest(jsonData).build();
}
/**
* Generates an authentication request from a {@link LoginTokenRequestJson} object.
*
* @param ctx The Javalin context.
* @param jsonData The JSON data.
* @return An authentication request.
*/
static AuthenticationRequest fromTokenRequest(Context ctx, LoginTokenRequestJson jsonData) {
return AuthenticationRequest.builder()
.context(ctx)
.tokenRequest(jsonData)
.build();
return AuthenticationRequest.builder().context(ctx).tokenRequest(jsonData).build();
}
/**
* Generates an authentication request from a {@link ComboTokenReqJson} object.
*
* @param ctx The Javalin context.
* @param jsonData The JSON data.
* @return An authentication request.
*/
static AuthenticationRequest fromComboTokenRequest(Context ctx, ComboTokenReqJson jsonData,
ComboTokenReqJson.LoginTokenData tokenData) {
static AuthenticationRequest fromComboTokenRequest(
Context ctx, ComboTokenReqJson jsonData, ComboTokenReqJson.LoginTokenData tokenData) {
return AuthenticationRequest.builder()
.context(ctx)
.sessionKeyRequest(jsonData)
@@ -121,10 +52,101 @@ public interface AuthenticationSystem {
/**
* Generates an authentication request from a {@link Context} object.
*
* @param ctx The Javalin context.
* @return An authentication request.
*/
static AuthenticationRequest fromExternalRequest(Context ctx) {
return AuthenticationRequest.builder().context(ctx).build();
}
/**
* Called when a user requests to make an account.
*
* @param username The provided username.
* @param password The provided password. (SHA-256'ed)
*/
void createAccount(String username, String password);
/**
* Called when a user requests to reset their password.
*
* @param username The username of the account to reset.
*/
void resetPassword(String username);
/**
* Called by plugins to internally verify a user's identity.
*
* @param details A unique identifier to identify the user. (For example: a JWT token)
* @return The user's account if the verification was successful, null if the user was unable to
* be verified.
*/
Account verifyUser(String details);
/**
* This is the authenticator used for password authentication.
*
* @return An authenticator.
*/
Authenticator<LoginResultJson> getPasswordAuthenticator();
/**
* This is the authenticator used for token authentication.
*
* @return An authenticator.
*/
Authenticator<LoginResultJson> getTokenAuthenticator();
/**
* This is the authenticator used for session authentication.
*
* @return An authenticator.
*/
Authenticator<ComboTokenResJson> getSessionKeyAuthenticator();
/**
* This is the authenticator used for validating session tokens. This is a part of the logic in
* {@link DispatchUtils#authenticate(String, String)}.
*
* <p>Plugins can override this authenticator to add support for alternate session authentication
* methods.
*
* @return {@code true} if the session token is valid, {@code false} otherwise.
*/
Authenticator<Account> getSessionTokenValidator();
/**
* This is the authenticator used for handling external authentication requests.
*
* @return An authenticator.
*/
ExternalAuthenticator getExternalAuthenticator();
/**
* This is the authenticator used for handling OAuth authentication requests.
*
* @return An authenticator.
*/
OAuthAuthenticator getOAuthAuthenticator();
/**
* This is the authenticator used for handling handbook authentication requests.
*
* @return An authenticator.
*/
HandbookAuthenticator getHandbookAuthenticator();
/** A data container that holds relevant data for authenticating a client. */
@Builder
@AllArgsConstructor
@Getter
class AuthenticationRequest {
@Nullable private final Context context;
@Nullable private final LoginAccountRequestJson passwordRequest;
@Nullable private final LoginTokenRequestJson tokenRequest;
@Nullable private final ComboTokenReqJson sessionKeyRequest;
@Nullable private final ComboTokenReqJson.LoginTokenData sessionKeyData;
}
}

View File

@@ -1,17 +1,22 @@
package emu.grasscutter.auth;
import emu.grasscutter.server.http.objects.*;
import emu.grasscutter.server.http.objects.ComboTokenResJson;
import emu.grasscutter.server.http.objects.LoginResultJson;
/**
* Handles username/password authentication from the client.
* @param <T> The response object type. Should be {@link LoginResultJson} or {@link ComboTokenResJson}
*
* @param <T> The response object type. Should be {@link LoginResultJson} or {@link
* ComboTokenResJson}
*/
public interface Authenticator<T> {
/**
* Attempt to authenticate the client with the provided credentials.
* @param request The authentication request wrapped in a {@link AuthenticationSystem.AuthenticationRequest} object.
*
* @param request The authentication request wrapped in a {@link
* AuthenticationSystem.AuthenticationRequest} object.
* @return The result of the login in an object.
*/
T authenticate(AuthenticationSystem.AuthenticationRequest request);
}
}

View File

@@ -1,24 +1,26 @@
package emu.grasscutter.auth;
import static emu.grasscutter.config.Configuration.ACCOUNT;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.auth.DefaultAuthenticators.*;
import emu.grasscutter.game.Account;
import emu.grasscutter.server.http.objects.ComboTokenResJson;
import emu.grasscutter.server.http.objects.LoginResultJson;
import static emu.grasscutter.config.Configuration.ACCOUNT;
import static emu.grasscutter.utils.Language.translate;
/**
* The default Grasscutter authentication implementation.
* Allows all users to access any account.
* The default Grasscutter authentication implementation. Allows all users to access any account.
*/
public final class DefaultAuthentication implements AuthenticationSystem {
private Authenticator<LoginResultJson> passwordAuthenticator;
private Authenticator<LoginResultJson> tokenAuthenticator = new TokenAuthenticator();
private Authenticator<ComboTokenResJson> sessionKeyAuthenticator = new SessionKeyAuthenticator();
private ExternalAuthenticator externalAuthenticator = new ExternalAuthentication();
private OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication();
private final Authenticator<LoginResultJson> passwordAuthenticator;
private final Authenticator<LoginResultJson> tokenAuthenticator = new TokenAuthenticator();
private final Authenticator<ComboTokenResJson> sessionKeyAuthenticator =
new SessionKeyAuthenticator();
private final Authenticator<Account> sessionTokenValidator = new SessionTokenValidator();
private final ExternalAuthenticator externalAuthenticator = new ExternalAuthentication();
private final OAuthAuthenticator oAuthAuthenticator = new OAuthAuthentication();
private final HandbookAuthenticator handbookAuthenticator = new HandbookAuthentication();
public DefaultAuthentication() {
if (ACCOUNT.EXPERIMENTAL_RealPassword) {
@@ -40,7 +42,8 @@ public final class DefaultAuthentication implements AuthenticationSystem {
@Override
public Account verifyUser(String details) {
Grasscutter.getLogger().info(translate("messages.dispatch.authentication.default_unable_to_verify"));
Grasscutter.getLogger()
.info(translate("messages.dispatch.authentication.default_unable_to_verify"));
return null;
}
@@ -59,6 +62,11 @@ public final class DefaultAuthentication implements AuthenticationSystem {
return this.sessionKeyAuthenticator;
}
@Override
public Authenticator<Account> getSessionTokenValidator() {
return this.sessionTokenValidator;
}
@Override
public ExternalAuthenticator getExternalAuthenticator() {
return this.externalAuthenticator;
@@ -68,4 +76,9 @@ public final class DefaultAuthentication implements AuthenticationSystem {
public OAuthAuthenticator getOAuthAuthenticator() {
return this.oAuthAuthenticator;
}
@Override
public HandbookAuthenticator getHandbookAuthenticator() {
return this.handbookAuthenticator;
}
}

View File

@@ -1,31 +1,29 @@
package emu.grasscutter.auth;
import static emu.grasscutter.config.Configuration.ACCOUNT;
import static emu.grasscutter.utils.lang.Language.translate;
import at.favre.lib.crypto.bcrypt.BCrypt;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.Account;
import emu.grasscutter.server.dispatch.*;
import emu.grasscutter.server.http.objects.*;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.Utils;
import javax.crypto.Cipher;
import emu.grasscutter.utils.*;
import io.javalin.http.ContentType;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.concurrent.*;
import javax.crypto.Cipher;
import static emu.grasscutter.config.Configuration.*;
import static emu.grasscutter.utils.Language.translate;
/**
* A class containing default authenticators.
*/
/** A class containing default authenticators. */
public final class DefaultAuthenticators {
/**
* Handles the authentication request from the username and password form.
*/
/** Handles the authentication request from the username and password form. */
public static class PasswordAuthenticator implements Authenticator<LoginResultJson> {
@Override
public LoginResultJson authenticate(AuthenticationRequest request) {
@@ -33,42 +31,39 @@ public final class DefaultAuthenticators {
var requestData = request.getPasswordRequest();
assert requestData != null; // This should never be null.
int playerCount = Grasscutter.getGameServer().getPlayers().size();
boolean successfulLogin = false;
String address = request.getContext().ip();
String address = Utils.address(request.getContext());
String responseMessage = translate("messages.dispatch.account.username_error");
String loggerMessage = "";
// Get account from database.
Account account = DatabaseHelper.getAccountByName(requestData.account);
if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) {
// Check if account exists.
if (account == null && ACCOUNT.autoCreate) {
// This account has been created AUTOMATICALLY. There will be no permissions added.
account = DatabaseHelper.createAccountWithUid(requestData.account, 0);
// Check if account exists.
if (account == null && ACCOUNT.autoCreate) {
// This account has been created AUTOMATICALLY. There will be no permissions added.
account = DatabaseHelper.createAccountWithUid(requestData.account, 0);
// Check if the account was created successfully.
if (account == null) {
responseMessage = translate("messages.dispatch.account.username_create_error");
Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_error", address));
} else {
// Continue with login.
successfulLogin = true;
// Log the creation.
Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", address, response.data.account.uid));
}
} else if (account != null)
// Check if the account was created successfully.
if (account == null) {
responseMessage = translate("messages.dispatch.account.username_create_error");
Grasscutter.getLogger()
.info(translate("messages.dispatch.account.account_login_create_error", address));
} else {
// Continue with login.
successfulLogin = true;
else
loggerMessage = translate("messages.dispatch.account.account_login_exist_error", address);
} else {
responseMessage = translate("messages.dispatch.account.server_max_player_limit");
loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address);
}
// Log the creation.
Grasscutter.getLogger()
.info(
translate(
"messages.dispatch.account.account_login_create_success",
address,
response.data.account.uid));
}
} else if (account != null) successfulLogin = true;
else
loggerMessage = translate("messages.dispatch.account.account_login_exist_error", address);
// Set response data.
if (successfulLogin) {
@@ -77,11 +72,11 @@ public final class DefaultAuthenticators {
response.data.account.token = account.generateSessionKey();
response.data.account.email = account.getEmail();
loggerMessage = translate("messages.dispatch.account.login_success", address, account.getId());
loggerMessage =
translate("messages.dispatch.account.login_success", address, account.getId());
} else {
response.retcode = -201;
response.message = responseMessage;
}
Grasscutter.getLogger().info(loggerMessage);
@@ -96,10 +91,9 @@ public final class DefaultAuthenticators {
var requestData = request.getPasswordRequest();
assert requestData != null; // This should never be null.
int playerCount = Grasscutter.getGameServer().getPlayers().size();
boolean successfulLogin = false;
String address = request.getContext().ip();
String address = Utils.address(request.getContext());
String responseMessage = translate("messages.dispatch.account.username_error");
String loggerMessage = "";
String decryptedPassword = "";
@@ -113,7 +107,10 @@ public final class DefaultAuthenticators {
cipher.init(Cipher.DECRYPT_MODE, private_key);
decryptedPassword = new String(cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)), StandardCharsets.UTF_8);
decryptedPassword =
new String(
cipher.doFinal(Utils.base64Decode(request.getPasswordRequest().password)),
StandardCharsets.UTF_8);
} catch (Exception ignored) {
decryptedPassword = request.getPasswordRequest().password;
}
@@ -126,54 +123,58 @@ public final class DefaultAuthenticators {
// Get account from database.
Account account = DatabaseHelper.getAccountByName(requestData.account);
if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) {
// Check if account exists.
if (account == null && ACCOUNT.autoCreate) {
// This account has been created AUTOMATICALLY. There will be no permissions added.
if (decryptedPassword.length() >= 8) {
account = DatabaseHelper.createAccountWithUid(requestData.account, 0);
account.setPassword(BCrypt.withDefaults().hashToString(12, decryptedPassword.toCharArray()));
account.save();
// Check if account exists.
if (account == null && ACCOUNT.autoCreate) {
// This account has been created AUTOMATICALLY. There will be no permissions added.
if (decryptedPassword.length() >= 8) {
account = DatabaseHelper.createAccountWithUid(requestData.account, 0);
account.setPassword(
BCrypt.withDefaults().hashToString(12, decryptedPassword.toCharArray()));
account.save();
// Check if the account was created successfully.
if (account == null) {
responseMessage = translate("messages.dispatch.account.username_create_error");
loggerMessage = translate("messages.dispatch.account.account_login_create_error", address);
} else {
// Continue with login.
successfulLogin = true;
// Check if the account was created successfully.
if (account == null) {
responseMessage = translate("messages.dispatch.account.username_create_error");
loggerMessage =
translate("messages.dispatch.account.account_login_create_error", address);
} else {
// Continue with login.
successfulLogin = true;
// Log the creation.
Grasscutter.getLogger().info(translate("messages.dispatch.account.account_login_create_success", address, response.data.account.uid));
}
// Log the creation.
Grasscutter.getLogger()
.info(
translate(
"messages.dispatch.account.account_login_create_success",
address,
response.data.account.uid));
}
} else {
successfulLogin = false;
loggerMessage = translate("messages.dispatch.account.login_password_error", address);
responseMessage = translate("messages.dispatch.account.password_length_error");
}
} else if (account != null) {
if (account.getPassword() != null && !account.getPassword().isEmpty()) {
if (BCrypt.verifyer()
.verify(decryptedPassword.toCharArray(), account.getPassword())
.verified) {
successfulLogin = true;
} else {
successfulLogin = false;
loggerMessage = translate("messages.dispatch.account.login_password_error", address);
responseMessage = translate("messages.dispatch.account.password_length_error");
}
} else if (account != null) {
if (account.getPassword() != null && !account.getPassword().isEmpty()) {
if (BCrypt.verifyer().verify(decryptedPassword.toCharArray(), account.getPassword()).verified) {
successfulLogin = true;
} else {
successfulLogin = false;
loggerMessage = translate("messages.dispatch.account.login_password_error", address);
responseMessage = translate("messages.dispatch.account.password_error");
}
} else {
successfulLogin = false;
loggerMessage = translate("messages.dispatch.account.login_password_storage_error", address);
responseMessage = translate("messages.dispatch.account.password_storage_error");
responseMessage = translate("messages.dispatch.account.password_error");
}
} else {
loggerMessage = translate("messages.dispatch.account.account_login_exist_error", address);
successfulLogin = false;
loggerMessage =
translate("messages.dispatch.account.login_password_storage_error", address);
responseMessage = translate("messages.dispatch.account.password_storage_error");
}
} else {
responseMessage = translate("messages.dispatch.account.server_max_player_limit");
loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address);
loggerMessage = translate("messages.dispatch.account.account_login_exist_error", address);
}
// Set response data.
if (successfulLogin) {
response.message = "OK";
@@ -181,11 +182,11 @@ public final class DefaultAuthenticators {
response.data.account.token = account.generateSessionKey();
response.data.account.email = account.getEmail();
loggerMessage = translate("messages.dispatch.account.login_success", address, account.getId());
loggerMessage =
translate("messages.dispatch.account.login_success", address, account.getId());
} else {
response.retcode = -201;
response.message = responseMessage;
}
Grasscutter.getLogger().info(loggerMessage);
@@ -193,9 +194,7 @@ public final class DefaultAuthenticators {
}
}
/**
* Handles the authentication request from the game when using a registry token.
*/
/** Handles the authentication request from the game when using a registry token. */
public static class TokenAuthenticator implements Authenticator<LoginResultJson> {
@Override
public LoginResultJson authenticate(AuthenticationRequest request) {
@@ -205,43 +204,35 @@ public final class DefaultAuthenticators {
assert requestData != null;
boolean successfulLogin;
String address = request.getContext().ip();
String address = Utils.address(request.getContext());
String loggerMessage;
int playerCount = Grasscutter.getGameServer().getPlayers().size();
// Log the attempt.
Grasscutter.getLogger().info(translate("messages.dispatch.account.login_token_attempt", address));
Grasscutter.getLogger()
.info(translate("messages.dispatch.account.login_token_attempt", address));
if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) {
// Get account from database.
Account account = DatabaseHelper.getAccountById(requestData.uid);
// Get account from database.
Account account = DatabaseHelper.getAccountById(requestData.uid);
// Check if account exists/token is valid.
successfulLogin = account != null && account.getSessionKey().equals(requestData.token);
// Check if account exists/token is valid.
successfulLogin = account != null && account.getSessionKey().equals(requestData.token);
// Set response data.
if (successfulLogin) {
response.message = "OK";
response.data.account.uid = account.getId();
response.data.account.token = account.getSessionKey();
response.data.account.email = account.getEmail();
// Log the login.
loggerMessage = translate("messages.dispatch.account.login_token_success", address, requestData.uid);
} else {
response.retcode = -201;
response.message = translate("messages.dispatch.account.account_cache_error");
// Log the failure.
loggerMessage = translate("messages.dispatch.account.login_token_error", address);
}
// Set response data.
if (successfulLogin) {
response.message = "OK";
response.data.account.uid = account.getId();
response.data.account.token = account.getSessionKey();
response.data.account.email = account.getEmail();
// Log the login.
loggerMessage =
translate("messages.dispatch.account.login_token_success", address, requestData.uid);
} else {
response.retcode = -201;
response.message = translate("messages.dispatch.account.server_max_player_limit");
response.message = translate("messages.dispatch.account.account_cache_error");
loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address);
// Log the failure.
loggerMessage = translate("messages.dispatch.account.login_token_error", address);
}
Grasscutter.getLogger().info(loggerMessage);
@@ -249,9 +240,7 @@ public final class DefaultAuthenticators {
}
}
/**
* Handles the authentication request from the game when using a combo token/session key.
*/
/** Handles the authentication request from the game when using a combo token/session key. */
public static class SessionKeyAuthenticator implements Authenticator<ComboTokenResJson> {
@Override
public ComboTokenResJson authenticate(AuthenticationRequest request) {
@@ -263,39 +252,31 @@ public final class DefaultAuthenticators {
assert loginData != null;
boolean successfulLogin;
String address = request.getContext().ip();
String address = Utils.address(request.getContext());
String loggerMessage;
int playerCount = Grasscutter.getGameServer().getPlayers().size();
if (ACCOUNT.maxPlayer <= -1 || playerCount < ACCOUNT.maxPlayer) {
// Get account from database.
Account account = DatabaseHelper.getAccountById(loginData.uid);
// Get account from database.
Account account = DatabaseHelper.getAccountById(loginData.uid);
// Check if account exists/token is valid.
successfulLogin = account != null && account.getSessionKey().equals(loginData.token);
// Check if account exists/token is valid.
successfulLogin = account != null && account.getSessionKey().equals(loginData.token);
// Set response data.
if (successfulLogin) {
response.message = "OK";
response.data.open_id = account.getId();
response.data.combo_id = "157795300";
response.data.combo_token = account.generateLoginToken();
// Set response data.
if (successfulLogin) {
response.message = "OK";
response.data.open_id = account.getId();
response.data.combo_id = "157795300";
response.data.combo_token = account.generateLoginToken();
// Log the login.
loggerMessage = translate("messages.dispatch.account.combo_token_success", address);
// Log the login.
loggerMessage = translate("messages.dispatch.account.combo_token_success", address);
} else {
response.retcode = -201;
response.message = translate("messages.dispatch.account.session_key_error");
// Log the failure.
loggerMessage = translate("messages.dispatch.account.combo_token_error", address);
}
} else {
response.retcode = -201;
response.message = translate("messages.dispatch.account.server_max_player_limit");
response.message = translate("messages.dispatch.account.session_key_error");
loggerMessage = translate("messages.dispatch.account.login_max_player_limit", address);
// Log the failure.
loggerMessage = translate("messages.dispatch.account.combo_token_error", address);
}
Grasscutter.getLogger().info(loggerMessage);
@@ -303,43 +284,161 @@ public final class DefaultAuthenticators {
}
}
/**
* Handles authentication requests from external sources.
*/
/** Handles authentication requests from external sources. */
public static class ExternalAuthentication implements ExternalAuthenticator {
@Override
public void handleLogin(AuthenticationRequest request) {
request.getContext().result("Authentication is not available with the default authentication method.");
request
.getContext()
.result("Authentication is not available with the default authentication method.");
}
@Override
public void handleAccountCreation(AuthenticationRequest request) {
request.getContext().result("Authentication is not available with the default authentication method.");
request
.getContext()
.result("Authentication is not available with the default authentication method.");
}
@Override
public void handlePasswordReset(AuthenticationRequest request) {
request.getContext().result("Authentication is not available with the default authentication method.");
request
.getContext()
.result("Authentication is not available with the default authentication method.");
}
}
/**
* Handles authentication requests from OAuth sources.Zenlith
*/
/** Handles authentication requests from OAuth sources.Zenlith */
public static class OAuthAuthentication implements OAuthAuthenticator {
@Override
public void handleLogin(AuthenticationRequest request) {
request.getContext().result("Authentication is not available with the default authentication method.");
request
.getContext()
.result("Authentication is not available with the default authentication method.");
}
@Override
public void handleRedirection(AuthenticationRequest request, ClientType type) {
request.getContext().result("Authentication is not available with the default authentication method.");
request
.getContext()
.result("Authentication is not available with the default authentication method.");
}
@Override
public void handleTokenProcess(AuthenticationRequest request) {
request.getContext().result("Authentication is not available with the default authentication method.");
request
.getContext()
.result("Authentication is not available with the default authentication method.");
}
}
/** Validates a session token during game login. */
public static class SessionTokenValidator implements Authenticator<Account> {
@Override
public Account authenticate(AuthenticationRequest request) {
var tokenRequest = request.getTokenRequest();
if (tokenRequest == null) {
Grasscutter.getLogger().warn("Invalid session token validator request.");
return null;
}
// Prepare the request.
var client = Grasscutter.getGameServer().getDispatchClient();
var future = new CompletableFuture<Account>();
client.registerCallback(
PacketIds.TokenValidateRsp,
packet -> {
var data = IDispatcher.decode(packet);
// Check if the token is valid.
var valid = data.get("valid").getAsBoolean();
if (!valid) {
future.complete(null);
return;
}
// Return the account data.
future.complete(IDispatcher.decode(data.get("account"), Account.class));
});
client.sendMessage(PacketIds.TokenValidateReq, tokenRequest);
try {
return future.get(5, TimeUnit.SECONDS);
} catch (Exception ignored) {
return null;
}
}
}
/** Handles authentication for the web GM Handbook. */
public static class HandbookAuthentication implements HandbookAuthenticator {
private final String authPage;
public HandbookAuthentication() {
try {
this.authPage = new String(FileUtils.readResource("/html/handbook_auth.html"));
} catch (Exception ignored) {
throw new RuntimeException("Failed to load handbook auth page.");
}
}
@Override
public void presentPage(AuthenticationRequest request) {
var ctx = request.getContext();
if (ctx == null) return;
// Check to see if an IP authentication can be performed.
if (Grasscutter.getRunMode() == ServerRunMode.HYBRID) {
var player = Grasscutter.getGameServer().getPlayerByIpAddress(Utils.address(ctx));
if (player != null) {
// Get the player's session token.
var sessionKey = player.getAccount().getSessionKey();
// Respond with the handbook auth page.
ctx.status(200)
.result(
this.authPage
.replace("{{VALUE}}", "true")
.replace("{{SESSION_TOKEN}}", sessionKey)
.replace("{{PLAYER_ID}}", String.valueOf(player.getUid())));
return;
}
}
// Respond with the handbook auth page.
ctx.contentType(ContentType.TEXT_HTML).result(this.authPage);
}
@Override
public Response authenticate(AuthenticationRequest request) {
var ctx = request.getContext();
if (ctx == null) return null;
// Get the body data.
var playerId = ctx.formParam("playerid");
if (playerId == null) {
return Response.builder().status(400).body("Invalid player ID.").build();
}
try {
// Get the player's session token.
var sessionKey = DispatchUtils.fetchSessionKey(Integer.parseInt(playerId));
if (sessionKey == null) {
return Response.builder().status(400).body("Invalid player ID.").build();
}
// Check if the account is banned.
return Response.builder()
.status(200)
.body(
this.authPage
.replace("{{VALUE}}", "true")
.replace("{{SESSION_TOKEN}}", sessionKey)
.replace("{{PLAYER_ID}}", playerId))
.build();
} catch (NumberFormatException ignored) {
return Response.builder().status(500).body("Invalid player ID.").build();
}
}
}
}

View File

@@ -2,32 +2,31 @@ package emu.grasscutter.auth;
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
/**
* Handles authentication via external routes.
*/
/** Handles authentication via external routes. */
public interface ExternalAuthenticator {
/**
* Called when an external login request is made.
*
* @param request The authentication request.
*/
void handleLogin(AuthenticationRequest request);
/**
* Called when an external account creation request is made.
* @param request The authentication request.
*
* For developers: Use AuthenticationRequest#getRequest() to get the request body.
* Use AuthenticationRequest#getResponse() to get the response body.
* @param request The authentication request.
* <p>For developers: Use AuthenticationRequest#getRequest() to get the request body. Use
* AuthenticationRequest#getResponse() to get the response body.
*/
void handleAccountCreation(AuthenticationRequest request);
/**
* Called when an external password reset request is made.
* @param request The authentication request.
*
* For developers: Use AuthenticationRequest#getRequest() to get the request body.
* Use AuthenticationRequest#getResponse() to get the response body.
* @param request The authentication request.
* <p>For developers: Use AuthenticationRequest#getRequest() to get the request body. Use
* AuthenticationRequest#getResponse() to get the response body.
*/
void handlePasswordReset(AuthenticationRequest request);
}

View File

@@ -0,0 +1,35 @@
package emu.grasscutter.auth;
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
import lombok.Builder;
import lombok.Getter;
/** Handles player authentication for the web GM handbook. */
public interface HandbookAuthenticator {
@Getter
@Builder
class Response {
private final int status;
private final String body;
@Builder.Default private boolean html = false;
}
/**
* Invoked when the user requests to authenticate. This should respond with a page that allows the
* user to authenticate.
*
* @route GET /handbook/authenticate
* @param request The authentication request.
*/
void presentPage(AuthenticationRequest request);
/**
* Invoked when the user requests to authenticate. This is called when the user submits the
* authentication form. This should respond with HTML that sends a message to the GM Handbook. See
* the default handbook authentication page for an example.
*
* @param request The authentication request.
* @return The response to send to the client.
*/
Response authenticate(AuthenticationRequest request);
}

View File

@@ -2,34 +2,33 @@ package emu.grasscutter.auth;
import emu.grasscutter.auth.AuthenticationSystem.AuthenticationRequest;
/**
* Handles authentication via OAuth routes.
*/
/** Handles authentication via OAuth routes. */
public interface OAuthAuthenticator {
/**
* Called when an OAuth login request is made.
*
* @param request The authentication request.
*/
void handleLogin(AuthenticationRequest request);
/**
* Called when a client requests to redirect to login page.
*
* @param request The authentication request.
*/
void handleRedirection(AuthenticationRequest request, ClientType clientType);
/**
* Called when an OAuth login requests callback.
*
* @param request The authentication request.
*/
void handleTokenProcess(AuthenticationRequest request);
/**
* The type of the client.
* Used for handling redirection.
*/
/** The type of the client. Used for handling redirection. */
enum ClientType {
DESKTOP, MOBILE
DESKTOP,
MOBILE
}
}

View File

@@ -15,13 +15,14 @@ public @interface Command {
String permissionTargeted() default "";
public enum TargetRequirement {
NONE, // targetPlayer is not required
OFFLINE, // targetPlayer must be offline
PLAYER, // targetPlayer can be online or offline
ONLINE // targetPlayer must be online
}
TargetRequirement targetRequirement() default TargetRequirement.ONLINE;
boolean threading() default false;
enum TargetRequirement {
NONE, // targetPlayer is not required
OFFLINE, // targetPlayer must be offline
PLAYER, // targetPlayer can be online or offline
ONLINE // targetPlayer must be online
}
}

View File

@@ -1,12 +1,10 @@
package emu.grasscutter.command;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.game.ReceiveCommandFeedbackEvent;
import emu.grasscutter.utils.Language;
import static emu.grasscutter.utils.Language.translate;
import java.util.List;
import java.util.StringJoiner;
@@ -15,7 +13,7 @@ public interface CommandHandler {
/**
* Send a message to the target.
*
* @param player The player to send the message to, or null for the server console.
* @param player The player to send the message to, or null for the server console.
* @param message The message to send.
*/
static void sendMessage(Player player, String message) {
@@ -43,22 +41,23 @@ public interface CommandHandler {
String usage_prefix = translate(player, "commands.execution.usage_prefix");
String command = annotation.label();
for (String alias : annotation.aliases()) {
if (alias.length() < command.length())
command = alias;
if (alias.length() < command.length()) command = alias;
}
if (player != null) {
command = "/" + command;
}
String target = switch (annotation.targetRequirement()) {
case NONE -> "";
case OFFLINE -> "@<UID> "; // TODO: make translation keys for offline and online players
case ONLINE -> (player == null) ? "@<UID> " : "[@<UID>] "; // TODO: make translation keys for offline and online players
case PLAYER -> (player == null) ? "@<UID> " : "[@<UID>] ";
};
String target =
switch (annotation.targetRequirement()) {
case NONE -> "";
case OFFLINE -> "@<UID> "; // TODO: make translation keys for offline and online players
case ONLINE -> (player == null)
? "@<UID> "
: "[@<UID>] "; // TODO: make translation keys for offline and online players
case PLAYER -> (player == null) ? "@<UID> " : "[@<UID>] ";
};
String[] usages = annotation.usage();
StringJoiner joiner = new StringJoiner("\n\t");
for (String usage : usages)
joiner.add(usage_prefix + command + " " + target + usage);
for (String usage : usages) joiner.add(usage_prefix + command + " " + target + usage);
return joiner.toString();
}
@@ -81,9 +80,9 @@ public interface CommandHandler {
/**
* Called when a player/console invokes a command.
*
* @param sender The player/console that invoked the command.
* @param args The arguments to the command.
*/
default void execute(Player sender, Player targetPlayer, List<String> args) {
}
default void execute(Player sender, Player targetPlayer, List<String> args) {}
}

View File

@@ -1,15 +1,17 @@
package emu.grasscutter.command;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
public class CommandHelpers {
public static final Pattern lvlRegex = Pattern.compile("(?<!\\w)l(?:vl?)?(\\d+)"); // Java doesn't have raw string literals :(
public static final Pattern amountRegex = Pattern.compile("((?<=(?<!\\w)x)\\d+|\\d+(?=x)(?!x\\d))");
public static final Pattern lvlRegex =
Pattern.compile("(?<!\\w)l(?:vl?)?(\\d+)"); // Java doesn't have raw string literals :(
public static final Pattern amountRegex =
Pattern.compile("((?<=(?<!\\w)x)\\d+|\\d+(?=x)(?!x\\d))");
public static final Pattern refineRegex = Pattern.compile("(?<!\\w)r(\\d+)");
public static final Pattern rankRegex = Pattern.compile("(\\d+)\\*");
public static final Pattern constellationRegex = Pattern.compile("(?<!\\w)c(\\d+)");
@@ -23,28 +25,35 @@ public class CommandHelpers {
public static final Pattern atkRegex = Pattern.compile("atk(\\d+)");
public static final Pattern defRegex = Pattern.compile("def(\\d+)");
public static final Pattern aiRegex = Pattern.compile("ai(\\d+)");
public static final Pattern sceneRegex = Pattern.compile("scene(\\d+)");
public static final Pattern suiteRegex = Pattern.compile("suite(\\d+)");
public static int matchIntOrNeg(Pattern pattern, String arg) {
Matcher match = pattern.matcher(arg);
if (match.find()) {
return Integer.parseInt(match.group(1)); // This should be exception-safe as only \d+ can be passed to it (i.e. non-empty string of pure digits)
return Integer.parseInt(
match.group(
1)); // This should be exception-safe as only \d+ can be passed to it (i.e. non-empty
// string of pure digits)
}
return -1;
}
public static <T> List<String> parseIntParameters(List<String> args, @Nonnull T params, Map<Pattern, BiConsumer<T, Integer>> map) {
args.removeIf(arg -> {
var argL = arg.toLowerCase();
boolean deleteArg = false;
for (var entry : map.entrySet()) {
int argNum = matchIntOrNeg(entry.getKey(), argL);
if (argNum != -1) {
entry.getValue().accept(params, argNum);
deleteArg = true;
}
}
return deleteArg;
});
public static <T> List<String> parseIntParameters(
List<String> args, @Nonnull T params, Map<Pattern, BiConsumer<T, Integer>> map) {
args.removeIf(
arg -> {
var argL = arg.toLowerCase();
boolean deleteArg = false;
for (var entry : map.entrySet()) {
int argNum = matchIntOrNeg(entry.getKey(), argL);
if (argNum != -1) {
entry.getValue().accept(params, argNum);
deleteArg = true;
}
}
return deleteArg;
});
return args;
}
}

View File

@@ -1,27 +1,23 @@
package emu.grasscutter.command;
import static emu.grasscutter.config.Configuration.SERVER;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.player.Player;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.reflections.Reflections;
import java.net.IDN;
import java.util.*;
import static emu.grasscutter.config.Configuration.ACCOUNT;
import static emu.grasscutter.config.Configuration.SERVER;
import org.reflections.Reflections;
@SuppressWarnings({"UnusedReturnValue", "unused"})
public final class CommandMap {
private static final int INVALID_UID = Integer.MIN_VALUE;
private static final String consoleId = "console";
private final Map<String, CommandHandler> commands = new TreeMap<>();
private final Map<String, CommandHandler> aliases = new TreeMap<>();
private final Map<String, Command> annotations = new TreeMap<>();
private final Object2IntMap<String> targetPlayerIds = new Object2IntOpenHashMap<>();
private static final int INVALID_UID = Integer.MIN_VALUE;
private static final String consoleId = "console";
public CommandMap() {
this(false);
@@ -35,10 +31,24 @@ public final class CommandMap {
return Grasscutter.getCommandMap();
}
private static int getUidFromString(String input) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException ignored) {
var account = DatabaseHelper.getAccountByName(input);
if (account == null) return INVALID_UID;
var player = DatabaseHelper.getPlayerByAccount(account, Player.class);
if (player == null) return INVALID_UID;
// We will be immediately fetching the player again after this,
// but offline vs online Player safety is more important than saving a lookup
return player.getUid();
}
}
/**
* Register a command handler.
*
* @param label The command label.
* @param label The command label.
* @param command The command handler.
* @return Instance chaining.
*/
@@ -52,11 +62,9 @@ public final class CommandMap {
this.commands.put(label, command);
// Register aliases.
if (annotation.aliases().length > 0) {
for (String alias : annotation.aliases()) {
this.aliases.put(alias, command);
this.annotations.put(alias, annotation);
}
for (String alias : annotation.aliases()) {
this.aliases.put(alias, command);
this.annotations.put(alias, annotation);
}
return this;
}
@@ -78,11 +86,9 @@ public final class CommandMap {
this.commands.remove(label);
// Unregister aliases.
if (annotation.aliases().length > 0) {
for (String alias : annotation.aliases()) {
this.aliases.remove(alias);
this.annotations.remove(alias);
}
for (String alias : annotation.aliases()) {
this.aliases.remove(alias);
this.annotations.remove(alias);
}
return this;
@@ -124,21 +130,8 @@ public final class CommandMap {
return handler;
}
private static int getUidFromString(String input) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException ignored) {
var account = DatabaseHelper.getAccountByName(input);
if (account == null) return INVALID_UID;
var player = DatabaseHelper.getPlayerByAccount(account);
if (player == null) return INVALID_UID;
// We will be immediately fetching the player again after this,
// but offline vs online Player safety is more important than saving a lookup
return player.getUid();
}
}
private Player getTargetPlayer(String playerId, Player player, Player targetPlayer, List<String> args) {
private Player getTargetPlayer(
String playerId, Player player, Player targetPlayer, List<String> args) {
// Top priority: If any @UID argument is present, override targetPlayer with it.
for (int i = 0; i < args.size(); i++) {
String arg = args.get(i);
@@ -146,7 +139,8 @@ public final class CommandMap {
arg = args.remove(i).substring(1);
if (arg.equals("")) {
// This is a special case to target nothing, distinct from failing to assign a target.
// This is specifically to allow in-game players to run a command without targeting themselves or anyone else.
// This is specifically to allow in-game players to run a command without targeting
// themselves or anyone else.
return null;
}
int uid = getUidFromString(arg);
@@ -171,7 +165,8 @@ public final class CommandMap {
// Next priority: Use previously-set target. (see /target [[@]UID])
if (targetPlayerIds.containsKey(playerId)) {
targetPlayer = Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.getInt(playerId), true);
targetPlayer =
Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.getInt(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");
@@ -180,7 +175,8 @@ public final class CommandMap {
return targetPlayer;
}
// Lowest priority: Target the player invoking the command. In the case of the console, this will return null.
// Lowest priority: Target the player invoking the command. In the case of the console, this
// will return null.
return player;
}
@@ -206,21 +202,33 @@ public final class CommandMap {
targetPlayerIds.put(playerId, uid);
String target = uid + " (" + targetPlayer.getAccount().getUsername() + ")";
CommandHandler.sendTranslatedMessage(player, "commands.execution.set_target", target);
CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline() ? "commands.execution.set_target_online" : "commands.execution.set_target_offline", target);
CommandHandler.sendTranslatedMessage(
player,
targetPlayer.isOnline()
? "commands.execution.set_target_online"
: "commands.execution.set_target_offline",
target);
return true;
}
/**
* Invoke a command handler with the given arguments.
*
* @param player The player invoking the command or null for the server console.
* @param player The player invoking the command or null for the server console.
* @param rawMessage The messaged used to invoke the command.
*/
public void invoke(Player player, Player targetPlayer, String rawMessage) {
// The console outputs in-game command. [{Account Username} (Player UID: {Player Uid})]
if (SERVER.logCommands) {
if (player != null) {
Grasscutter.getLogger().info("Command used by [" + player.getAccount().getUsername() + " (Player UID: " + player.getUid() + ")]: " + rawMessage);
Grasscutter.getLogger()
.info(
"Command used by ["
+ player.getAccount().getUsername()
+ " (Player UID: "
+ player.getUid()
+ ")]: "
+ rawMessage);
} else {
Grasscutter.getLogger().info("Command used by server console: " + rawMessage);
}
@@ -276,7 +284,12 @@ public final class CommandMap {
}
// Check for permissions.
if (!Grasscutter.getPermissionHandler().checkPermission(player, targetPlayer, annotation.permission(), this.annotations.get(label).permissionTargeted())) {
if (!Grasscutter.getPermissionHandler()
.checkPermission(
player,
targetPlayer,
annotation.permission(),
this.annotations.get(label).permissionTargeted())) {
return;
}
@@ -315,23 +328,27 @@ public final class CommandMap {
}
}
/**
* Scans for all classes annotated with {@link Command} and registers them.
*/
/** Scans for all classes annotated with {@link Command} and registers them. */
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.getDeclaredConstructor().newInstance();
if (object instanceof CommandHandler)
this.registerCommand(cmdData.label(), (CommandHandler) object);
else Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a CommandHandler!");
} catch (Exception exception) {
Grasscutter.getLogger().error("Failed to register command handler for " + annotated.getSimpleName(), exception);
}
});
classes.forEach(
annotated -> {
try {
Command cmdData = annotated.getAnnotation(Command.class);
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!");
} catch (Exception exception) {
Grasscutter.getLogger()
.error(
"Failed to register command handler for " + annotated.getSimpleName(),
exception);
}
});
}
}

View File

@@ -10,13 +10,14 @@ public class DefaultPermissionHandler implements PermissionHandler {
}
@Override
public boolean checkPermission(Player player, Player targetPlayer, String permissionNode, String permissionNodeTargeted) {
if(player == null) {
public boolean checkPermission(
Player player, Player targetPlayer, String permissionNode, String permissionNodeTargeted) {
if (player == null) {
return true;
}
Account account = player.getAccount();
if (player != targetPlayer) { // Additional permission required for targeting another player
if (player != targetPlayer) { // Additional permission required for targeting another player
if (!permissionNodeTargeted.isEmpty() && !account.hasPermission(permissionNodeTargeted)) {
CommandHandler.sendTranslatedMessage(player, "commands.generic.permission_error");
return false;

View File

@@ -3,6 +3,8 @@ package emu.grasscutter.command;
import emu.grasscutter.game.player.Player;
public interface PermissionHandler {
public boolean EnablePermissionCommand();
public boolean checkPermission(Player player, Player targetPlayer, String permissionNode, String permissionNodeTargeted);
boolean EnablePermissionCommand();
boolean checkPermission(
Player player, Player targetPlayer, String permissionNode, String permissionNodeTargeted);
}

View File

@@ -1,26 +1,28 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import at.favre.lib.crypto.bcrypt.BCrypt;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.config.Configuration;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.database.DatabaseManager;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
import java.util.stream.Collectors;
@Command(
label = "account",
usage = {
"create <username> [<UID>]", // Only with EXPERIMENTAL_RealPassword == false
"delete <username>",
"create <username> <password> [<UID>]", // Only with EXPERIMENTAL_RealPassword == true
"resetpass <username> <password>"}, // Only with EXPERIMENTAL_RealPassword == true
targetRequirement = Command.TargetRequirement.NONE)
label = "account",
usage = {
"create <username> [<UID>]", // Only with EXPERIMENTAL_RealPassword == false
"delete <username>",
"create <username> <password> [<UID>]", // Only with EXPERIMENTAL_RealPassword == true
"resetpass <username> <password>"
}, // Only with EXPERIMENTAL_RealPassword == true
targetRequirement = Command.TargetRequirement.NONE)
public final class AccountCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
@@ -38,16 +40,14 @@ public final class AccountCommand implements CommandHandler {
String username = args.get(1);
switch (action) {
default:
sendUsageMessage(sender);
return;
case "create":
default -> this.sendUsageMessage(sender);
case "create" -> {
int uid = 0;
String password = "";
if (Configuration.ACCOUNT.EXPERIMENTAL_RealPassword == true) {
if (Configuration.ACCOUNT.EXPERIMENTAL_RealPassword) {
if (args.size() < 3) {
CommandHandler.sendMessage(sender, "EXPERIMENTAL_RealPassword requires a password argument");
CommandHandler.sendMessage(
sender, "EXPERIMENTAL_RealPassword requires a password argument");
CommandHandler.sendMessage(sender, "Usage: account create <username> <password> [uid]");
return;
}
@@ -58,9 +58,12 @@ public final class AccountCommand implements CommandHandler {
uid = Integer.parseInt(args.get(3));
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.account.invalid"));
if (Configuration.ACCOUNT.EXPERIMENTAL_RealPassword == true) {
CommandHandler.sendMessage(sender, "EXPERIMENTAL_RealPassword requires argument 2 to be a password, not a uid");
CommandHandler.sendMessage(sender, "Usage: account create <username> <password> [uid]");
if (Configuration.ACCOUNT.EXPERIMENTAL_RealPassword) {
CommandHandler.sendMessage(
sender,
"EXPERIMENTAL_RealPassword requires argument 2 to be a password, not a uid");
CommandHandler.sendMessage(
sender, "Usage: account create <username> <password> [uid]");
}
return;
}
@@ -75,47 +78,43 @@ public final class AccountCommand implements CommandHandler {
}
}
}
emu.grasscutter.game.Account account = DatabaseHelper.createAccountWithUid(username, uid);
Account account = DatabaseHelper.createAccountWithUid(username, uid);
if (account == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.account.exists"));
return;
} else {
if (Configuration.ACCOUNT.EXPERIMENTAL_RealPassword == true) {
if (Configuration.ACCOUNT.EXPERIMENTAL_RealPassword) {
account.setPassword(BCrypt.withDefaults().hashToString(12, password.toCharArray()));
}
account.addPermission("*");
account.save(); // Save account to database.
CommandHandler.sendMessage(sender, translate(sender, "commands.account.create", account.getReservedPlayerUid()));
CommandHandler.sendMessage(
sender, translate(sender, "commands.account.create", account.getReservedPlayerUid()));
}
return;
case "delete":
}
case "delete" -> {
// Get the account we want to delete.
Account toDelete = DatabaseHelper.getAccountByName(username);
if (toDelete == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.account.no_account"));
return;
}
DatabaseHelper.deleteAccount(toDelete);
CommandHandler.sendMessage(sender, translate(sender, "commands.account.delete"));
return;
case "resetpass":
if (Configuration.ACCOUNT.EXPERIMENTAL_RealPassword != true) {
CommandHandler.sendMessage(sender, "resetpass requires EXPERIMENTAL_RealPassword to be true.");
}
case "resetpass" -> {
if (!Configuration.ACCOUNT.EXPERIMENTAL_RealPassword) {
CommandHandler.sendMessage(
sender, "resetpass requires EXPERIMENTAL_RealPassword to be true.");
return;
}
if (args.size() != 3) {
CommandHandler.sendMessage(sender, "Invalid Args");
CommandHandler.sendMessage(sender, "Usage: account resetpass <username> <password>");
return;
}
Account toUpdate = DatabaseHelper.getAccountByName(username);
if (toUpdate == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.account.no_account"));
return;
@@ -123,14 +122,42 @@ public final class AccountCommand implements CommandHandler {
// Make sure player can't stay logged in with old password.
kickAccount(toUpdate);
toUpdate.setPassword(BCrypt.withDefaults().hashToString(12, args.get(2).toCharArray()));
toUpdate.save();
CommandHandler.sendMessage(sender, "Password Updated.");
return;
}
case "list" -> {
CommandHandler.sendMessage(sender, "Note: This command might take a while to complete.");
CommandHandler.sendMessage(
sender,
"Accounts: \n"
+ DatabaseManager.getAccountDatastore().find(Account.class).stream()
.map(
acc ->
"%s: %s (%s)"
.formatted(
acc.getId(),
acc.getUsername(),
acc.getReservedPlayerUid() == 0
? this.getPlayerUid(acc)
: acc.getReservedPlayerUid()))
.collect(Collectors.joining("\n")));
}
}
}
/**
* Returns the UID of the player associated with the given account. If the player is not found,
* returns "no UID".
*
* @param account The account to get the UID of.
* @return The UID of the player associated with the given account.
*/
private String getPlayerUid(Account account) {
var player = DatabaseHelper.getPlayerByAccount(account, Player.class);
return player == null ? "no UID" : String.valueOf(player.getUid());
}
private void kickAccount(Account account) {
Player player = Grasscutter.getGameServer().getPlayerByAccountId(account.getId());
if (player != null) {

View File

@@ -3,24 +3,73 @@ package emu.grasscutter.command.commands;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.AchievementData;
import emu.grasscutter.data.excels.achievement.AchievementData;
import emu.grasscutter.game.achievement.AchievementControlReturns;
import emu.grasscutter.game.achievement.Achievements;
import emu.grasscutter.game.player.Player;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
@Command(
label = "achievement",
usage = {"(grant|revoke) <achievementId>", "progress <achievementId> <progress>", "grantall", "revokeall"},
aliases = {"am"},
permission = "player.achievement",
permissionTargeted = "player.achievement.others",
targetRequirement = Command.TargetRequirement.PLAYER,
threading = true)
public class AchievementCommand implements CommandHandler {
label = "achievement",
usage = {
"(grant|revoke) <achievementId>",
"progress <achievementId> <progress>",
"grantall",
"revokeall"
},
aliases = {"am"},
permission = "player.achievement",
permissionTargeted = "player.achievement.others",
targetRequirement = Command.TargetRequirement.PLAYER,
threading = true)
public final class AchievementCommand implements CommandHandler {
private static void sendSuccessMessage(Player sender, String cmd, Object... args) {
CommandHandler.sendTranslatedMessage(
sender, AchievementControlReturns.Return.SUCCESS.getKey() + cmd, args);
}
private static Optional<Integer> parseInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
private static void grantAll(Player sender, Player targetPlayer, Achievements achievements) {
var counter = new AtomicInteger();
GameData.getAchievementDataMap().values().stream()
.filter(AchievementData::isUsed)
.filter(AchievementData::isParent)
.forEach(
data -> {
var success = achievements.grant(data.getId());
if (success.getRet() == AchievementControlReturns.Return.SUCCESS) {
counter.addAndGet(success.getChangedAchievementStatusNum());
}
});
sendSuccessMessage(sender, "grantall", counter.intValue(), targetPlayer.getNickname());
}
private static void revokeAll(Player sender, Player targetPlayer, Achievements achievements) {
var counter = new AtomicInteger();
GameData.getAchievementDataMap().values().stream()
.filter(AchievementData::isUsed)
.filter(AchievementData::isParent)
.forEach(
data -> {
var success = achievements.revoke(data.getId());
if (success.getRet() == AchievementControlReturns.Return.SUCCESS) {
counter.addAndGet(success.getChangedAchievementStatusNum());
}
});
sendSuccessMessage(sender, "revokeall", counter.intValue(), targetPlayer.getNickname());
}
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (args.size() < 1) {
@@ -40,91 +89,70 @@ public class AchievementCommand implements CommandHandler {
}
}
private void grant(Player sender, Player targetPlayer, Achievements achievements, List<String> args) {
private void grant(
Player sender, Player targetPlayer, Achievements achievements, List<String> args) {
if (args.size() < 1) {
this.sendUsageMessage(sender);
}
parseInt(args.remove(0)).ifPresentOrElse(integer -> {
var ret = achievements.grant(integer);
switch (ret.getRet()) {
case SUCCESS -> sendSuccessMessage(sender, "grant", targetPlayer.getNickname());
case ACHIEVEMENT_NOT_FOUND -> CommandHandler.sendTranslatedMessage(sender, ret.getRet().getKey());
case ALREADY_ACHIEVED -> CommandHandler.sendTranslatedMessage(sender, ret.getRet().getKey(), targetPlayer.getNickname());
}
}, () -> this.sendUsageMessage(sender));
parseInt(args.remove(0))
.ifPresentOrElse(
integer -> {
var ret = achievements.grant(integer);
switch (ret.getRet()) {
case SUCCESS -> sendSuccessMessage(sender, "grant", targetPlayer.getNickname());
case ACHIEVEMENT_NOT_FOUND -> CommandHandler.sendTranslatedMessage(
sender, ret.getRet().getKey());
case ALREADY_ACHIEVED -> CommandHandler.sendTranslatedMessage(
sender, ret.getRet().getKey(), targetPlayer.getNickname());
}
},
() -> this.sendUsageMessage(sender));
}
private void revoke(Player sender, Player targetPlayer, Achievements achievements, List<String> args) {
private void revoke(
Player sender, Player targetPlayer, Achievements achievements, List<String> args) {
if (args.size() < 1) {
this.sendUsageMessage(sender);
}
parseInt(args.remove(0)).ifPresentOrElse(integer -> {
var ret = achievements.revoke(integer);
switch (ret.getRet()) {
case SUCCESS -> sendSuccessMessage(sender, "revoke", targetPlayer.getNickname());
case ACHIEVEMENT_NOT_FOUND -> CommandHandler.sendTranslatedMessage(sender, ret.getRet().getKey());
case NOT_YET_ACHIEVED -> CommandHandler.sendTranslatedMessage(sender, ret.getRet().getKey(), targetPlayer.getNickname());
}
}, () -> this.sendUsageMessage(sender));
parseInt(args.remove(0))
.ifPresentOrElse(
integer -> {
var ret = achievements.revoke(integer);
switch (ret.getRet()) {
case SUCCESS -> sendSuccessMessage(sender, "revoke", targetPlayer.getNickname());
case ACHIEVEMENT_NOT_FOUND -> CommandHandler.sendTranslatedMessage(
sender, ret.getRet().getKey());
case NOT_YET_ACHIEVED -> CommandHandler.sendTranslatedMessage(
sender, ret.getRet().getKey(), targetPlayer.getNickname());
}
},
() -> this.sendUsageMessage(sender));
}
private void progress(Player sender, Player targetPlayer, Achievements achievements, List<String> args) {
private void progress(
Player sender, Player targetPlayer, Achievements achievements, List<String> args) {
if (args.size() < 2) {
this.sendUsageMessage(sender);
}
parseInt(args.remove(0)).ifPresentOrElse(integer -> {
parseInt(args.remove(0)).ifPresentOrElse(progress -> {
var ret = achievements.progress(integer, progress);
switch (ret.getRet()) {
case SUCCESS -> sendSuccessMessage(sender, "progress", targetPlayer.getNickname(), integer, progress);
case ACHIEVEMENT_NOT_FOUND -> CommandHandler.sendTranslatedMessage(sender, ret.getRet().getKey());
}
}, () -> this.sendUsageMessage(sender));
}, () -> this.sendUsageMessage(sender));
}
private static void sendSuccessMessage(Player sender, String cmd, Object... args) {
CommandHandler.sendTranslatedMessage(sender, AchievementControlReturns.Return.SUCCESS.getKey() + cmd, args);
}
private static Optional<Integer> parseInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
private static void grantAll(Player sender, Player targetPlayer, Achievements achievements) {
var counter = new AtomicInteger();
GameData.getAchievementDataMap().values().stream()
.filter(AchievementData::isUsed)
.filter(AchievementData::isParent)
.forEach(data -> {
var success = achievements.grant(data.getId());
if (success.getRet() == AchievementControlReturns.Return.SUCCESS) {
counter.addAndGet(success.getChangedAchievementStatusNum());
}
});
sendSuccessMessage(sender, "grantall", counter.intValue(), targetPlayer.getNickname());
}
private static void revokeAll(Player sender, Player targetPlayer, Achievements achievements) {
var counter = new AtomicInteger();
GameData.getAchievementDataMap().values().stream()
.filter(AchievementData::isUsed)
.filter(AchievementData::isParent)
.forEach(data -> {
var success = achievements.revoke(data.getId());
if (success.getRet() == AchievementControlReturns.Return.SUCCESS) {
counter.addAndGet(success.getChangedAchievementStatusNum());
}
});
sendSuccessMessage(sender, "revokeall", counter.intValue(), targetPlayer.getNickname());
parseInt(args.remove(0))
.ifPresentOrElse(
integer -> {
parseInt(args.remove(0))
.ifPresentOrElse(
progress -> {
var ret = achievements.progress(integer, progress);
switch (ret.getRet()) {
case SUCCESS -> sendSuccessMessage(
sender, "progress", targetPlayer.getNickname(), integer, progress);
case ACHIEVEMENT_NOT_FOUND -> CommandHandler.sendTranslatedMessage(
sender, ret.getRet().getKey());
}
},
() -> this.sendUsageMessage(sender));
},
() -> this.sendUsageMessage(sender));
}
}

View File

@@ -1,22 +1,22 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.packet.send.PacketServerAnnounceNotify;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "announce",
usage = {"<content>", "refresh", "(tpl|revoke) <templateId>"},
permission = "server.announce",
aliases = {"a"},
targetRequirement = Command.TargetRequirement.NONE)
@Command(
label = "announce",
usage = {"<content>", "refresh", "(tpl|revoke) <templateId>"},
permission = "server.announce",
aliases = {"a"},
targetRequirement = Command.TargetRequirement.NONE)
public final class AnnounceCommand implements CommandHandler {
@Override
@@ -37,17 +37,20 @@ public final class AnnounceCommand implements CommandHandler {
var templateId = Integer.parseInt(args.get(1));
var tpl = manager.getAnnounceConfigItemMap().get(templateId);
if (tpl == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.announce.not_found", templateId));
CommandHandler.sendMessage(
sender, translate(sender, "commands.announce.not_found", templateId));
return;
}
manager.broadcast(Collections.singletonList(tpl));
CommandHandler.sendMessage(sender, translate(sender, "commands.announce.send_success", tpl.getTemplateId()));
CommandHandler.sendMessage(
sender, translate(sender, "commands.announce.send_success", tpl.getTemplateId()));
break;
case "refresh":
var num = manager.refresh();
CommandHandler.sendMessage(sender, translate(sender, "commands.announce.refresh_success", num));
CommandHandler.sendMessage(
sender, translate(sender, "commands.announce.refresh_success", num));
break;
case "revoke":
@@ -58,16 +61,18 @@ public final class AnnounceCommand implements CommandHandler {
var templateId1 = Integer.parseInt(args.get(1));
manager.revoke(templateId1);
CommandHandler.sendMessage(sender, translate(sender, "commands.announce.revoke_done", templateId1));
CommandHandler.sendMessage(
sender, translate(sender, "commands.announce.revoke_done", templateId1));
break;
default:
var id = new Random().nextInt(10000, 99999);
var text = String.join(" ", args);
manager.getOnlinePlayers().forEach(i -> i.sendPacket(new PacketServerAnnounceNotify(text, id)));
manager
.getOnlinePlayers()
.forEach(i -> i.sendPacket(new PacketServerAnnounceNotify(text, id)));
CommandHandler.sendMessage(sender, translate(sender, "commands.announce.send_success", id));
}
}
}

View File

@@ -1,19 +1,17 @@
package emu.grasscutter.command.commands;
import java.util.List;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.game.GameSession;
import java.util.List;
@Command(
label = "ban",
usage = {"[<time> [<reason>]]"},
permission = "server.ban",
targetRequirement = Command.TargetRequirement.PLAYER
)
label = "ban",
usage = {"[<time> [<reason>]]"},
permission = "server.ban",
targetRequirement = Command.TargetRequirement.PLAYER)
public final class BanCommand implements CommandHandler {
private boolean banAccount(Player targetPlayer, int time, String reason) {
@@ -43,14 +41,14 @@ public final class BanCommand implements CommandHandler {
switch (args.size()) {
case 2:
reason = args.get(1); // Fall-through
reason = args.get(1); // Fall-through
case 1:
try {
time = Integer.parseInt(args.get(0));
} catch (NumberFormatException ignored) {
CommandHandler.sendTranslatedMessage(sender, "commands.ban.invalid_time");
return;
} // Fall-through, unimportant
} // Fall-through, unimportant
default:
break;
}

View File

@@ -1,41 +1,35 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.command.CommandHelpers.*;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.Inventory;
import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.player.Player;
import lombok.Setter;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static emu.grasscutter.command.CommandHelpers.*;
import lombok.Setter;
@Command(
label = "clear",
usage = {"(all|wp|art|mat) [lv<max level>] [r<max refinement>] [<max rarity>*]"},
permission = "player.clearinv",
permissionTargeted = "player.clearinv.others")
label = "clear",
usage = {"(all|wp|art|mat) [lv<max level>] [r<max refinement>] [<max rarity>*]"},
permission = "player.clearinv",
permissionTargeted = "player.clearinv.others")
public final class ClearCommand implements CommandHandler {
private static final Map<Pattern, BiConsumer<ClearItemParameters, Integer>> intCommandHandlers = Map.ofEntries(
Map.entry(lvlRegex, ClearItemParameters::setLvl),
Map.entry(refineRegex, ClearItemParameters::setRefinement),
Map.entry(rankRegex, ClearItemParameters::setRank)
);
private static final Map<Pattern, BiConsumer<ClearItemParameters, Integer>> intCommandHandlers =
Map.ofEntries(
Map.entry(lvlRegex, ClearItemParameters::setLvl),
Map.entry(refineRegex, ClearItemParameters::setRefinement),
Map.entry(rankRegex, ClearItemParameters::setRank));
private static class ClearItemParameters {
@Setter public int lvl = 1;
@Setter public int refinement = 1;
@Setter public int rank = 4;
}
private Stream<GameItem> getOther(ItemType type, Inventory playerInventory, ClearItemParameters param) {
private Stream<GameItem> getOther(
ItemType type, Inventory playerInventory, ClearItemParameters param) {
return playerInventory.getItems().values().stream()
.filter(item -> item.getItemType() == type)
.filter(item -> item.getItemData().getRankLevel() <= param.rank)
@@ -66,7 +60,7 @@ public final class ClearCommand implements CommandHandler {
return;
}
String playerString = targetPlayer.getNickname(); // Should probably be UID instead but whatever
String playerString = targetPlayer.getNickname(); // Should probably be UID instead but whatever
switch (args.get(0)) {
case "wp" -> {
playerInventory.removeItems(getWeapons(playerInventory, param).toList());
@@ -77,7 +71,8 @@ public final class ClearCommand implements CommandHandler {
CommandHandler.sendTranslatedMessage(sender, "commands.clear.artifacts", playerString);
}
case "mat" -> {
playerInventory.removeItems(getOther(ItemType.ITEM_MATERIAL, playerInventory, param).toList());
playerInventory.removeItems(
getOther(ItemType.ITEM_MATERIAL, playerInventory, param).toList());
CommandHandler.sendTranslatedMessage(sender, "commands.clear.materials", playerString);
}
case "all" -> {
@@ -85,16 +80,26 @@ public final class ClearCommand implements CommandHandler {
CommandHandler.sendTranslatedMessage(sender, "commands.clear.artifacts", playerString);
playerInventory.removeItems(getWeapons(playerInventory, param).toList());
CommandHandler.sendTranslatedMessage(sender, "commands.clear.weapons", playerString);
playerInventory.removeItems(getOther(ItemType.ITEM_MATERIAL, playerInventory, param).toList());
playerInventory.removeItems(
getOther(ItemType.ITEM_MATERIAL, playerInventory, param).toList());
CommandHandler.sendTranslatedMessage(sender, "commands.clear.materials", playerString);
playerInventory.removeItems(getOther(ItemType.ITEM_FURNITURE, playerInventory, param).toList());
playerInventory.removeItems(
getOther(ItemType.ITEM_FURNITURE, playerInventory, param).toList());
CommandHandler.sendTranslatedMessage(sender, "commands.clear.furniture", playerString);
playerInventory.removeItems(getOther(ItemType.ITEM_DISPLAY, playerInventory, param).toList());
playerInventory.removeItems(
getOther(ItemType.ITEM_DISPLAY, playerInventory, param).toList());
CommandHandler.sendTranslatedMessage(sender, "commands.clear.displays", playerString);
playerInventory.removeItems(getOther(ItemType.ITEM_VIRTUAL, playerInventory, param).toList());
playerInventory.removeItems(
getOther(ItemType.ITEM_VIRTUAL, playerInventory, param).toList());
CommandHandler.sendTranslatedMessage(sender, "commands.clear.virtuals", playerString);
CommandHandler.sendTranslatedMessage(sender, "commands.clear.everything", playerString);
}
}
}
private static class ClearItemParameters {
@Setter public int lvl = 1;
@Setter public int refinement = 1;
@Setter public int rank = 4;
}
}

View File

@@ -4,23 +4,26 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import java.util.List;
@Command(label = "coop", usage = {"[<host UID>]"}, permission = "server.coop", permissionTargeted = "server.coop.others")
@Command(
label = "coop",
usage = {"[<host UID>]"},
permission = "server.coop",
permissionTargeted = "server.coop.others")
public final class CoopCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
Player host = sender;
switch (args.size()) {
case 0: // Summon target to self
case 0: // Summon target to self
if (sender == null) { // Console doesn't have a self to summon to
sendUsageMessage(sender);
return;
}
break;
case 1: // Summon target to argument
case 1: // Summon target to argument
try {
int hostId = Integer.parseInt(args.get(0));
host = Grasscutter.getGameServer().getPlayerByUid(hostId);
@@ -38,12 +41,17 @@ public final class CoopCommand implements CommandHandler {
return;
}
// There's no target==host check but this just places them in multiplayer in their own world which seems fine.
// There's no target==host check but this just places them in multiplayer in their own world
// which seems fine.
if (targetPlayer.isInMultiplayer()) {
targetPlayer.getServer().getMultiplayerSystem().leaveCoop(targetPlayer);
}
host.getServer().getMultiplayerSystem().applyEnterMp(targetPlayer, host.getUid());
targetPlayer.getServer().getMultiplayerSystem().applyEnterMpReply(host, targetPlayer.getUid(), true);
CommandHandler.sendTranslatedMessage(sender, "commands.coop.success", targetPlayer.getNickname(), host.getNickname());
targetPlayer
.getServer()
.getMultiplayerSystem()
.applyEnterMpReply(host, targetPlayer.getUid(), true);
CommandHandler.sendTranslatedMessage(
sender, "commands.coop.success", targetPlayer.getNickname(), host.getNickname());
}
}

View File

@@ -0,0 +1,27 @@
package emu.grasscutter.command.commands;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.packet.send.PacketCutsceneBeginNotify;
import java.util.List;
import lombok.val;
@Command(
label = "cutscene",
aliases = {"c"},
usage = {"[<cutsceneId>]"},
permission = "player.group",
permissionTargeted = "player.group.others")
public final class CutsceneCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (args.isEmpty()) {
sendUsageMessage(sender);
return;
}
val cutSceneId = Integer.parseInt(args.get(0));
targetPlayer.sendPacket(new PacketCutsceneBeginNotify(cutSceneId));
}
}

View File

@@ -1,14 +1,18 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "enter_dungeon", aliases = {"enterdungeon", "dungeon"}, usage = {"<dungeonId>"}, permission = "player.enterdungeon", permissionTargeted = "player.enterdungeon.others")
@Command(
label = "enter_dungeon",
aliases = {"enterdungeon", "dungeon"},
usage = {"<dungeonId>"},
permission = "player.enterdungeon",
permissionTargeted = "player.enterdungeon.others")
public final class EnterDungeonCommand implements CommandHandler {
@Override
@@ -21,16 +25,23 @@ public final class EnterDungeonCommand implements CommandHandler {
try {
int dungeonId = Integer.parseInt(args.get(0));
if (dungeonId == targetPlayer.getSceneId()) {
CommandHandler.sendMessage(sender, translate(sender, "commands.enter_dungeon.in_dungeon_error"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.enter_dungeon.in_dungeon_error"));
return;
}
boolean result = targetPlayer.getServer().getDungeonSystem().enterDungeon(targetPlayer.getSession().getPlayer(), 0, dungeonId);
boolean result =
targetPlayer
.getServer()
.getDungeonSystem()
.enterDungeon(targetPlayer.getSession().getPlayer(), 0, dungeonId);
if (!result) {
CommandHandler.sendMessage(sender, translate(sender, "commands.enter_dungeon.not_found_error"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.enter_dungeon.not_found_error"));
} else {
CommandHandler.sendMessage(sender, translate(sender, "commands.enter_dungeon.changed", dungeonId));
CommandHandler.sendMessage(
sender, translate(sender, "commands.enter_dungeon.changed", dungeonId));
}
} catch (Exception e) {
sendUsageMessage(sender);

View File

@@ -0,0 +1,139 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.command.CommandHelpers.*;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ElementType;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.server.event.entity.EntityDamageEvent;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import lombok.Setter;
@Command(
label = "entity",
usage = {
"<configId gadget> [state<state>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>]",
"<configId monster> [ai<aiId>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>]"
},
permission = "server.entity")
public final class EntityCommand implements CommandHandler {
private static final Map<Pattern, BiConsumer<EntityParameters, Integer>> intCommandHandlers =
Map.ofEntries(
Map.entry(stateRegex, EntityParameters::setState),
Map.entry(maxHPRegex, EntityParameters::setMaxHP),
Map.entry(hpRegex, EntityParameters::setHp),
Map.entry(defRegex, EntityParameters::setDef),
Map.entry(atkRegex, EntityParameters::setAtk),
Map.entry(aiRegex, EntityParameters::setAi));
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
EntityParameters param = new EntityParameters();
parseIntParameters(args, param, intCommandHandlers);
// At this point, first remaining argument MUST be the id and the rest the pos
if (args.size() != 1) {
sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar
throw new IllegalArgumentException();
}
try {
param.configId = Integer.parseInt(args.get(0));
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.cfgId"));
}
param.scene = targetPlayer.getScene();
var entity = param.scene.getEntityByConfigId(param.configId);
if (entity == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.entity.not_found_error"));
return;
}
applyFightProps(entity, param);
applyGadgetParams(entity, param);
applyMonsterParams(entity, param);
CommandHandler.sendMessage(sender, translate(sender, "commands.status.success"));
}
private void applyGadgetParams(GameEntity entity, EntityParameters param) {
if (!(entity instanceof EntityGadget)) {
return;
}
if (param.state != -1) {
((EntityGadget) entity).updateState(param.state);
}
}
private void applyMonsterParams(GameEntity entity, EntityParameters param) {
if (!(entity instanceof EntityMonster)) {
return;
}
if (param.ai != -1) {
((EntityMonster) entity).setAiId(param.ai);
// TODO notify
}
}
private void applyFightProps(GameEntity entity, EntityParameters param) {
var changedFields = new ArrayList<FightProperty>();
if (param.maxHP != -1) {
setFightProperty(entity, FightProperty.FIGHT_PROP_MAX_HP, param.maxHP, changedFields);
}
if (param.hp != -1) {
float targetHp = param.hp == 0 ? Float.MAX_VALUE : param.hp;
float oldHp = entity.getFightProperty(FightProperty.FIGHT_PROP_CUR_HP);
setFightProperty(entity, FightProperty.FIGHT_PROP_CUR_HP, targetHp, changedFields);
EntityDamageEvent event =
new EntityDamageEvent(entity, oldHp - targetHp, ElementType.None, null);
callHPEvents(entity, event);
}
if (param.atk != -1) {
setFightProperty(entity, FightProperty.FIGHT_PROP_ATTACK, param.atk, changedFields);
setFightProperty(entity, FightProperty.FIGHT_PROP_CUR_ATTACK, param.atk, changedFields);
}
if (param.def != -1) {
setFightProperty(entity, FightProperty.FIGHT_PROP_DEFENSE, param.def, changedFields);
setFightProperty(entity, FightProperty.FIGHT_PROP_CUR_DEFENSE, param.def, changedFields);
}
if (!changedFields.isEmpty()) {
entity
.getScene()
.broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, changedFields));
}
}
private void callHPEvents(GameEntity entity, EntityDamageEvent event) {
entity.runLuaCallbacks(event);
}
private void setFightProperty(
GameEntity entity, FightProperty property, float value, List<FightProperty> modifiedProps) {
entity.setFightProperty(property, value);
modifiedProps.add(property);
}
private static class EntityParameters {
@Setter public int configId = -1;
@Setter public int state = -1;
@Setter public int hp = -1;
@Setter public int maxHP = -1;
@Setter public int atk = -1;
@Setter public int def = -1;
@Setter public int ai = -1;
public Scene scene = null;
}
}

View File

@@ -1,72 +1,285 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.GameConstants.*;
import static emu.grasscutter.command.CommandHelpers.*;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.GameDepot;
import emu.grasscutter.data.excels.AvatarData;
import emu.grasscutter.data.excels.AvatarSkillDepotData;
import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.data.excels.ReliquaryAffixData;
import emu.grasscutter.data.excels.ReliquaryMainPropData;
import emu.grasscutter.data.excels.avatar.AvatarData;
import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData;
import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.inventory.GameItem;
import emu.grasscutter.game.inventory.ItemType;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActionReason;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.utils.SparseSet;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import static emu.grasscutter.command.CommandHelpers.*;
import lombok.Setter;
@Command(
label = "give",
aliases = {"g", "item", "giveitem"},
usage = {
"(<itemId>|<avatarId>|all|weapons|mats|avatars) [lv<level>] [r<refinement>] [x<amount>] [c<constellation>] [sl<skilllevel>]",
"<artifactId> [lv<level>] [x<amount>] [<mainPropId>] [<appendPropId>[,<times>]]..."},
permission = "player.give",
permissionTargeted = "player.give.others",
threading = true)
label = "give",
aliases = {"g", "item", "giveitem"},
usage = {
"(<itemId>|<avatarId>|all|weapons|mats|avatars) [lv<level>] [r<refinement>] [x<amount>] [c<constellation>] [sl<skilllevel>]",
"<artifactId> [lv<level>] [x<amount>] [<mainPropId>] [<appendPropId>[,<times>]]..."
},
permission = "player.give",
permissionTargeted = "player.give.others",
threading = true)
public final class GiveCommand implements CommandHandler {
private enum GiveAllType {
NONE,
ALL,
WEAPONS,
MATS,
AVATARS
private static final Map<Pattern, BiConsumer<GiveItemParameters, Integer>> intCommandHandlers =
Map.ofEntries(
Map.entry(lvlRegex, GiveItemParameters::setLvl),
Map.entry(refineRegex, GiveItemParameters::setRefinement),
Map.entry(amountRegex, GiveItemParameters::setAmount),
Map.entry(constellationRegex, GiveItemParameters::setConstellation),
Map.entry(skillLevelRegex, GiveItemParameters::setSkillLevel));
private static Avatar makeAvatar(GiveItemParameters param) {
return makeAvatar(
param.avatarData,
param.lvl,
Avatar.getMinPromoteLevel(param.lvl),
param.constellation,
param.skillLevel);
}
private static final Map<Pattern, BiConsumer<GiveItemParameters, Integer>> intCommandHandlers = Map.ofEntries(
Map.entry(lvlRegex, GiveItemParameters::setLvl),
Map.entry(refineRegex, GiveItemParameters::setRefinement),
Map.entry(amountRegex, GiveItemParameters::setAmount),
Map.entry(constellationRegex, GiveItemParameters::setConstellation),
Map.entry(skillLevelRegex, GiveItemParameters::setSkillLevel)
);
private static class GiveItemParameters {
public int id;
@Setter public int lvl = 0;
@Setter public int amount = 1;
@Setter public int refinement = 1;
@Setter public int constellation = -1;
@Setter public int skillLevel = 1;
public int mainPropId = -1;
public List<Integer> appendPropIdList;
public ItemData data;
public AvatarData avatarData;
public GiveAllType giveAllType = GiveAllType.NONE;
private static Avatar makeAvatar(
AvatarData avatarData, int level, int promoteLevel, int constellation, int skillLevel) {
Avatar avatar = new Avatar(avatarData);
avatar.setLevel(level);
avatar.setPromoteLevel(promoteLevel);
avatar
.getSkillDepot()
.getSkillsAndEnergySkill()
.forEach(id -> avatar.setSkillLevel(id, skillLevel));
avatar.forceConstellationLevel(constellation);
avatar.recalcStats(true);
avatar.save();
return avatar;
}
private GiveItemParameters parseArgs(Player sender, List<String> args) throws IllegalArgumentException {
private static void giveAllAvatars(Player player, GiveItemParameters param) {
int promoteLevel = Avatar.getMinPromoteLevel(param.lvl);
if (param.constellation < 0 || param.constellation > 6)
param.constellation =
6; // constellation's default is -1 so if no parameters set for constellations it'll
// automatically be 6
for (AvatarData avatarData : GameData.getAvatarDataMap().values()) {
int id = avatarData.getId();
if (id < 10000002 || id >= 11000000) continue; // Exclude test avatars
// Don't try to add each avatar to the current team
player.addAvatar(
makeAvatar(avatarData, param.lvl, promoteLevel, param.constellation, param.skillLevel),
false);
}
}
private static List<GameItem> makeUnstackableItems(GiveItemParameters param) {
int promoteLevel = GameItem.getMinPromoteLevel(param.lvl);
int totalExp = 0;
if (param.data.getItemType() == ItemType.ITEM_WEAPON) {
int rankLevel = param.data.getRankLevel();
for (int i = 1; i < param.lvl; i++) totalExp += GameData.getWeaponExpRequired(rankLevel, i);
}
List<GameItem> items = new ArrayList<>(param.amount);
for (int i = 0; i < param.amount; i++) {
GameItem item = new GameItem(param.data);
item.setLevel(param.lvl);
if (item.getItemType() == ItemType.ITEM_WEAPON) {
item.setPromoteLevel(promoteLevel);
item.setTotalExp(totalExp);
item.setRefinement(param.refinement - 1); // Actual refinement data is 0..4 not 1..5
}
items.add(item);
}
return items;
}
private static List<GameItem> makeArtifacts(GiveItemParameters param) {
param.lvl = Math.min(param.lvl, param.data.getMaxLevel());
int rank = param.data.getRankLevel();
int totalExp = 0;
for (int i = 1; i < param.lvl; i++) totalExp += GameData.getRelicExpRequired(rank, i);
List<GameItem> items = new ArrayList<>(param.amount);
for (int i = 0; i < param.amount; i++) {
// Create item for the artifact.
GameItem item = new GameItem(param.data);
item.setLevel(param.lvl);
item.setTotalExp(totalExp);
int numAffixes = param.data.getAppendPropNum() + (param.lvl - 1) / 4;
if (param.mainPropId > 0) // Keep random mainProp if we didn't specify one
item.setMainPropId(param.mainPropId);
if (param.appendPropIdList != null) {
item.getAppendPropIdList().clear();
item.getAppendPropIdList().addAll(param.appendPropIdList);
}
// If we didn't include enough substats, top them up to the appropriate level at random
item.addAppendProps(numAffixes - item.getAppendPropIdList().size());
items.add(item);
}
return items;
}
private static int getArtifactMainProp(ItemData itemData, FightProperty prop)
throws IllegalArgumentException {
if (prop != FightProperty.FIGHT_PROP_NONE)
for (ReliquaryMainPropData data :
GameDepot.getRelicMainPropList(itemData.getMainPropDepotId()))
if (data.getWeight() > 0 && data.getFightProp() == prop) return data.getId();
throw new IllegalArgumentException();
}
private static List<Integer> getArtifactAffixes(ItemData itemData, FightProperty prop)
throws IllegalArgumentException {
if (prop == FightProperty.FIGHT_PROP_NONE) {
throw new IllegalArgumentException();
}
List<Integer> affixes = new ArrayList<>();
for (ReliquaryAffixData data : GameDepot.getRelicAffixList(itemData.getAppendPropDepotId())) {
if (data.getWeight() > 0 && data.getFightProp() == prop) {
affixes.add(data.getId());
}
}
return affixes;
}
private static int getAppendPropId(String substatText, ItemData itemData)
throws IllegalArgumentException {
// If the given substat text is an integer, we just use that as the append prop ID.
try {
return Integer.parseInt(substatText);
} catch (NumberFormatException ignored) {
// If the argument was not an integer, we try to determine
// the append prop ID from the given text + artifact information.
// A substat string has the format `substat_tier`, with the
// `_tier` part being optional, defaulting to the maximum.
String[] substatArgs = substatText.split("_");
String substatType = substatArgs[0];
int substatTier = 4;
if (substatArgs.length > 1) {
substatTier = Integer.parseInt(substatArgs[1]);
}
List<Integer> substats =
getArtifactAffixes(itemData, FightProperty.getPropByShortName(substatType));
if (substats.isEmpty()) {
throw new IllegalArgumentException();
}
substatTier -= 1; // 1-indexed to 0-indexed
substatTier = Math.min(Math.max(0, substatTier), substats.size() - 1);
return substats.get(substatTier);
}
}
private static void parseRelicArgs(GiveItemParameters param, List<String> args)
throws IllegalArgumentException {
// Get the main stat from the arguments.
// If the given argument is an integer, we use that.
// If not, we check if the argument string is in the main prop map.
String mainPropIdString = args.remove(0);
try {
param.mainPropId = Integer.parseInt(mainPropIdString);
} catch (NumberFormatException ignored) {
// This can in turn throw an exception which we don't want to catch here.
param.mainPropId =
getArtifactMainProp(param.data, FightProperty.getPropByShortName(mainPropIdString));
}
// Get substats.
param.appendPropIdList = new ArrayList<>();
// Every remaining argument is a substat.
for (String prop : args) {
// The substat syntax permits specifying a number of rolls for the given
// substat. Split the string into stat and number if that is the case here.
String[] arr = prop.split(",");
prop = arr[0];
int n = 1;
if (arr.length > 1) {
n = Math.min(Integer.parseInt(arr[1]), 200);
}
// Determine the substat ID.
int appendPropId = getAppendPropId(prop, param.data);
// Add the current substat.
for (int i = 0; i < n; i++) {
param.appendPropIdList.add(appendPropId);
}
}
}
private static void addItemsChunked(Player player, List<GameItem> items, int packetSize) {
// Send the items in multiple packets
int lastIdx = items.size() - 1;
for (int i = 0; i <= lastIdx; i += packetSize) {
player.getInventory().addItems(items.subList(i, Math.min(i + packetSize, lastIdx)));
}
}
private static void giveAllMats(Player player, GiveItemParameters param) {
List<GameItem> itemList = new ArrayList<>();
for (ItemData itemdata : GameData.getItemDataMap().values()) {
int id = itemdata.getId();
if (id < 100_000) continue; // Nothing meaningful below this
if (ILLEGAL_ITEMS.contains(id)) continue;
if (itemdata.isEquip()) continue;
GameItem item = new GameItem(itemdata);
item.setCount(param.amount);
itemList.add(item);
}
addItemsChunked(player, itemList, 100);
}
private static void giveAllWeapons(Player player, GiveItemParameters param) {
int promoteLevel = GameItem.getMinPromoteLevel(param.lvl);
int quantity = Math.min(param.amount, 5);
int refinement = param.refinement - 1;
List<GameItem> itemList = new ArrayList<>();
for (ItemData itemdata : GameData.getItemDataMap().values()) {
int id = itemdata.getId();
if (id < 11100 || id > 16000) continue; // All extant weapons are within this range
if (ILLEGAL_WEAPONS.contains(id)) continue;
if (!itemdata.isEquip()) continue;
if (itemdata.getItemType() != ItemType.ITEM_WEAPON) continue;
for (int i = 0; i < quantity; i++) {
GameItem item = new GameItem(itemdata);
item.setLevel(param.lvl);
item.setPromoteLevel(promoteLevel);
item.setRefinement(refinement);
itemList.add(item);
}
}
addItemsChunked(player, itemList, 100);
}
private static void giveAll(Player player, GiveItemParameters param) {
giveAllAvatars(player, param);
giveAllMats(player, param);
giveAllWeapons(player, param);
}
private GiveItemParameters parseArgs(Player sender, List<String> args)
throws IllegalArgumentException {
GiveItemParameters param = new GiveItemParameters();
// Extract any tagged arguments (e.g. "lv90", "x100", "r5")
@@ -74,7 +287,7 @@ public final class GiveCommand implements CommandHandler {
// At this point, first remaining argument MUST be itemId/avatarId
if (args.size() < 1) {
sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar
sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar
throw new IllegalArgumentException();
}
String id = args.remove(0);
@@ -108,7 +321,9 @@ public final class GiveCommand implements CommandHandler {
param.avatarData = GameData.getAvatarDataMap().get(param.id - 1000 + 10_000_000);
isRelic = ((param.data != null) && (param.data.getItemType() == ItemType.ITEM_RELIQUARY));
if (!isRelic && !args.isEmpty() && (param.amount == 1)) { // A concession for the people that truly hate [x<amount>].
if (!isRelic
&& !args.isEmpty()
&& (param.amount == 1)) { // A concession for the people that truly hate [x<amount>].
try {
param.amount = Integer.parseInt(args.remove(0));
} catch (NumberFormatException e) {
@@ -126,7 +341,7 @@ public final class GiveCommand implements CommandHandler {
if (param.lvl < 0) param.lvl = 0;
if (param.lvl > 20) param.lvl = 20;
param.lvl += 1;
if (illegalRelicIds.contains(param.id))
if (ILLEGAL_RELICS.contains(param.id))
CommandHandler.sendTranslatedMessage(sender, "commands.give.illegal_relic");
} else {
// Suitable for Avatars and Weapons
@@ -186,7 +401,8 @@ public final class GiveCommand implements CommandHandler {
if (param.avatarData != null) {
Avatar avatar = makeAvatar(param);
targetPlayer.addAvatar(avatar);
CommandHandler.sendTranslatedMessage(sender, "commands.give.given_avatar", param.id, param.lvl, targetPlayer.getUid());
CommandHandler.sendTranslatedMessage(
sender, "commands.give.given_avatar", param.id, param.lvl, targetPlayer.getUid());
return;
}
// If it's not an avatar, it needs to be a valid item
@@ -197,252 +413,58 @@ public final class GiveCommand implements CommandHandler {
switch (param.data.getItemType()) {
case ITEM_WEAPON:
targetPlayer.getInventory().addItems(makeUnstackableItems(param), ActionReason.SubfieldDrop);
CommandHandler.sendTranslatedMessage(sender, "commands.give.given_with_level_and_refinement", param.id, param.lvl, param.refinement, param.amount, targetPlayer.getUid());
targetPlayer
.getInventory()
.addItems(makeUnstackableItems(param), ActionReason.SubfieldDrop);
CommandHandler.sendTranslatedMessage(
sender,
"commands.give.given_with_level_and_refinement",
param.id,
param.lvl,
param.refinement,
param.amount,
targetPlayer.getUid());
return;
case ITEM_RELIQUARY:
targetPlayer.getInventory().addItems(makeArtifacts(param), ActionReason.SubfieldDrop);
CommandHandler.sendTranslatedMessage(sender, "commands.give.given_level", param.id, param.lvl, param.amount, targetPlayer.getUid());
CommandHandler.sendTranslatedMessage(
sender,
"commands.give.given_level",
param.id,
param.lvl,
param.amount,
targetPlayer.getUid());
return;
default:
targetPlayer.getInventory().addItem(new GameItem(param.data, param.amount), ActionReason.SubfieldDrop);
CommandHandler.sendTranslatedMessage(sender, "commands.give.given", param.amount, param.id, targetPlayer.getUid());
return;
targetPlayer
.getInventory()
.addItem(new GameItem(param.data, param.amount), ActionReason.SubfieldDrop);
CommandHandler.sendTranslatedMessage(
sender, "commands.give.given", param.amount, param.id, targetPlayer.getUid());
}
} catch (IllegalArgumentException ignored) {
return;
}
}
private static Avatar makeAvatar(GiveItemParameters param) {
return makeAvatar(param.avatarData, param.lvl, Avatar.getMinPromoteLevel(param.lvl), param.constellation, param.skillLevel);
private enum GiveAllType {
NONE,
ALL,
WEAPONS,
MATS,
AVATARS
}
private static Avatar makeAvatar(AvatarData avatarData, int level, int promoteLevel, int constellation, int skillLevel) {
Avatar avatar = new Avatar(avatarData);
avatar.setLevel(level);
avatar.setPromoteLevel(promoteLevel);
avatar.getSkillDepot().getSkillsAndEnergySkill().forEach(id -> avatar.setSkillLevel(id, skillLevel));
avatar.forceConstellationLevel(constellation);
avatar.recalcStats(true);
avatar.save();
return avatar;
private static class GiveItemParameters {
public int id;
@Setter public int lvl = 0;
@Setter public int amount = 1;
@Setter public int refinement = 1;
@Setter public int constellation = -1;
@Setter public int skillLevel = 1;
public int mainPropId = -1;
public List<Integer> appendPropIdList;
public ItemData data;
public AvatarData avatarData;
public GiveAllType giveAllType = GiveAllType.NONE;
}
private static void giveAllAvatars(Player player, GiveItemParameters param) {
int promoteLevel = Avatar.getMinPromoteLevel(param.lvl);
if (param.constellation < 0 || param.constellation > 6) param.constellation = 6; // constellation's default is -1 so if no parameters set for constellations it'll automatically be 6
for (AvatarData avatarData : GameData.getAvatarDataMap().values()) {
int id = avatarData.getId();
if (id < 10000002 || id >= 11000000) continue; // Exclude test avatars
// Don't try to add each avatar to the current team
player.addAvatar(makeAvatar(avatarData, param.lvl, promoteLevel, param.constellation, param.skillLevel), false);
}
}
private static List<GameItem> makeUnstackableItems(GiveItemParameters param) {
int promoteLevel = GameItem.getMinPromoteLevel(param.lvl);
int totalExp = 0;
if (param.data.getItemType() == ItemType.ITEM_WEAPON) {
int rankLevel = param.data.getRankLevel();
for (int i = 1; i < param.lvl; i++)
totalExp += GameData.getWeaponExpRequired(rankLevel, i);
}
List<GameItem> items = new ArrayList<>(param.amount);
for (int i = 0; i < param.amount; i++) {
GameItem item = new GameItem(param.data);
item.setLevel(param.lvl);
if (item.getItemType() == ItemType.ITEM_WEAPON) {
item.setPromoteLevel(promoteLevel);
item.setTotalExp(totalExp);
item.setRefinement(param.refinement - 1); // Actual refinement data is 0..4 not 1..5
}
items.add(item);
}
return items;
}
private static List<GameItem> makeArtifacts(GiveItemParameters param) {
param.lvl = Math.min(param.lvl, param.data.getMaxLevel());
int rank = param.data.getRankLevel();
int totalExp = 0;
for (int i = 1; i < param.lvl; i++)
totalExp += GameData.getRelicExpRequired(rank, i);
List<GameItem> items = new ArrayList<>(param.amount);
for (int i = 0; i < param.amount; i++) {
// Create item for the artifact.
GameItem item = new GameItem(param.data);
item.setLevel(param.lvl);
item.setTotalExp(totalExp);
int numAffixes = param.data.getAppendPropNum() + (param.lvl-1)/4;
if (param.mainPropId > 0) // Keep random mainProp if we didn't specify one
item.setMainPropId(param.mainPropId);
if (param.appendPropIdList != null) {
item.getAppendPropIdList().clear();
item.getAppendPropIdList().addAll(param.appendPropIdList);
}
// If we didn't include enough substats, top them up to the appropriate level at random
item.addAppendProps(numAffixes - item.getAppendPropIdList().size());
items.add(item);
}
return items;
}
private static int getArtifactMainProp(ItemData itemData, FightProperty prop) throws IllegalArgumentException {
if (prop != FightProperty.FIGHT_PROP_NONE)
for (ReliquaryMainPropData data : GameDepot.getRelicMainPropList(itemData.getMainPropDepotId()))
if (data.getWeight() > 0 && data.getFightProp() == prop)
return data.getId();
throw new IllegalArgumentException();
}
private static List<Integer> getArtifactAffixes(ItemData itemData, FightProperty prop) throws IllegalArgumentException {
if (prop == FightProperty.FIGHT_PROP_NONE) {
throw new IllegalArgumentException();
}
List<Integer> affixes = new ArrayList<>();
for (ReliquaryAffixData data : GameDepot.getRelicAffixList(itemData.getAppendPropDepotId())) {
if (data.getWeight() > 0 && data.getFightProp() == prop) {
affixes.add(data.getId());
}
}
return affixes;
}
private static int getAppendPropId(String substatText, ItemData itemData) throws IllegalArgumentException {
// If the given substat text is an integer, we just use that as the append prop ID.
try {
return Integer.parseInt(substatText);
} catch (NumberFormatException ignored) {
// If the argument was not an integer, we try to determine
// the append prop ID from the given text + artifact information.
// A substat string has the format `substat_tier`, with the
// `_tier` part being optional, defaulting to the maximum.
String[] substatArgs = substatText.split("_");
String substatType = substatArgs[0];
int substatTier = 4;
if (substatArgs.length > 1) {
substatTier = Integer.parseInt(substatArgs[1]);
}
List<Integer> substats = getArtifactAffixes(itemData, FightProperty.getPropByShortName(substatType));
if (substats.isEmpty()) {
throw new IllegalArgumentException();
}
substatTier -= 1; // 1-indexed to 0-indexed
substatTier = Math.min(Math.max(0, substatTier), substats.size() - 1);
return substats.get(substatTier);
}
}
private static void parseRelicArgs(GiveItemParameters param, List<String> args) throws IllegalArgumentException {
// Get the main stat from the arguments.
// If the given argument is an integer, we use that.
// If not, we check if the argument string is in the main prop map.
String mainPropIdString = args.remove(0);
try {
param.mainPropId = Integer.parseInt(mainPropIdString);
} catch (NumberFormatException ignored) {
// This can in turn throw an exception which we don't want to catch here.
param.mainPropId = getArtifactMainProp(param.data, FightProperty.getPropByShortName(mainPropIdString));
}
// Get substats.
param.appendPropIdList = new ArrayList<>();
// Every remaining argument is a substat.
for (String prop : args) {
// The substat syntax permits specifying a number of rolls for the given
// substat. Split the string into stat and number if that is the case here.
String[] arr = prop.split(",");
prop = arr[0];
int n = 1;
if (arr.length > 1) {
n = Math.min(Integer.parseInt(arr[1]), 200);
}
// Determine the substat ID.
int appendPropId = getAppendPropId(prop, param.data);
// Add the current substat.
for (int i = 0; i < n; i++) {
param.appendPropIdList.add(appendPropId);
}
}
}
private static void addItemsChunked(Player player, List<GameItem> items, int packetSize) {
// Send the items in multiple packets
int lastIdx = items.size() - 1;
for (int i = 0; i <= lastIdx; i += packetSize) {
player.getInventory().addItems(items.subList(i, Math.min(i + packetSize, lastIdx)));
}
}
private static void giveAllMats(Player player, GiveItemParameters param) {
List<GameItem> itemList = new ArrayList<>();
for (ItemData itemdata : GameData.getItemDataMap().values()) {
int id = itemdata.getId();
if (id < 100_000) continue; // Nothing meaningful below this
if (illegalItemIds.contains(id)) continue;
if (itemdata.isEquip()) continue;
GameItem item = new GameItem(itemdata);
item.setCount(param.amount);
itemList.add(item);
}
addItemsChunked(player, itemList, 100);
}
private static void giveAllWeapons(Player player, GiveItemParameters param) {
int promoteLevel = GameItem.getMinPromoteLevel(param.lvl);
int quantity = Math.min(param.amount, 5);
int refinement = param.refinement - 1;
List<GameItem> itemList = new ArrayList<>();
for (ItemData itemdata : GameData.getItemDataMap().values()) {
int id = itemdata.getId();
if (id < 11100 || id > 16000) continue; // All extant weapons are within this range
if (illegalWeaponIds.contains(id)) continue;
if (!itemdata.isEquip()) continue;
if (itemdata.getItemType() != ItemType.ITEM_WEAPON) continue;
for (int i = 0; i < quantity; i++) {
GameItem item = new GameItem(itemdata);
item.setLevel(param.lvl);
item.setPromoteLevel(promoteLevel);
item.setRefinement(refinement);
itemList.add(item);
}
}
addItemsChunked(player, itemList, 100);
}
private static void giveAll(Player player, GiveItemParameters param) {
giveAllAvatars(player, param);
giveAllMats(player, param);
giveAllWeapons(player, param);
}
private static final SparseSet illegalWeaponIds = new SparseSet("""
10000-10008, 11411, 11506-11508, 12505, 12506, 12508, 12509,
13503, 13506, 14411, 14503, 14505, 14508, 15504-15506
""");
private static final SparseSet illegalRelicIds = new SparseSet("""
20001, 23300-23340, 23383-23385, 78310-78554, 99310-99554
""");
private static final SparseSet illegalItemIds = new SparseSet("""
100086, 100087, 100100-101000, 101106-101110, 101306, 101500-104000,
105001, 105004, 106000-107000, 107011, 108000, 109000-110000,
115000-130000, 200200-200899, 220050, 220054
""");
}

View File

@@ -0,0 +1,72 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.SceneGroupInstance;
import java.util.List;
@Command(
label = "group",
aliases = {"g"},
usage = {"(refresh) [<groupId>] [<suiteId>]"},
permission = "player.group",
permissionTargeted = "player.group.others")
public final class GroupCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (args.isEmpty()) {
return;
}
String cmd = args.remove(0).toLowerCase();
int groupId = 0;
int suiteId = 0;
switch (args.size()) {
case 2:
try {
suiteId = Integer.parseInt(args.get(1));
} catch (Exception e) {
CommandHandler.sendMessage(sender, translate(sender, "commands.group.invalid_suiteid"));
return;
} // Fallthrough
case 1:
try {
groupId = Integer.parseInt(args.get(0));
} catch (Exception e) {
CommandHandler.sendMessage(sender, translate(sender, "commands.group.invalid_groupid"));
return;
}
break;
default:
sendUsageMessage(sender);
return;
}
switch (cmd) {
case "refresh" -> {
SceneGroupInstance groupInstance =
targetPlayer.getScene().getScriptManager().getGroupInstanceById(groupId);
if (groupInstance == null) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.group.group_not_found", groupId));
return;
}
if (args.size() >= 2) {
targetPlayer.getScene().getScriptManager().refreshGroup(groupInstance, suiteId, false);
} else {
targetPlayer.getScene().getScriptManager().refreshGroup(groupInstance);
}
CommandHandler.sendMessage(sender, translate(sender, "commands.group.refreshed", groupId));
}
default -> {
sendUsageMessage(sender);
}
}
}
}

View File

@@ -1,32 +1,44 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.server.packet.send.PacketAvatarFightPropUpdateNotify;
import emu.grasscutter.server.packet.send.PacketAvatarLifeStateChangeNotify;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "heal", aliases = {"h"}, permission = "player.heal", permissionTargeted = "player.heal.others")
@Command(
label = "heal",
aliases = {"h"},
permission = "player.heal",
permissionTargeted = "player.heal.others")
public final class HealCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
targetPlayer.getTeamManager().getActiveTeam().forEach(entity -> {
boolean isAlive = entity.isAlive();
entity.setFightProperty(
FightProperty.FIGHT_PROP_CUR_HP,
entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP)
);
entity.getWorld().broadcastPacket(new PacketAvatarFightPropUpdateNotify(entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP));
if (!isAlive) {
entity.getWorld().broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar()));
}
});
targetPlayer
.getTeamManager()
.getActiveTeam()
.forEach(
entity -> {
boolean isAlive = entity.isAlive();
entity.setFightProperty(
FightProperty.FIGHT_PROP_CUR_HP,
entity.getFightProperty(FightProperty.FIGHT_PROP_MAX_HP));
entity
.getWorld()
.broadcastPacket(
new PacketAvatarFightPropUpdateNotify(
entity.getAvatar(), FightProperty.FIGHT_PROP_CUR_HP));
if (!isAlive) {
entity
.getWorld()
.broadcastPacket(new PacketAvatarLifeStateChangeNotify(entity.getAvatar()));
}
});
CommandHandler.sendMessage(sender, translate(sender, "commands.heal.success"));
}
}

View File

@@ -1,25 +1,30 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.command.CommandMap;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "help", usage = {"[<command>]"}, targetRequirement = Command.TargetRequirement.NONE)
@Command(
label = "help",
usage = {"[<command>]"},
targetRequirement = Command.TargetRequirement.NONE)
public final class HelpCommand implements CommandHandler {
private final boolean SHOW_COMMANDS_WITHOUT_PERMISSIONS = false; // TODO: Make this into a server config key
private final boolean SHOW_COMMANDS_WITHOUT_PERMISSIONS =
false; // TODO: Make this into a server config key
private String createCommand(Player player, CommandHandler command, List<String> args) {
StringBuilder builder = new StringBuilder(command.getLabel())
.append(" - ")
.append(command.getDescriptionString(player))
.append("\n\t")
.append(command.getUsageString(player, args.toArray(new String[0])));
StringBuilder builder =
new StringBuilder(command.getLabel())
.append(" - ")
.append(command.getDescriptionString(player))
.append("\n\t")
.append(command.getUsageString(player, args.toArray(new String[0])));
Command annotation = command.getClass().getAnnotation(Command.class);
if (annotation.aliases().length > 0) {
@@ -38,7 +43,9 @@ public final class HelpCommand implements CommandHandler {
if (!annotation.permissionTargeted().isEmpty()) {
String permissionTargeted = annotation.permissionTargeted();
builder.append(" ").append(translate(player, "commands.help.tip_permission_targeted", permissionTargeted));
builder
.append(" ")
.append(translate(player, "commands.help.tip_permission_targeted", permissionTargeted));
}
return builder.toString();
}
@@ -50,14 +57,17 @@ public final class HelpCommand implements CommandHandler {
List<String> commands = new ArrayList<>();
List<String> commands_no_permission = new ArrayList<>();
if (args.isEmpty()) {
commandMap.getHandlers().forEach((key, command) -> {
Command annotation = command.getClass().getAnnotation(Command.class);
if (player == null || account.hasPermission(annotation.permission())) {
commands.add(createCommand(player, command, args));
} else if (SHOW_COMMANDS_WITHOUT_PERMISSIONS) {
commands_no_permission.add(createCommand(player, command, args));
}
});
commandMap
.getHandlers()
.forEach(
(key, command) -> {
Command annotation = command.getClass().getAnnotation(Command.class);
if (player == null || account.hasPermission(annotation.permission())) {
commands.add(createCommand(player, command, args));
} else if (SHOW_COMMANDS_WITHOUT_PERMISSIONS) {
commands_no_permission.add(createCommand(player, command, args));
}
});
CommandHandler.sendTranslatedMessage(player, "commands.help.available_commands");
} else {
String command_str = args.remove(0).toLowerCase();

View File

@@ -3,21 +3,30 @@ package emu.grasscutter.command.commands;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import java.util.List;
@Command(label = "kick", aliases = {"restart"}, permissionTargeted = "server.kick")
@Command(
label = "kick",
aliases = {"restart"},
permissionTargeted = "server.kick")
public final class KickCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (sender != null) {
CommandHandler.sendTranslatedMessage(sender, "commands.kick.player_kick_player",
sender.getUid(), sender.getAccount().getUsername(),
targetPlayer.getUid(), targetPlayer.getAccount().getUsername());
CommandHandler.sendTranslatedMessage(
sender,
"commands.kick.player_kick_player",
sender.getUid(),
sender.getAccount().getUsername(),
targetPlayer.getUid(),
targetPlayer.getAccount().getUsername());
} else {
CommandHandler.sendTranslatedMessage(sender, "commands.kick.server_kick_player",
targetPlayer.getUid(), targetPlayer.getAccount().getUsername());
CommandHandler.sendTranslatedMessage(
sender,
"commands.kick.server_kick_player",
targetPlayer.getUid(),
targetPlayer.getAccount().getUsername());
}
targetPlayer.getSession().close();

View File

@@ -1,17 +1,20 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.entity.EntityMonster;
import emu.grasscutter.game.entity.GameEntity;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Scene;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "killall", usage = {"[<sceneId>]"}, permission = "server.killall", permissionTargeted = "server.killall.others")
@Command(
label = "killall",
usage = {"[<sceneId>]"},
permission = "server.killall",
permissionTargeted = "server.killall.others")
public final class KillAllCommand implements CommandHandler {
@Override
@@ -32,16 +35,20 @@ public final class KillAllCommand implements CommandHandler {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error"));
}
if (scene == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.killall.scene_not_found_in_player_world"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.killall.scene_not_found_in_player_world"));
return;
}
// Separate into list to avoid concurrency issue
final Scene sceneF = scene;
List<GameEntity> toKill = sceneF.getEntities().values().stream()
.filter(entity -> entity instanceof EntityMonster)
.toList();
List<GameEntity> toKill =
sceneF.getEntities().values().stream()
.filter(entity -> entity instanceof EntityMonster)
.toList();
toKill.forEach(entity -> sceneF.killEntity(entity, 0));
CommandHandler.sendMessage(sender, translate(sender, "commands.killall.kill_monsters_in_scene", toKill.size(), scene.getId()));
CommandHandler.sendMessage(
sender,
translate(sender, "commands.killall.kill_monsters_in_scene", toKill.size(), scene.getId()));
}
}

View File

@@ -1,5 +1,7 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.entity.EntityAvatar;
@@ -8,12 +10,13 @@ import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.props.LifeState;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
import emu.grasscutter.server.packet.send.PacketLifeStateChangeNotify;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "killCharacter", aliases = {"suicide", "kill"}, permission = "player.killcharacter", permissionTargeted = "player.killcharacter.others")
@Command(
label = "killCharacter",
aliases = {"suicide", "kill"},
permission = "player.killcharacter",
permissionTargeted = "player.killcharacter.others")
public final class KillCharacterCommand implements CommandHandler {
@Override
@@ -21,12 +24,18 @@ public final class KillCharacterCommand implements CommandHandler {
EntityAvatar entity = targetPlayer.getTeamManager().getCurrentAvatarEntity();
entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, 0f);
// Packets
entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
entity.getWorld().broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD));
entity
.getWorld()
.broadcastPacket(
new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP));
entity
.getWorld()
.broadcastPacket(new PacketLifeStateChangeNotify(0, entity, LifeState.LIFE_DEAD));
// remove
targetPlayer.getScene().removeEntity(entity);
entity.onDeath(0);
CommandHandler.sendMessage(sender, translate(sender, "commands.killCharacter.success", targetPlayer.getNickname()));
CommandHandler.sendMessage(
sender, translate(sender, "commands.killCharacter.success", targetPlayer.getNickname()));
}
}

View File

@@ -1,17 +1,20 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.utils.Utils;
import java.util.List;
import java.util.Locale;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "language", usage = {"[<language code>]"}, aliases = {"lang"}, targetRequirement = Command.TargetRequirement.NONE)
@Command(
label = "language",
usage = {"[<language code>]"},
aliases = {"lang"},
targetRequirement = Command.TargetRequirement.NONE)
public final class LanguageCommand implements CommandHandler {
@Override
@@ -20,11 +23,11 @@ public final class LanguageCommand implements CommandHandler {
String curLangCode = null;
if (sender != null) {
curLangCode = Utils.getLanguageCode(sender.getAccount().getLocale());
}
else {
} else {
curLangCode = Grasscutter.getLanguage().getLanguageCode();
}
CommandHandler.sendMessage(sender, translate(sender, "commands.language.current_language", curLangCode));
CommandHandler.sendMessage(
sender, translate(sender, "commands.language.current_language", curLangCode));
return;
}
@@ -37,8 +40,7 @@ public final class LanguageCommand implements CommandHandler {
var account = sender.getAccount();
account.setLocale(locale);
account.save();
}
else {
} else {
Grasscutter.setLanguage(languageInst);
var config = Grasscutter.getConfig();
config.language.language = locale;
@@ -46,10 +48,11 @@ public final class LanguageCommand implements CommandHandler {
}
if (!langCode.equals(actualLangCode)) {
CommandHandler.sendMessage(sender, translate(sender, "commands.language.language_not_found", langCode));
CommandHandler.sendMessage(
sender, translate(sender, "commands.language.language_not_found", langCode));
}
CommandHandler.sendMessage(sender, translate(sender, "commands.language.language_changed", actualLangCode));
CommandHandler.sendMessage(
sender, translate(sender, "commands.language.language_changed", actualLangCode));
}
}

View File

@@ -1,16 +1,19 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import java.util.List;
import java.util.Map;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "list", aliases = {"players"}, usage = {"[<UID>]"}, targetRequirement = Command.TargetRequirement.NONE)
@Command(
label = "list",
aliases = {"players"},
usage = {"[<UID>]"},
targetRequirement = Command.TargetRequirement.NONE)
public final class ListCommand implements CommandHandler {
@Override
@@ -22,29 +25,29 @@ public final class ListCommand implements CommandHandler {
needUID = args.get(0).equals("uid");
}
CommandHandler.sendMessage(sender, translate(sender, "commands.list.success", playersMap.size()));
CommandHandler.sendMessage(
sender, translate(sender, "commands.list.success", playersMap.size()));
if (playersMap.size() != 0) {
StringBuilder playerSet = new StringBuilder();
boolean finalNeedUID = needUID;
playersMap.values().forEach(player -> {
playerSet.append(player.getNickname());
playersMap
.values()
.forEach(
player -> {
playerSet.append(player.getNickname());
if (finalNeedUID) {
if (sender != null) {
playerSet.append(" <color=green>(")
.append(player.getUid())
.append(")</color>");
} else {
playerSet.append(" (")
.append(player.getUid())
.append(")");
}
}
if (finalNeedUID) {
if (sender != null) {
playerSet.append(" <color=green>(").append(player.getUid()).append(")</color>");
} else {
playerSet.append(" (").append(player.getUid()).append(")");
}
}
playerSet.append(", ");
});
playerSet.append(", ");
});
String players = playerSet.toString();
CommandHandler.sendMessage(sender, players.substring(0, players.length() - 2));

View File

@@ -1,22 +1,20 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.command.Command.TargetRequirement;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "permission", usage = {
"add <permission>",
"remove <permission>",
"clear",
"list"
}, permission = "permission", targetRequirement = TargetRequirement.PLAYER)
@Command(
label = "permission",
usage = {"add <permission>", "remove <permission>", "clear", "list"},
permission = "permission",
targetRequirement = TargetRequirement.PLAYER)
public final class PermissionCommand implements CommandHandler {
@Override
@@ -52,12 +50,15 @@ public final class PermissionCommand implements CommandHandler {
sendUsageMessage(sender);
} else if (account.addPermission(permission)) {
CommandHandler.sendMessage(sender, translate(sender, "commands.permission.add"));
} else CommandHandler.sendMessage(sender, translate(sender, "commands.permission.has_error"));
} else
CommandHandler.sendMessage(sender, translate(sender, "commands.permission.has_error"));
break;
case "remove":
if (account.removePermission(permission)) {
CommandHandler.sendMessage(sender, translate(sender, "commands.permission.remove"));
} else CommandHandler.sendMessage(sender, translate(sender, "commands.permission.not_have_error"));
} else
CommandHandler.sendMessage(
sender, translate(sender, "commands.permission.not_have_error"));
break;
case "clear":
account.clearPermission();

View File

@@ -3,18 +3,27 @@ package emu.grasscutter.command.commands;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.utils.Position;
import emu.grasscutter.game.world.Position;
import java.util.List;
@Command(label = "position", aliases = {"pos"})
@Command(
label = "position",
aliases = {"pos"})
public final class PositionCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
Position pos = targetPlayer.getPosition();
Position rot = targetPlayer.getRotation();
CommandHandler.sendTranslatedMessage(sender, "commands.position.success",
pos.getX(), pos.getY(), pos.getZ(), rot.getX(), rot.getY(), rot.getZ(), targetPlayer.getSceneId());
CommandHandler.sendTranslatedMessage(
sender,
"commands.position.success",
pos.getX(),
pos.getY(),
pos.getZ(),
rot.getX(),
rot.getY(),
rot.getZ(),
targetPlayer.getSceneId());
}
}

View File

@@ -1,19 +1,19 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.quest.GameQuest;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "quest",
aliases = {"q"},
usage = {"(add|finish) [<questId>]"},
permission = "player.quest",
permissionTargeted = "player.quest.others")
@Command(
label = "quest",
aliases = {"q"},
usage = {"(add|finish) [<questId>]"},
permission = "player.quest",
permissionTargeted = "player.quest.others")
public final class QuestCommand implements CommandHandler {
@Override
@@ -56,9 +56,82 @@ public final class QuestCommand implements CommandHandler {
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.finished", questId));
}
default -> {
sendUsageMessage(sender);
case "running" -> {
var quest = targetPlayer.getQuestManager().getQuestById(questId);
if (quest == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.not_found"));
return;
}
CommandHandler.sendMessage(
sender,
translate(
sender,
"commands.quest.running",
questId,
translate(
sender,
switch (quest.state) {
case QUEST_STATE_NONE, NONE -> "commands.quest.state.none";
case QUEST_STATE_UNSTARTED, UNSTARTED -> "commands.quest.state.unstarted";
case QUEST_STATE_UNFINISHED, UNFINISHED -> "commands.quest.state.unfinished";
case QUEST_STATE_FINISHED, FINISHED -> "commands.quest.state.finished";
case QUEST_STATE_FAILED, FAILED -> "commands.quest.state.failed";
}),
quest.getState().getValue()));
}
case "talking" -> {
var mainQuest = targetPlayer.getQuestManager().getMainQuestByTalkId(questId);
if (mainQuest == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.not_found"));
return;
}
var talk = mainQuest.getTalks().get(questId);
CommandHandler.sendMessage(
sender,
translate(
sender,
"commands.quest.talking",
questId,
talk == null
? translate(sender, "commands.quest.state.not_exists")
: translate(sender, "commands.quest.state.exists"),
mainQuest.getParentQuestId(),
mainQuest.getState().getValue()));
}
case "dungeons" -> {
var dungeons = targetPlayer.getPlayerProgress().getCompletedDungeons();
CommandHandler.sendMessage(
sender,
"Dungeons completed: "
+ String.join(", ", dungeons.intStream().mapToObj(String::valueOf).toList()));
}
case "debug" -> {
var loggedQuests = targetPlayer.getQuestManager().getLoggedQuests();
var shouldAdd = !loggedQuests.contains(questId);
if (shouldAdd) loggedQuests.add(questId);
else loggedQuests.remove(questId);
CommandHandler.sendMessage(
sender,
"Quest %s will %s."
.formatted(questId, shouldAdd ? "now be logged" : "no longer be logged"));
}
case "triggers" -> {
var quest = targetPlayer.getQuestManager().getQuestById(questId);
if (quest == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.quest.not_found"));
return;
}
CommandHandler.sendMessage(
sender,
"Triggers registered for %s: %s."
.formatted(questId, String.join(", ", quest.getTriggers().keySet())));
}
default -> this.sendUsageMessage(sender);
}
}
}

View File

@@ -1,15 +1,17 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "reload", permission = "server.reload", targetRequirement = Command.TargetRequirement.NONE)
@Command(
label = "reload",
permission = "server.reload",
targetRequirement = Command.TargetRequirement.NONE)
public final class ReloadCommand implements CommandHandler {
@Override
@@ -19,7 +21,6 @@ public final class ReloadCommand implements CommandHandler {
Grasscutter.loadConfig();
Grasscutter.loadLanguage();
Grasscutter.getGameServer().getGachaSystem().load();
Grasscutter.getGameServer().getDropSystem().load();
Grasscutter.getGameServer().getShopSystem().load();
CommandHandler.sendMessage(sender, translate(sender, "commands.reload.reload_done"));

View File

@@ -1,21 +1,20 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.player.Player;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(
label = "resetConst",
aliases = {"resetconstellation"},
usage = "[all]",
permission = "player.resetconstellation",
permissionTargeted = "player.resetconstellation.others")
label = "resetConst",
aliases = {"resetconstellation"},
usage = "[all]",
permission = "player.resetconstellation",
permissionTargeted = "player.resetconstellation.others")
public final class ResetConstCommand implements CommandHandler {
@Override
@@ -32,7 +31,9 @@ public final class ResetConstCommand implements CommandHandler {
Avatar avatar = entity.getAvatar();
this.resetConstellation(avatar);
CommandHandler.sendMessage(sender, translate(sender, "commands.resetConst.success", avatar.getAvatarData().getName()));
CommandHandler.sendMessage(
sender,
translate(sender, "commands.resetConst.success", avatar.getAvatarData().getName()));
}
}

View File

@@ -1,15 +1,17 @@
package emu.grasscutter.command.commands;
import emu.grasscutter.Grasscutter;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "resetShopLimit", aliases = {"resetshop"}, permission = "server.resetshop", permissionTargeted = "server.resetshop.others")
@Command(
label = "resetShopLimit",
aliases = {"resetshop"},
permission = "server.resetshop",
permissionTargeted = "server.resetshop.others")
public final class ResetShopLimitCommand implements CommandHandler {
@Override

View File

@@ -1,32 +1,34 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.database.DatabaseHelper;
import emu.grasscutter.game.mail.Mail;
import emu.grasscutter.game.player.Player;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import static emu.grasscutter.utils.Language.translate;
@SuppressWarnings("ConstantConditions")
@Command(
label = "sendMail",
usage = {"(<userId>|all) [<templateId>]", "help"},
permission = "server.sendmail",
targetRequirement = Command.TargetRequirement.NONE)
label = "sendMail",
usage = {"(<userId>|all) [<templateId>]", "help"},
permission = "server.sendmail",
targetRequirement = Command.TargetRequirement.NONE)
public final class SendMailCommand implements CommandHandler {
// TODO: You should be able to do /sendmail and then just send subsequent messages until you finish
// However, due to the current nature of the command system, I don't think this is possible without rewriting
// TODO: You should be able to do /sendmail and then just send subsequent messages until you
// finish
// However, due to the current nature of the command system, I don't think this is possible
// without rewriting
// the command system (again). For now this will do
// Key = User that is constructing the mail.
private static final HashMap<Integer, MailBuilder> mailBeingConstructed = new HashMap<Integer, MailBuilder>();
private static final HashMap<Integer, MailBuilder> mailBeingConstructed =
new HashMap<Integer, MailBuilder>();
// Yes this is awful and I hate it.
@Override
@@ -52,16 +54,20 @@ public final class SendMailCommand implements CommandHandler {
if (DatabaseHelper.getPlayerByUid(Integer.parseInt(args.get(0))) != null) {
mailBuilder = new MailBuilder(Integer.parseInt(args.get(0)), new Mail());
} else {
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.user_not_exist", args.get(0)));
CommandHandler.sendMessage(
sender, translate(sender, "commands.sendMail.user_not_exist", args.get(0)));
return;
}
}
}
mailBeingConstructed.put(senderId, mailBuilder);
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.start_composition"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.sendMail.start_composition"));
}
case 2 -> CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.templates"));
default -> CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.invalid_arguments"));
case 2 -> CommandHandler.sendMessage(
sender, translate(sender, "commands.sendMail.templates"));
default -> CommandHandler.sendMessage(
sender, translate(sender, "commands.sendMail.invalid_arguments"));
}
} else {
MailBuilder mailBuilder = mailBeingConstructed.get(senderId);
@@ -71,48 +77,67 @@ public final class SendMailCommand implements CommandHandler {
case "stop" -> {
mailBeingConstructed.remove(senderId);
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.send_cancel"));
return;
}
case "finish" -> {
if (mailBuilder.constructionStage == 3) {
if (!mailBuilder.sendToAll) {
Grasscutter.getGameServer().getPlayerByUid(mailBuilder.recipient, true).sendMail(mailBuilder.mail);
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.send_done", mailBuilder.recipient));
Grasscutter.getGameServer()
.getPlayerByUid(mailBuilder.recipient, true)
.sendMail(mailBuilder.mail);
CommandHandler.sendMessage(
sender,
translate(sender, "commands.sendMail.send_done", mailBuilder.recipient));
} else {
DatabaseHelper.getByGameClass(Player.class).forEach(player -> {
var onlinePlayer = Grasscutter.getGameServer().getPlayerByUid(player.getUid(), false);
Objects.requireNonNullElse(onlinePlayer, player).sendMail(mailBuilder.mail);
});
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.send_all_done"));
DatabaseHelper.getByGameClass(Player.class)
.forEach(
player -> {
var onlinePlayer =
Grasscutter.getGameServer().getPlayerByUid(player.getUid(), false);
Objects.requireNonNullElse(onlinePlayer, player)
.sendMail(mailBuilder.mail);
});
CommandHandler.sendMessage(
sender, translate(sender, "commands.sendMail.send_all_done"));
}
mailBeingConstructed.remove(senderId);
} else {
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.not_composition_end", getConstructionArgs(mailBuilder.constructionStage, sender)));
CommandHandler.sendMessage(
sender,
translate(
sender,
"commands.sendMail.not_composition_end",
getConstructionArgs(mailBuilder.constructionStage, sender)));
}
return;
}
case "help" -> {
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.please_use", getConstructionArgs(mailBuilder.constructionStage, sender)));
return;
CommandHandler.sendMessage(
sender,
translate(
sender,
"commands.sendMail.please_use",
getConstructionArgs(mailBuilder.constructionStage, sender)));
}
default -> {
switch (mailBuilder.constructionStage) {
case 0 -> {
String title = String.join(" ", args.subList(0, args.size()));
mailBuilder.mail.mailContent.title = title;
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.set_title", title));
CommandHandler.sendMessage(
sender, translate(sender, "commands.sendMail.set_title", title));
mailBuilder.constructionStage++;
}
case 1 -> {
String contents = String.join(" ", args.subList(0, args.size()));
mailBuilder.mail.mailContent.content = contents;
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.set_contents", contents));
CommandHandler.sendMessage(
sender, translate(sender, "commands.sendMail.set_contents", contents));
mailBuilder.constructionStage++;
}
case 2 -> {
String msgSender = String.join(" ", args.subList(0, args.size()));
mailBuilder.mail.mailContent.sender = msgSender;
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.set_message_sender", msgSender));
CommandHandler.sendMessage(
sender, translate(sender, "commands.sendMail.set_message_sender", msgSender));
mailBuilder.constructionStage++;
}
case 3 -> {
@@ -121,33 +146,38 @@ public final class SendMailCommand implements CommandHandler {
int amount = 1;
int refinement = 0;
switch (args.size()) {
case 4: // <itemId|itemName> [amount] [level] [refinement] // TODO: this requires Mail support but there's no harm leaving it here for now
case 4: // <itemId|itemName> [amount] [level] [refinement] // TODO: this requires
// Mail support but there's no harm leaving it here for now
try {
refinement = Integer.parseInt(args.get(3));
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemRefinement"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.generic.invalid.itemRefinement"));
return;
} // Fallthrough
} // Fallthrough
case 3: // <itemId|itemName> [amount] [level]
try {
lvl = Integer.parseInt(args.get(2));
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemLevel"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.generic.invalid.itemLevel"));
return;
} // Fallthrough
} // Fallthrough
case 2: // <itemId|itemName> [amount]
try {
amount = Integer.parseInt(args.get(1));
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.amount"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.generic.invalid.amount"));
return;
} // Fallthrough
} // Fallthrough
case 1: // <itemId|itemName>
try {
item = Integer.parseInt(args.get(0));
} catch (NumberFormatException ignored) {
// TODO: Parse from item name using GM Handbook.
CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.itemId"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.generic.invalid.itemId"));
return;
}
break;
@@ -156,13 +186,19 @@ public final class SendMailCommand implements CommandHandler {
return;
}
mailBuilder.mail.itemList.add(new Mail.MailItem(item, amount, lvl));
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.send", amount, item, lvl));
CommandHandler.sendMessage(
sender, translate(sender, "commands.sendMail.send", amount, item, lvl));
}
}
}
}
} else {
CommandHandler.sendMessage(sender, translate(sender, "commands.sendMail.invalid_arguments_please_use", getConstructionArgs(mailBuilder.constructionStage, sender)));
CommandHandler.sendMessage(
sender,
translate(
sender,
"commands.sendMail.invalid_arguments_please_use",
getConstructionArgs(mailBuilder.constructionStage, sender)));
}
}
}

View File

@@ -2,19 +2,18 @@ package emu.grasscutter.command.commands;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.command.Command.TargetRequirement;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import java.util.List;
@Command(
label = "sendMessage",
aliases = {"say", "sendservmsg", "sendservermessage", "b", "broadcast"},
usage = {"<message>"},
permission = "server.sendmessage",
permissionTargeted = "server.sendmessage.others",
targetRequirement = TargetRequirement.NONE)
label = "sendMessage",
aliases = {"say", "sendservmsg", "sendservermessage", "b", "broadcast"},
usage = {"<message>"},
permission = "server.sendmessage",
permissionTargeted = "server.sendmessage.others",
targetRequirement = TargetRequirement.NONE)
public final class SendMessageCommand implements CommandHandler {
@Override

View File

@@ -5,19 +5,18 @@ import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Position;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.game.world.World;
import emu.grasscutter.server.packet.send.*;
import emu.grasscutter.utils.Position;
import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify;
import java.util.List;
@Command(
label = "setConst",
aliases = {"setconstellation"},
usage = {"<constellation level> [all]"},
permission = "player.setconstellation",
permissionTargeted = "player.setconstellation.others")
label = "setConst",
aliases = {"setconstellation"},
usage = {"<constellation level> [all]"},
permission = "player.setconstellation",
permissionTargeted = "player.setconstellation.others")
public final class SetConstCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
@@ -38,15 +37,16 @@ public final class SetConstCommand implements CommandHandler {
if (entity == null) return;
Avatar avatar = entity.getAvatar();
this.setConstellation(targetPlayer, avatar, constLevel);
CommandHandler.sendTranslatedMessage(sender, "commands.setConst.success", avatar.getAvatarData().getName(), constLevel);
CommandHandler.sendTranslatedMessage(
sender, "commands.setConst.success", avatar.getAvatarData().getName(), constLevel);
return;
}
// Check if there's an additional argument which is "all", if it does then go setAllConstellation
// Check if there's an additional argument which is "all", if it does then go
// setAllConstellation
if (args.size() > 1 && args.get(1).equalsIgnoreCase("all")) {
this.setAllConstellation(targetPlayer, constLevel);
CommandHandler.sendTranslatedMessage(sender, "commands.setConst.successall", constLevel);
}
else sendUsageMessage(sender);
} else sendUsageMessage(sender);
} catch (NumberFormatException ignored) {
CommandHandler.sendTranslatedMessage(sender, "commands.setConst.level_error");
}
@@ -68,12 +68,15 @@ public final class SetConstCommand implements CommandHandler {
}
private void setAllConstellation(Player player, int constLevel) {
player.getAvatars().forEach(avatar -> {
avatar.forceConstellationLevel(constLevel);
avatar.recalcConstellations();
avatar.recalcStats(true);
avatar.save();
});
player
.getAvatars()
.forEach(
avatar -> {
avatar.forceConstellationLevel(constLevel);
avatar.recalcConstellations();
avatar.recalcStats(true);
avatar.save();
});
// Just reload scene once, shorter than having to check for each constLevel < currentConstLevel
this.reloadScene(player);
}

View File

@@ -1,6 +1,6 @@
package emu.grasscutter.command.commands;
import java.util.List;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
@@ -8,15 +8,14 @@ import emu.grasscutter.data.GameData;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.packet.send.PacketAvatarFetterDataNotify;
import static emu.grasscutter.utils.Language.translate;
import java.util.List;
@Command(
label = "setFetterLevel",
usage = {"<level>"},
aliases = {"setfetterlvl", "setfriendship"},
permission = "player.setfetterlevel",
permissionTargeted = "player.setfetterlevel.others")
label = "setFetterLevel",
usage = {"<level>"},
aliases = {"setfetterlvl", "setfriendship"},
permission = "player.setfetterlevel",
permissionTargeted = "player.setfetterlevel.others")
public final class SetFetterLevelCommand implements CommandHandler {
@Override
@@ -29,7 +28,8 @@ public final class SetFetterLevelCommand implements CommandHandler {
try {
int fetterLevel = Integer.parseInt(args.get(0));
if (fetterLevel < 0 || fetterLevel > 10) {
CommandHandler.sendMessage(sender, translate(sender, "commands.setFetterLevel.range_error"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.setFetterLevel.range_error"));
return;
}
Avatar avatar = targetPlayer.getTeamManager().getCurrentAvatarEntity().getAvatar();
@@ -41,10 +41,10 @@ public final class SetFetterLevelCommand implements CommandHandler {
avatar.save();
targetPlayer.sendPacket(new PacketAvatarFetterDataNotify(avatar));
CommandHandler.sendMessage(sender, translate(sender, "commands.setFetterLevel.success", fetterLevel));
CommandHandler.sendMessage(
sender, translate(sender, "commands.setFetterLevel.success", fetterLevel));
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.setFetterLevel.level_error"));
}
}
}

View File

@@ -1,10 +1,5 @@
package emu.grasscutter.command.commands;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.data.GameData;
@@ -14,62 +9,34 @@ import emu.grasscutter.game.tower.TowerLevelRecord;
import emu.grasscutter.server.packet.send.PacketOpenStateChangeNotify;
import emu.grasscutter.server.packet.send.PacketSceneAreaUnlockNotify;
import emu.grasscutter.server.packet.send.PacketScenePointUnlockNotify;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
@Command(label = "setProp", aliases = {"prop"}, usage = {"<prop> <value>"}, permission = "player.setprop", permissionTargeted = "player.setprop.others")
@Command(
label = "setProp",
aliases = {"prop"},
usage = {"<prop> <value>"},
permission = "player.setprop",
permissionTargeted = "player.setprop.others")
public final class SetPropCommand implements CommandHandler {
static enum PseudoProp {
NONE,
WORLD_LEVEL,
TOWER_LEVEL,
BP_LEVEL,
GOD_MODE,
UNLIMITED_STAMINA,
UNLIMITED_ENERGY,
SET_OPENSTATE,
UNSET_OPENSTATE,
UNLOCK_MAP
}
static class Prop {
String name;
PlayerProperty prop;
PseudoProp pseudoProp;
public Prop(PlayerProperty prop) {
this(prop.toString(), prop, PseudoProp.NONE);
}
public Prop(String name) {
this(name, PlayerProperty.PROP_NONE, PseudoProp.NONE);
}
public Prop(String name, PseudoProp pseudoProp) {
this(name, PlayerProperty.PROP_NONE, pseudoProp);
}
public Prop(String name, PlayerProperty prop) {
this(name, prop, PseudoProp.NONE);
}
public Prop(String name, PlayerProperty prop, PseudoProp pseudoProp) {
this.name = name;
this.prop = prop;
this.pseudoProp = pseudoProp;
}
}
Map<String, Prop> props;
// List of map areas. Unfortunately, there is no readily available source for them in excels or
// bins.
private static final List<Integer> sceneAreas = IntStream.range(1, 1000).boxed().toList();
private final Map<String, Prop> props;
public SetPropCommand() {
this.props = new HashMap<>();
// Full PlayerProperty enum that won't be advertised but can be used by devs
for (PlayerProperty prop : PlayerProperty.values()) {
String name = prop.toString().substring(5); // PROP_EXP -> EXP
String key = name.toLowerCase(); // EXP -> exp
String name = prop.toString().substring(5); // PROP_EXP -> EXP
String key = name.toLowerCase(); // EXP -> exp
this.props.put(key, new Prop(name, prop));
}
// Add special props
Prop worldlevel = new Prop("World Level", PlayerProperty.PROP_PLAYER_WORLD_LEVEL, PseudoProp.WORLD_LEVEL);
Prop worldlevel =
new Prop("World Level", PlayerProperty.PROP_PLAYER_WORLD_LEVEL, PseudoProp.WORLD_LEVEL);
this.props.put("worldlevel", worldlevel);
this.props.put("wl", worldlevel);
@@ -112,6 +79,12 @@ public final class SetPropCommand implements CommandHandler {
Prop unlockmap = new Prop("UnlockMap", PseudoProp.UNLOCK_MAP);
this.props.put("unlockmap", unlockmap);
this.props.put("um", unlockmap);
Prop flyable = new Prop("IsFlyable", PlayerProperty.PROP_IS_FLYABLE, PseudoProp.IS_FLYABLE);
this.props.put("canfly", flyable);
this.props.put("fly", flyable);
this.props.put("glider", flyable);
this.props.put("canglide", flyable);
}
@Override
@@ -129,12 +102,13 @@ public final class SetPropCommand implements CommandHandler {
return;
}
try {
value = switch (valueStr.toLowerCase()) {
case "on", "true" -> 1;
case "off", "false" -> 0;
case "toggle" -> -1;
default -> Integer.parseInt(valueStr);
};
value =
switch (valueStr.toLowerCase()) {
case "on", "true" -> 1;
case "off", "false" -> 0;
case "toggle" -> -1;
default -> Integer.parseInt(valueStr);
};
} catch (NumberFormatException ignored) {
CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
return;
@@ -143,29 +117,35 @@ public final class SetPropCommand implements CommandHandler {
boolean success = false;
Prop prop = props.get(propStr);
success = switch (prop.pseudoProp) {
case WORLD_LEVEL -> targetPlayer.setWorldLevel(value);
case BP_LEVEL -> targetPlayer.getBattlePassManager().setLevel(value);
case TOWER_LEVEL -> this.setTowerLevel(sender, targetPlayer, value);
case GOD_MODE, UNLIMITED_STAMINA, UNLIMITED_ENERGY -> this.setBool(sender, targetPlayer, prop.pseudoProp, value);
case SET_OPENSTATE -> this.setOpenState(targetPlayer, value, 1);
case UNSET_OPENSTATE -> this.setOpenState(targetPlayer, value, 0);
case UNLOCK_MAP -> unlockMap(targetPlayer);
default -> targetPlayer.setProperty(prop.prop, value);
};
success =
switch (prop.pseudoProp) {
case WORLD_LEVEL -> targetPlayer.setWorldLevel(value);
case BP_LEVEL -> targetPlayer.getBattlePassManager().setLevel(value);
case TOWER_LEVEL -> this.setTowerLevel(sender, targetPlayer, value);
case GOD_MODE, UNLIMITED_STAMINA, UNLIMITED_ENERGY -> this.setBool(
sender, targetPlayer, prop.pseudoProp, value);
case SET_OPENSTATE -> this.setOpenState(targetPlayer, value, 1);
case UNSET_OPENSTATE -> this.setOpenState(targetPlayer, value, 0);
case UNLOCK_MAP -> unlockMap(targetPlayer);
default -> targetPlayer.setProperty(prop.prop, value);
};
if (success) {
if (targetPlayer == sender) {
CommandHandler.sendTranslatedMessage(sender, "commands.generic.set_to", prop.name, valueStr);
CommandHandler.sendTranslatedMessage(
sender, "commands.generic.set_to", prop.name, valueStr);
} else {
String uidStr = targetPlayer.getAccount().getId();
CommandHandler.sendTranslatedMessage(sender, "commands.generic.set_for_to", prop.name, uidStr, valueStr);
CommandHandler.sendTranslatedMessage(
sender, "commands.generic.set_for_to", prop.name, uidStr, valueStr);
}
} else {
if (prop.prop != PlayerProperty.PROP_NONE) { // PseudoProps need to do their own error messages
if (prop.prop
!= PlayerProperty.PROP_NONE) { // PseudoProps need to do their own error messages
int min = targetPlayer.getPropertyMin(prop.prop);
int max = targetPlayer.getPropertyMax(prop.prop);
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.value_between", prop.name, min, max);
CommandHandler.sendTranslatedMessage(
sender, "commands.generic.invalid.value_between", prop.name, min, max);
}
}
}
@@ -173,7 +153,8 @@ public final class SetPropCommand implements CommandHandler {
private boolean setTowerLevel(Player sender, Player targetPlayer, int topFloor) {
List<Integer> floorIds = targetPlayer.getServer().getTowerSystem().getAllFloors();
if (topFloor < 0 || topFloor > floorIds.size()) {
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.value_between", "Tower Level", 0, floorIds.size());
CommandHandler.sendTranslatedMessage(
sender, "commands.generic.invalid.value_between", "Tower Level", 0, floorIds.size());
return false;
}
@@ -186,33 +167,38 @@ public final class SetPropCommand implements CommandHandler {
}
// Remove records for each floor past our target
for (int floor : floorIds.subList(topFloor, floorIds.size())) {
if (recordMap.containsKey(floor)) {
recordMap.remove(floor);
}
recordMap.remove(floor);
}
// Six stars required on Floor 8 to unlock Floor 9+
if (topFloor > 8) {
recordMap.get(floorIds.get(7)).setLevelStars(0, 6); // levelIds seem to start at 1 for Floor 1 Chamber 1, so this doesn't get shown at all
recordMap
.get(floorIds.get(7))
.setLevelStars(
0,
6); // levelIds seem to start at 1 for Floor 1 Chamber 1, so this doesn't get shown at
// all
}
return true;
}
private boolean setBool(Player sender, Player targetPlayer, PseudoProp pseudoProp, int value) {
boolean enabled = switch (pseudoProp) {
case GOD_MODE -> targetPlayer.inGodmode();
case UNLIMITED_STAMINA -> targetPlayer.getUnlimitedStamina();
case UNLIMITED_ENERGY -> !targetPlayer.getEnergyManager().getEnergyUsage();
default -> false;
};
enabled = switch (value) {
case -1 -> !enabled;
case 0 -> false;
default -> true;
};
boolean enabled =
switch (pseudoProp) {
case GOD_MODE -> targetPlayer.isInGodMode();
case UNLIMITED_STAMINA -> targetPlayer.isUnlimitedStamina();
case UNLIMITED_ENERGY -> !targetPlayer.getEnergyManager().isEnergyUsage();
default -> false;
};
enabled =
switch (value) {
case -1 -> !enabled;
case 0 -> false;
default -> true;
};
switch (pseudoProp) {
case GOD_MODE:
targetPlayer.setGodmode(enabled);
targetPlayer.setInGodMode(enabled);
break;
case UNLIMITED_STAMINA:
targetPlayer.setUnlimitedStamina(enabled);
@@ -231,22 +217,68 @@ public final class SetPropCommand implements CommandHandler {
return true;
}
// List of map areas. Unfortunately, there is no readily available source for them in excels or bins.
final static private List<Integer> sceneAreas = IntStream.range(1, 1000).boxed().toList();
private boolean unlockMap(Player targetPlayer) {
// Unlock.
GameData.getScenePointsPerScene().forEach((sceneId, scenePoints) -> {
// Unlock trans points.
targetPlayer.getUnlockedScenePoints(sceneId).addAll(scenePoints);
GameData.getScenePointsPerScene()
.forEach(
(sceneId, scenePoints) -> {
// Unlock trans points.
targetPlayer.getUnlockedScenePoints(sceneId).addAll(scenePoints);
// Unlock map areas.
targetPlayer.getUnlockedSceneAreas(sceneId).addAll(sceneAreas);
});
// Unlock map areas.
targetPlayer.getUnlockedSceneAreas(sceneId).addAll(sceneAreas);
});
// Send notify.
int playerScene = targetPlayer.getSceneId();
targetPlayer.sendPacket(new PacketScenePointUnlockNotify(playerScene, targetPlayer.getUnlockedScenePoints(playerScene)));
targetPlayer.sendPacket(new PacketSceneAreaUnlockNotify(playerScene, targetPlayer.getUnlockedSceneAreas(playerScene)));
targetPlayer.sendPacket(
new PacketScenePointUnlockNotify(
playerScene, targetPlayer.getUnlockedScenePoints(playerScene)));
targetPlayer.sendPacket(
new PacketSceneAreaUnlockNotify(
playerScene, targetPlayer.getUnlockedSceneAreas(playerScene)));
return true;
}
enum PseudoProp {
NONE,
WORLD_LEVEL,
TOWER_LEVEL,
BP_LEVEL,
GOD_MODE,
UNLIMITED_STAMINA,
UNLIMITED_ENERGY,
SET_OPENSTATE,
UNSET_OPENSTATE,
UNLOCK_MAP,
IS_FLYABLE
}
static class Prop {
String name;
PlayerProperty prop;
PseudoProp pseudoProp;
public Prop(PlayerProperty prop) {
this(prop.toString(), prop, PseudoProp.NONE);
}
public Prop(String name) {
this(name, PlayerProperty.PROP_NONE, PseudoProp.NONE);
}
public Prop(String name, PseudoProp pseudoProp) {
this(name, PlayerProperty.PROP_NONE, pseudoProp);
}
public Prop(String name, PlayerProperty prop) {
this(name, prop, PseudoProp.NONE);
}
public Prop(String name, PlayerProperty prop, PseudoProp pseudoProp) {
this.name = name;
this.prop = prop;
this.pseudoProp = pseudoProp;
}
}
}

View File

@@ -1,9 +1,5 @@
package emu.grasscutter.command.commands;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.avatar.Avatar;
@@ -11,45 +7,22 @@ import emu.grasscutter.game.entity.EntityAvatar;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Command(
label = "setStats",
aliases = {"stats", "stat"},
usage = {
"[set] <stat> <value>",
"(lock|freeze) <stat> [<value>]", // Can lock to current value
"(unlock|unfreeze) <stat>"},
permission = "player.setstats",
permissionTargeted = "player.setstats.others")
label = "setStats",
aliases = {"stats", "stat"},
usage = {
"[set] <stat> <value>",
"(lock|freeze) <stat> [<value>]", // Can lock to current value
"(unlock|unfreeze) <stat>"
},
permission = "player.setstats",
permissionTargeted = "player.setstats.others")
public final class SetStatsCommand implements CommandHandler {
private static class Stat {
String name;
FightProperty prop;
public Stat(FightProperty prop) {
this.name = prop.toString();
this.prop = prop;
}
public Stat(String name, FightProperty prop) {
this.name = name;
this.prop = prop;
}
}
private static enum Action {
ACTION_SET("commands.generic.set_to", "commands.generic.set_for_to"),
ACTION_LOCK("commands.setStats.locked_to", "commands.setStats.locked_for_to"),
ACTION_UNLOCK("commands.setStats.unlocked", "commands.setStats.unlocked_for");
public final String messageKeySelf;
public final String messageKeyOther;
private Action(String messageKeySelf, String messageKeyOther) {
this.messageKeySelf = messageKeySelf;
this.messageKeyOther = messageKeyOther;
}
}
private Map<String, Stat> stats;
private final Map<String, Stat> stats;
public SetStatsCommand() {
this.stats = new HashMap<>();
@@ -59,18 +32,22 @@ public final class SetStatsCommand implements CommandHandler {
// Full FightProperty enum that won't be advertised but can be used by devs
// They have a prefix to avoid the "hp" clash
for (FightProperty prop : FightProperty.values()) {
String name = prop.toString().substring(10); // FIGHT_PROP_BASE_HP -> _BASE_HP
String key = name.toLowerCase(); // _BASE_HP -> _base_hp
name = name.substring(1); // _BASE_HP -> BASE_HP
String name = prop.toString().substring(10); // FIGHT_PROP_BASE_HP -> _BASE_HP
String key = name.toLowerCase(); // _BASE_HP -> _base_hp
name = name.substring(1); // _BASE_HP -> BASE_HP
this.stats.put(key, new Stat(name, prop));
}
// Compatibility aliases
this.stats.put("mhp", this.stats.get("maxhp"));
this.stats.put("hp", this.stats.get("_cur_hp")); // Overrides FIGHT_PROP_HP
this.stats.put("atk", this.stats.get("_cur_attack")); // Overrides FIGHT_PROP_ATTACK
this.stats.put("def", this.stats.get("_cur_defense")); // Overrides FIGHT_PROP_DEFENSE
this.stats.put("atkb", this.stats.get("_base_attack")); // This doesn't seem to get used to recalculate ATK, so it's only useful for stuff like Bennett's buff.
this.stats.put("hp", this.stats.get("_cur_hp")); // Overrides FIGHT_PROP_HP
this.stats.put("atk", this.stats.get("_cur_attack")); // Overrides FIGHT_PROP_ATTACK
this.stats.put("def", this.stats.get("_cur_defense")); // Overrides FIGHT_PROP_DEFENSE
this.stats.put(
"atkb",
this.stats.get(
"_base_attack")); // This doesn't seem to get used to recalculate ATK, so it's only
// useful for stuff like Bennett's buff.
this.stats.put("eanemo", this.stats.get("anemo%"));
this.stats.put("ecryo", this.stats.get("cryo%"));
this.stats.put("edendro", this.stats.get("dendro%"));
@@ -86,7 +63,7 @@ public final class SetStatsCommand implements CommandHandler {
public static float parsePercent(String input) throws NumberFormatException {
if (input.endsWith("%")) {
return Float.parseFloat(input.substring(0, input.length()-1))/100f;
return Float.parseFloat(input.substring(0, input.length() - 1)) / 100f;
} else {
return Float.parseFloat(input);
}
@@ -105,17 +82,21 @@ public final class SetStatsCommand implements CommandHandler {
// Get the action and stat
String arg0 = args.remove(0).toLowerCase();
Action action = switch (arg0) {
default -> {statStr = arg0; yield Action.ACTION_SET;} // Implicit set command
case "set" -> Action.ACTION_SET; // Explicit set command
case "lock", "freeze" -> Action.ACTION_LOCK;
case "unlock", "unfreeze" -> Action.ACTION_UNLOCK;
};
Action action =
switch (arg0) {
default -> {
statStr = arg0;
yield Action.ACTION_SET;
} // Implicit set command
case "set" -> Action.ACTION_SET; // Explicit set command
case "lock", "freeze" -> Action.ACTION_LOCK;
case "unlock", "unfreeze" -> Action.ACTION_UNLOCK;
};
if (statStr == null) {
statStr = args.remove(0).toLowerCase();
}
if (!stats.containsKey(statStr)) {
sendUsageMessage(sender); // Invalid stat or action
sendUsageMessage(sender); // Invalid stat or action
return;
}
Stat stat = stats.get(statStr);
@@ -126,10 +107,10 @@ public final class SetStatsCommand implements CommandHandler {
try {
switch (action) {
case ACTION_LOCK:
if (args.isEmpty()) { // Lock to current value
if (args.isEmpty()) { // Lock to current value
value = avatar.getFightProperty(stat.prop);
break;
} // Else fall-through and lock to supplied value
} // Else fall-through and lock to supplied value
case ACTION_SET:
value = parsePercent(args.remove(0));
break;
@@ -144,7 +125,7 @@ public final class SetStatsCommand implements CommandHandler {
return;
}
if (!args.isEmpty()) { // Leftover arguments!
if (!args.isEmpty()) { // Leftover arguments!
sendUsageMessage(sender);
return;
}
@@ -174,8 +155,36 @@ public final class SetStatsCommand implements CommandHandler {
CommandHandler.sendTranslatedMessage(sender, action.messageKeySelf, stat.name, valueStr);
} else {
String uidStr = targetPlayer.getAccount().getId();
CommandHandler.sendTranslatedMessage(sender, action.messageKeyOther, stat.name, uidStr, valueStr);
CommandHandler.sendTranslatedMessage(
sender, action.messageKeyOther, stat.name, uidStr, valueStr);
}
}
private enum Action {
ACTION_SET("commands.generic.set_to", "commands.generic.set_for_to"),
ACTION_LOCK("commands.setStats.locked_to", "commands.setStats.locked_for_to"),
ACTION_UNLOCK("commands.setStats.unlocked", "commands.setStats.unlocked_for");
public final String messageKeySelf;
public final String messageKeyOther;
Action(String messageKeySelf, String messageKeyOther) {
this.messageKeySelf = messageKeySelf;
this.messageKeyOther = messageKeyOther;
}
}
private static class Stat {
String name;
FightProperty prop;
public Stat(FightProperty prop) {
this.name = prop.toString();
this.prop = prop;
}
public Stat(String name, FightProperty prop) {
this.name = name;
this.prop = prop;
}
return;
}
}

View File

@@ -0,0 +1,48 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Position;
import emu.grasscutter.server.packet.send.PacketScenePlayerSoundNotify;
import java.util.List;
import lombok.val;
@Command(
label = "sound",
aliases = {"s", "audio"},
usage = {"[<audioname>] [<x><y><z>]"},
permission = "player.group",
permissionTargeted = "player.group.others")
public final class SoundCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (args.isEmpty()) {
sendUsageMessage(sender);
return;
}
val soundName = args.get(0);
var playPosition = targetPlayer.getPosition();
if (args.size() == 4) {
try {
float x, y, z;
x = Float.parseFloat(args.get(1));
y = Float.parseFloat(args.get(2));
z = Float.parseFloat(args.get(3));
playPosition = new Position(x, y, z);
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error"));
return;
}
} else if (args.size() > 1) {
sendUsageMessage(sender);
return;
}
targetPlayer
.getScene()
.broadcastPacket(new PacketScenePlayerSoundNotify(playPosition, soundName, 1));
}
}

View File

@@ -1,51 +1,51 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.command.CommandHelpers.*;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.GadgetData;
import emu.grasscutter.data.excels.ItemData;
import emu.grasscutter.data.excels.MonsterData;
import emu.grasscutter.data.excels.monster.MonsterData;
import emu.grasscutter.game.entity.*;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.EntityType;
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.game.world.Position;
import emu.grasscutter.game.world.Scene;
import emu.grasscutter.utils.Position;
import lombok.Setter;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.regex.Pattern;
import static emu.grasscutter.command.CommandHelpers.*;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
import static emu.grasscutter.utils.Language.translate;
import lombok.Setter;
@Command(
label = "spawn",
aliases = {"drop", "s"},
usage = {
"<itemId> [x<amount>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
"<gadgetId> [x<amount>] [state<state>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
"<monsterId> [x<amount>] [lv<level>] [ai<aiId>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>"},
permission = "server.spawn",
permissionTargeted = "server.spawn.others")
label = "spawn",
aliases = {"drop", "s"},
usage = {
"<itemId> [x<amount>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
"<gadgetId> [x<amount>] [state<state>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>",
"<monsterId> [x<amount>] [lv<level>] [ai<aiId>] [maxhp<maxhp>] [hp<hp>(0 for infinite)] [atk<atk>] [def<def>] [blk<blockId>] [grp<groupId>] [cfg<configId>] <x> <y> <z>"
},
permission = "server.spawn",
permissionTargeted = "server.spawn.others")
public final class SpawnCommand implements CommandHandler {
private static final Map<Pattern, BiConsumer<SpawnParameters, Integer>> intCommandHandlers = Map.ofEntries(
Map.entry(lvlRegex, SpawnParameters::setLvl),
Map.entry(amountRegex, SpawnParameters::setAmount),
Map.entry(stateRegex, SpawnParameters::setState),
Map.entry(blockRegex, SpawnParameters::setBlockId),
Map.entry(groupRegex, SpawnParameters::setGroupId),
Map.entry(configRegex, SpawnParameters::setConfigId),
Map.entry(maxHPRegex, SpawnParameters::setMaxHP),
Map.entry(hpRegex, SpawnParameters::setHp),
Map.entry(defRegex, SpawnParameters::setDef),
Map.entry(atkRegex, SpawnParameters::setAtk),
Map.entry(aiRegex, SpawnParameters::setAi)
);
private static final Map<Pattern, BiConsumer<SpawnParameters, Integer>> intCommandHandlers =
Map.ofEntries(
Map.entry(lvlRegex, SpawnParameters::setLvl),
Map.entry(amountRegex, SpawnParameters::setAmount),
Map.entry(stateRegex, SpawnParameters::setState),
Map.entry(blockRegex, SpawnParameters::setBlockId),
Map.entry(groupRegex, SpawnParameters::setGroupId),
Map.entry(configRegex, SpawnParameters::setConfigId),
Map.entry(maxHPRegex, SpawnParameters::setMaxHP),
Map.entry(hpRegex, SpawnParameters::setHp),
Map.entry(defRegex, SpawnParameters::setDef),
Map.entry(atkRegex, SpawnParameters::setAtk),
Map.entry(aiRegex, SpawnParameters::setAi));
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
@@ -55,7 +55,7 @@ public final class SpawnCommand implements CommandHandler {
// At this point, first remaining argument MUST be the id and the rest the pos
if (args.size() < 1) {
sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar
sendUsageMessage(sender); // Reachable if someone does `/give lv90` or similar
throw new IllegalArgumentException();
}
switch (args.size()) {
@@ -67,13 +67,15 @@ public final class SpawnCommand implements CommandHandler {
z = Float.parseFloat(args.get(3));
param.pos = new Position(x, y, z);
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error"));
} // Fallthrough
CommandHandler.sendMessage(
sender, translate(sender, "commands.execution.argument_error"));
} // Fallthrough
case 1:
try {
param.id = Integer.parseInt(args.get(0));
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.generic.invalid.entityId"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.generic.invalid.entityId"));
}
break;
default:
@@ -92,8 +94,13 @@ public final class SpawnCommand implements CommandHandler {
param.scene = targetPlayer.getScene();
if (param.scene.getEntities().size() + param.amount > GAME_OPTIONS.sceneEntityLimit) {
param.amount = Math.max(Math.min(GAME_OPTIONS.sceneEntityLimit - param.scene.getEntities().size(), param.amount), 0);
CommandHandler.sendMessage(sender, translate(sender, "commands.spawn.limit_reached", param.amount));
param.amount =
Math.max(
Math.min(
GAME_OPTIONS.sceneEntityLimit - param.scene.getEntities().size(), param.amount),
0);
CommandHandler.sendMessage(
sender, translate(sender, "commands.spawn.limit_reached", param.amount));
if (param.amount <= 0) {
return;
}
@@ -121,16 +128,16 @@ public final class SpawnCommand implements CommandHandler {
param.scene.addEntity(entity);
}
CommandHandler.sendMessage(sender, translate(sender, "commands.spawn.success", param.amount, param.id));
CommandHandler.sendMessage(
sender, translate(sender, "commands.spawn.success", param.amount, param.id));
}
;
private EntityItem createItem(ItemData itemData, SpawnParameters param, Position pos) {
return new EntityItem(param.scene, null, itemData, pos, 1, true);
}
private EntityMonster createMonster(MonsterData monsterData, SpawnParameters param, Position pos) {
private EntityMonster createMonster(
MonsterData monsterData, SpawnParameters param, Position pos) {
var entity = new EntityMonster(param.scene, monsterData, pos, param.lvl);
if (param.ai != -1) {
entity.setAiId(param.ai);
@@ -138,10 +145,13 @@ public final class SpawnCommand implements CommandHandler {
return entity;
}
private EntityBaseGadget createGadget(GadgetData gadgetData, SpawnParameters param, Position pos, Player targetPlayer) {
private EntityBaseGadget createGadget(
GadgetData gadgetData, SpawnParameters param, Position pos, Player targetPlayer) {
EntityBaseGadget entity;
if (gadgetData.getType() == EntityType.Vehicle) {
entity = new EntityVehicle(param.scene, targetPlayer, param.id, 0, pos, targetPlayer.getRotation());
entity =
new EntityVehicle(
param.scene, targetPlayer, param.id, 0, pos, targetPlayer.getRotation());
} else {
entity = new EntityGadget(param.scene, param.id, pos, targetPlayer.getRotation());
if (param.state != -1) {
@@ -167,7 +177,8 @@ public final class SpawnCommand implements CommandHandler {
entity.setFightProperty(FightProperty.FIGHT_PROP_BASE_HP, param.maxHP);
}
if (param.hp != -1) {
entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, param.hp == 0 ? Float.MAX_VALUE : param.hp);
entity.setFightProperty(
FightProperty.FIGHT_PROP_CUR_HP, param.hp == 0 ? Float.MAX_VALUE : param.hp);
}
if (param.atk != -1) {
entity.setFightProperty(FightProperty.FIGHT_PROP_ATTACK, param.atk);

View File

@@ -1,15 +1,18 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "stop", aliases = {"shutdown"}, permission = "server.stop", targetRequirement = Command.TargetRequirement.NONE)
@Command(
label = "stop",
aliases = {"shutdown"},
permission = "server.stop",
targetRequirement = Command.TargetRequirement.NONE)
public final class StopCommand implements CommandHandler {
@Override

View File

@@ -3,24 +3,24 @@ package emu.grasscutter.command.commands;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.data.GameData;
import emu.grasscutter.data.excels.AvatarSkillDepotData;
import emu.grasscutter.data.excels.avatar.AvatarSkillDepotData;
import emu.grasscutter.game.avatar.Avatar;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.utils.Language;
import emu.grasscutter.utils.lang.Language;
import java.util.List;
@Command(
label = "talent",
usage = {"set <talentId> <level>", "(n|e|q|all) <level>", "getid"},
permission = "player.settalent",
permissionTargeted = "player.settalent.others")
label = "talent",
usage = {"set <talentId> <level>", "(n|e|q|all) <level>", "getid"},
permission = "player.settalent",
permissionTargeted = "player.settalent.others")
public final class TalentCommand implements CommandHandler {
private void setTalentLevel(Player sender, Avatar avatar, int skillId, int newLevel) {
if (avatar.setSkillLevel(skillId, newLevel)) {
long nameHash = GameData.getAvatarSkillDataMap().get(skillId).getNameTextMapHash();
var name = Language.getTextMapKey(nameHash);
CommandHandler.sendTranslatedMessage(sender, "commands.talent.set_id", skillId, name, newLevel);
CommandHandler.sendTranslatedMessage(
sender, "commands.talent.set_id", skillId, name, newLevel);
} else {
CommandHandler.sendTranslatedMessage(sender, "commands.talent.out_of_range");
}
@@ -35,7 +35,9 @@ public final class TalentCommand implements CommandHandler {
Avatar avatar = targetPlayer.getTeamManager().getCurrentAvatarEntity().getAvatar();
AvatarSkillDepotData skillDepot = avatar.getSkillDepot();
if (skillDepot == null) { // Avatars without skill depots aren't a suitable target even with manual skillId specified
if (skillDepot
== null) { // Avatars without skill depots aren't a suitable target even with manual skillId
// specified
CommandHandler.sendTranslatedMessage(sender, "commands.talent.invalid_skill_id");
return;
}
@@ -46,7 +48,6 @@ public final class TalentCommand implements CommandHandler {
switch (cmdSwitch) {
default -> {
sendUsageMessage(sender);
return;
}
case "set" -> {
if (args.size() < 3) {
@@ -80,11 +81,12 @@ public final class TalentCommand implements CommandHandler {
return;
}
skillId = switch (cmdSwitch) {
default -> skillDepot.getSkills().get(0);
case "e" -> skillDepot.getSkills().get(1);
case "q" -> skillDepot.getEnergySkill();
};
skillId =
switch (cmdSwitch) {
default -> skillDepot.getSkills().get(0);
case "e" -> skillDepot.getSkills().get(1);
case "q" -> skillDepot.getEnergySkill();
};
setTalentLevel(sender, avatar, skillId, newLevel);
}
case "all" -> {
@@ -104,17 +106,23 @@ public final class TalentCommand implements CommandHandler {
return;
}
int finalNewLevel = newLevel;
skillDepot.getSkillsAndEnergySkill().forEach(id -> setTalentLevel(sender, avatar, id, finalNewLevel));
skillDepot
.getSkillsAndEnergySkill()
.forEach(id -> setTalentLevel(sender, avatar, id, finalNewLevel));
}
case "getid" -> {
var map = GameData.getAvatarSkillDataMap();
skillDepot.getSkillsAndEnergySkill().forEach(id -> {
var talent = map.get(id);
if (talent == null) return;
var talentName = Language.getTextMapKey(talent.getNameTextMapHash());
var talentDesc = Language.getTextMapKey(talent.getDescTextMapHash());
CommandHandler.sendTranslatedMessage(sender, "commands.talent.id_desc", id, talentName, talentDesc);
});
skillDepot
.getSkillsAndEnergySkill()
.forEach(
id -> {
var talent = map.get(id);
if (talent == null) return;
var talentName = Language.getTextMapKey(talent.getNameTextMapHash());
var talentDesc = Language.getTextMapKey(talent.getDescTextMapHash());
CommandHandler.sendTranslatedMessage(
sender, "commands.talent.id_desc", id, talentName, talentDesc);
});
}
}
}

View File

@@ -1,22 +1,20 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.config.Configuration.GAME_OPTIONS;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.packet.send.PacketChangeMpTeamAvatarRsp;
import java.util.List;
import static emu.grasscutter.config.Configuration.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@Command(
label = "team",
usage = {"add <avatarId,...>", "(remove|set) [index|first|last|index-index,...]"},
permission = "player.team",
permissionTargeted = "player.team.others")
label = "team",
usage = {"add <avatarId,...>", "(remove|set) [index|first|last|index-index,...]"},
permission = "player.team",
permissionTargeted = "player.team.others")
public final class TeamCommand implements CommandHandler {
private static final int BASE_AVATARID = 10000000;
@@ -46,8 +44,11 @@ public final class TeamCommand implements CommandHandler {
return;
}
targetPlayer.getTeamManager().updateTeamEntities(
new PacketChangeMpTeamAvatarRsp(targetPlayer, targetPlayer.getTeamManager().getCurrentTeamInfo()));
targetPlayer
.getTeamManager()
.updateTeamEntities(
new PacketChangeMpTeamAvatarRsp(
targetPlayer, targetPlayer.getTeamManager().getCurrentTeamInfo()));
}
private boolean addCommand(Player sender, Player targetPlayer, List<String> args) {
@@ -72,14 +73,16 @@ public final class TeamCommand implements CommandHandler {
var currentTeamAvatars = targetPlayer.getTeamManager().getCurrentTeamInfo().getAvatars();
if (currentTeamAvatars.size() + avatarIds.length > GAME_OPTIONS.avatarLimits.singlePlayerTeam) {
CommandHandler.sendTranslatedMessage(sender, "commands.team.add_too_much", GAME_OPTIONS.avatarLimits.singlePlayerTeam);
CommandHandler.sendTranslatedMessage(
sender, "commands.team.add_too_much", GAME_OPTIONS.avatarLimits.singlePlayerTeam);
return false;
}
for (var avatarId: avatarIds) {
for (var avatarId : avatarIds) {
int id = Integer.parseInt(avatarId);
if (!addAvatar(sender, targetPlayer, id, index))
CommandHandler.sendTranslatedMessage(sender, "commands.team.failed_to_add_avatar", avatarId);
CommandHandler.sendTranslatedMessage(
sender, "commands.team.failed_to_add_avatar", avatarId);
if (index > 0) ++index;
}
return true;
@@ -98,16 +101,17 @@ public final class TeamCommand implements CommandHandler {
var metaIndexList = args.get(1).split(",");
var indexes = new HashSet<Integer>();
var ignoreList = new ArrayList<Integer>();
for (var metaIndex: metaIndexList) {
for (var metaIndex : metaIndexList) {
// step 1: parse metaIndex to indexes
var subIndexes = transformToIndexes(metaIndex, avatarCount);
if (subIndexes == null) {
CommandHandler.sendTranslatedMessage(sender, "commands.team.failed_to_parse_index", metaIndex);
CommandHandler.sendTranslatedMessage(
sender, "commands.team.failed_to_parse_index", metaIndex);
continue;
}
// step 2: get all of the avatar id through indexes
for (var avatarIndex: subIndexes) {
for (var avatarIndex : subIndexes) {
try {
indexes.add(currentTeamAvatars.get(avatarIndex - 1));
} catch (Exception e) {
@@ -147,7 +151,8 @@ public final class TeamCommand implements CommandHandler {
index = Integer.parseInt(args.get(1)) - 1;
if (index < 0) index = 0;
} catch (Exception e) {
CommandHandler.sendTranslatedMessage(sender, "commands.team.failed_to_parse_index", args.get(1));
CommandHandler.sendTranslatedMessage(
sender, "commands.team.failed_to_parse_index", args.get(1));
return false;
}
@@ -160,7 +165,8 @@ public final class TeamCommand implements CommandHandler {
try {
avatarId = Integer.parseInt(args.get(2));
} catch (Exception e) {
CommandHandler.sendTranslatedMessage(sender, "commands.team.failed_parse_avatar_id", args.get(2));
CommandHandler.sendTranslatedMessage(
sender, "commands.team.failed_parse_avatar_id", args.get(2));
return false;
}
if (avatarId < BASE_AVATARID) {
@@ -168,7 +174,8 @@ public final class TeamCommand implements CommandHandler {
}
if (currentTeamAvatars.contains(avatarId)) {
CommandHandler.sendTranslatedMessage(sender, "commands.team.avatar_already_in_team", avatarId);
CommandHandler.sendTranslatedMessage(
sender, "commands.team.avatar_already_in_team", avatarId);
return false;
}
@@ -187,7 +194,8 @@ public final class TeamCommand implements CommandHandler {
}
var currentTeamAvatars = targetPlayer.getTeamManager().getCurrentTeamInfo().getAvatars();
if (currentTeamAvatars.contains(avatarId)) {
CommandHandler.sendTranslatedMessage(sender, "commands.team.avatar_already_in_team", avatarId);
CommandHandler.sendTranslatedMessage(
sender, "commands.team.avatar_already_in_team", avatarId);
return false;
}
if (!targetPlayer.getAvatars().hasAvatar(avatarId)) {
@@ -219,17 +227,19 @@ public final class TeamCommand implements CommandHandler {
int min, max;
try {
min = switch (range[0]) {
case "first" -> 1;
case "last" -> listLength;
default -> Integer.parseInt(range[0]);
};
min =
switch (range[0]) {
case "first" -> 1;
case "last" -> listLength;
default -> Integer.parseInt(range[0]);
};
max = switch (range[1]) {
case "first" -> 1;
case "last" -> listLength;
default -> Integer.parseInt(range[1]);
};
max =
switch (range[1]) {
case "first" -> 1;
case "last" -> listLength;
default -> Integer.parseInt(range[1]);
};
} catch (Exception e) {
return null;
}
@@ -255,5 +265,4 @@ public final class TeamCommand implements CommandHandler {
return null;
}
}
}

View File

@@ -1,15 +1,18 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "teleportAll", aliases = {"tpall"}, permission = "player.tpall", permissionTargeted = "player.tpall.others")
@Command(
label = "teleportAll",
aliases = {"tpall"},
permission = "player.tpall",
permissionTargeted = "player.tpall.others")
public final class TeleportAllCommand implements CommandHandler {
@Override
@@ -20,10 +23,12 @@ public final class TeleportAllCommand implements CommandHandler {
}
for (Player player : targetPlayer.getWorld().getPlayers()) {
if (player.equals(targetPlayer))
continue;
if (player.equals(targetPlayer)) continue;
player.getWorld().transferPlayerToScene(player, targetPlayer.getSceneId(), TeleportType.COMMAND, targetPlayer.getPosition());
player
.getWorld()
.transferPlayerToScene(
player, targetPlayer.getSceneId(), TeleportType.COMMAND, targetPlayer.getPosition());
}
CommandHandler.sendMessage(sender, translate(sender, "commands.teleportAll.success"));

View File

@@ -1,24 +1,29 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.world.Position;
import emu.grasscutter.server.event.player.PlayerTeleportEvent.TeleportType;
import emu.grasscutter.utils.Position;
import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "teleport", aliases = {"tp"}, usage = {"<x> <y> <z> [sceneId]"}, permission = "player.teleport", permissionTargeted = "player.teleport.others")
@Command(
label = "teleport",
aliases = {"tp"},
usage = {"<x> <y> <z> [sceneId]"},
permission = "player.teleport",
permissionTargeted = "player.teleport.others")
public final class TeleportCommand implements CommandHandler {
private float parseRelative(String input, Float current) { // TODO: Maybe this will be useful elsewhere later
if (input.contains("~")) { // Relative
if (!input.equals("~")) { // Relative with offset
private float parseRelative(
String input, Float current) { // TODO: Maybe this will be useful elsewhere later
if (input.contains("~")) { // Relative
if (!input.equals("~")) { // Relative with offset
current += Float.parseFloat(input.replace("~", ""));
} // Else no offset, no modification
} else { // Absolute
} // Else no offset, no modification
} else { // Absolute
current = Float.parseFloat(input);
}
return current;
@@ -36,16 +41,18 @@ public final class TeleportCommand implements CommandHandler {
case 4:
try {
sceneId = Integer.parseInt(args.get(3));
}catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.argument_error"));
} // Fallthrough
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.execution.argument_error"));
} // Fallthrough
case 3:
try {
x = this.parseRelative(args.get(0), x);
y = this.parseRelative(args.get(1), y);
z = this.parseRelative(args.get(2), z);
} catch (NumberFormatException ignored) {
CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.invalid_position"));
CommandHandler.sendMessage(
sender, translate(sender, "commands.teleport.invalid_position"));
}
break;
default:
@@ -54,15 +61,18 @@ public final class TeleportCommand implements CommandHandler {
}
Position target_pos = new Position(x, y, z);
boolean result = targetPlayer.getWorld().transferPlayerToScene(targetPlayer, sceneId, TeleportType.COMMAND, target_pos);
boolean result =
targetPlayer
.getWorld()
.transferPlayerToScene(targetPlayer, sceneId, TeleportType.COMMAND, target_pos);
if (!result) {
CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.exists_error"));
} else {
CommandHandler.sendMessage(sender, translate(sender, "commands.teleport.success",
targetPlayer.getNickname(), x, y, z, sceneId)
);
CommandHandler.sendMessage(
sender,
translate(
sender, "commands.teleport.success", targetPlayer.getNickname(), x, y, z, sceneId));
}
}
}

View File

@@ -0,0 +1,156 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.activity.trialavatar.TrialAvatarActivityHandler;
import emu.grasscutter.game.activity.trialavatar.TrialAvatarPlayerData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ActivityType;
import emu.grasscutter.server.packet.send.PacketActivityInfoNotify;
import emu.grasscutter.utils.JsonUtils;
import java.util.List;
@Command(
label = "trialAvatarActivity",
aliases = {"taa"},
usage = {
"change <scheduleId>",
"toggleDungeon <index(start from 1)|all>",
"toggleReward <index(start from 1)|all>"
},
permission = "player.trialavataractivity",
permissionTargeted = "player.trialavataractivity.others")
public final class TrialAvatarActivityCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (args.size() < 2) {
sendUsageMessage(sender);
return;
}
var action = args.get(0).toLowerCase();
var param = args.get(1);
var playerDataOption =
targetPlayer
.getActivityManager()
.getPlayerActivityDataByActivityType(ActivityType.NEW_ACTIVITY_TRIAL_AVATAR);
if (playerDataOption.isEmpty()) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.not_found"));
return;
}
var playerData = playerDataOption.get();
var handler = (TrialAvatarActivityHandler) playerData.getActivityHandler();
if (handler == null) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.not_found"));
return;
}
var trialAvatarPlayerData =
JsonUtils.decode(playerData.getDetail(), TrialAvatarPlayerData.class);
if (trialAvatarPlayerData == null) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.not_found"));
return;
}
switch (action) {
default -> this.sendUsageMessage(sender);
case "change" -> {
if (!param.chars().allMatch(Character::isDigit)) { // if its not number
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.invalid_param"));
return;
}
if (TrialAvatarPlayerData.getAvatarIdList(Integer.parseInt(param)).isEmpty()) {
CommandHandler.sendMessage(
sender,
translate(
sender,
"commands.trialAvatarActivity.schedule_not_found",
Integer.parseInt(param)));
return;
}
playerData.setDetail(TrialAvatarPlayerData.create(Integer.parseInt(param)));
playerData.save();
CommandHandler.sendMessage(
sender,
translate(
sender, "commands.trialAvatarActivity.success_schedule", Integer.parseInt(param)));
}
case "toggledungeon" -> {
if (param.chars().allMatch(Character::isDigit)) { // if its number
if (Integer.parseInt(param) - 1 >= trialAvatarPlayerData.getRewardInfoList().size()
|| Integer.parseInt(param) - 1 <= 0) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.invalid_param"));
return;
}
TrialAvatarPlayerData.RewardInfoItem rewardInfo =
trialAvatarPlayerData.getRewardInfoList().get(Integer.parseInt(param) - 1);
rewardInfo.setPassedDungeon(!rewardInfo.isPassedDungeon());
playerData.setDetail(trialAvatarPlayerData);
playerData.save();
CommandHandler.sendMessage(
sender,
translate(
sender, "commands.trialAvatarActivity.success_dungeon", Integer.parseInt(param)));
} else {
if (!param.equals("all")) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.invalid_param"));
return;
}
trialAvatarPlayerData
.getRewardInfoList()
.forEach(r -> r.setPassedDungeon(!r.isPassedDungeon()));
playerData.setDetail(trialAvatarPlayerData);
playerData.save();
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.success_dungeon_all"));
}
}
case "togglereward" -> {
if (param.chars().allMatch(Character::isDigit)) { // if its number
if (Integer.parseInt(param) - 1 >= trialAvatarPlayerData.getRewardInfoList().size()
|| Integer.parseInt(param) - 1 <= 0) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.invalid_param"));
return;
}
TrialAvatarPlayerData.RewardInfoItem rewardInfo =
trialAvatarPlayerData.getRewardInfoList().get(Integer.parseInt(param) - 1);
rewardInfo.setReceivedReward(!rewardInfo.isReceivedReward());
playerData.setDetail(trialAvatarPlayerData);
playerData.save();
CommandHandler.sendMessage(
sender,
translate(
sender, "commands.trialAvatarActivity.success_reward", Integer.parseInt(param)));
} else {
if (!param.toLowerCase().equals("all")) {
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.invalid_param"));
return;
}
trialAvatarPlayerData
.getRewardInfoList()
.forEach(r -> r.setReceivedReward(!r.isReceivedReward()));
playerData.setDetail(trialAvatarPlayerData);
playerData.save();
CommandHandler.sendMessage(
sender, translate(sender, "commands.trialAvatarActivity.success_reward_all"));
}
}
}
targetPlayer.sendPacket(
new PacketActivityInfoNotify(
handler.toProto(playerData, targetPlayer.getActivityManager().getConditionExecutor())));
}
}

View File

@@ -0,0 +1,46 @@
package emu.grasscutter.command.commands;
import emu.grasscutter.BuildConfig;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.tools.Tools;
import java.util.List;
@Command(label = "troubleshoot", aliases = {"helpme"},
usage = "/troubleshoot", permission = "grasscutter.command.troubleshoot",
targetRequirement = Command.TargetRequirement.NONE)
public final class TroubleshootCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
// Collect server information.
var build = "%s (%s)".formatted(
BuildConfig.VERSION, BuildConfig.GIT_HASH);
var playerCount = Grasscutter.getGameServer()
.getPlayers().size();
var resourceInfo = Tools.resourcesInfo();
// Collect configuration information.
var config = Grasscutter.getConfig();
var gameOptions = config.server.game;
var questingEnabled = gameOptions.gameOptions.questing.enabled;
var scriptsEnabled = gameOptions.enableScriptInBigWorld;
// TODO: Send to remote server (Grasscutter API) and send dump link.
CommandHandler.sendMessage(sender, """
Troubleshooting/Debug Information
Revision: %s
Player Count: %d
Questing Enabled: %s
Scripts Enabled: %s
Operating System: %s
Resource Information: %s"""
.formatted(
build, playerCount, questingEnabled, scriptsEnabled,
System.getProperty("os.name"), resourceInfo.toString()
)
);
}
}

View File

@@ -1,17 +1,15 @@
package emu.grasscutter.command.commands;
import java.util.List;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.Account;
import emu.grasscutter.game.player.Player;
import java.util.List;
@Command(
label = "unban",
permission = "server.ban",
targetRequirement = Command.TargetRequirement.PLAYER
)
label = "unban",
permission = "server.ban",
targetRequirement = Command.TargetRequirement.PLAYER)
public final class UnBanCommand implements CommandHandler {
private boolean unBanAccount(Player targetPlayer) {

View File

@@ -1,19 +1,22 @@
package emu.grasscutter.command.commands;
import static emu.grasscutter.utils.lang.Language.translate;
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.player.PlayerProgressManager;
import emu.grasscutter.server.packet.send.PacketOpenStateChangeNotify;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "unlockall", usage = {""}, permission = "player.unlockall", permissionTargeted = "player.unlockall.others")
@Command(
label = "unlockall",
usage = {""},
permission = "player.unlockall",
permissionTargeted = "player.unlockall.others")
public final class UnlockAllCommand implements CommandHandler {
@Override
@@ -34,6 +37,7 @@ public final class UnlockAllCommand implements CommandHandler {
targetPlayer.sendPacket(new PacketOpenStateChangeNotify(changed));
CommandHandler.sendMessage(sender, translate(sender, "commands.unlockall.success", targetPlayer.getNickname()));
CommandHandler.sendMessage(
sender, translate(sender, "commands.unlockall.success", targetPlayer.getNickname()));
}
}

View File

@@ -4,20 +4,28 @@ import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.game.props.ClimateType;
import java.util.List;
@Command(label = "weather", aliases = {"w"}, usage = {"weather [<weatherId>] [<climateType>]"}, permission = "player.weather", permissionTargeted = "player.weather.others")
@Command(
label = "weather",
aliases = {"w"},
usage = {"weather [<weatherId>] [<climateType>]"},
permission = "player.weather",
permissionTargeted = "player.weather.others")
public final class WeatherCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
int weatherId = targetPlayer.getWeatherId();
ClimateType climate = ClimateType.CLIMATE_NONE; // Sending ClimateType.CLIMATE_NONE to Scene.setWeather will use the default climate for that weather
ClimateType climate =
ClimateType
.CLIMATE_NONE; // Sending ClimateType.CLIMATE_NONE to Scene.setWeather will use the
// default climate for that weather
if (args.isEmpty()) {
climate = targetPlayer.getClimate();
CommandHandler.sendTranslatedMessage(sender, "commands.weather.status", weatherId, climate.getShortName());
CommandHandler.sendTranslatedMessage(
sender, "commands.weather.status", weatherId, climate.getShortName());
return;
}
@@ -37,7 +45,8 @@ public final class WeatherCommand implements CommandHandler {
}
targetPlayer.setWeather(weatherId, climate);
climate = targetPlayer.getClimate(); // Might be different to what we set
CommandHandler.sendTranslatedMessage(sender, "commands.weather.success", weatherId, climate.getShortName());
climate = targetPlayer.getClimate(); // Might be different to what we set
CommandHandler.sendTranslatedMessage(
sender, "commands.weather.success", weatherId, climate.getShortName());
}
}

View File

@@ -2,32 +2,42 @@ package emu.grasscutter.config;
import ch.qos.logback.classic.Level;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode;
import emu.grasscutter.Grasscutter.ServerRunMode;
import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.*;
import lombok.NoArgsConstructor;
import java.util.Set;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Locale;
import java.util.*;
import static emu.grasscutter.Grasscutter.config;
import static emu.grasscutter.Grasscutter.*;
/**
* *when your JVM fails*
*/
public class ConfigContainer {
/*
* Configuration changes:
* Version 5 - 'questing' has been changed from a boolean
* to a container of options ('questOptions').
* This field will be removed in future versions.
* Version 6 - 'questing' has been fully replaced with 'questOptions'.
* The field for 'legacyResources' has been removed.
* Version 7 - 'regionKey' is being added for authentication
* with the new dispatch server.
* Version 8 - 'server' is being added for enforcing handbook server
* addresses.
* Version 9 - 'limits' was added for handbook requests.
*/
private static int version() {
return 4;
return 9;
}
/**
* Attempts to update the server's existing configuration to the latest
* Attempts to update the server's existing configuration.
*/
public static void updateConfig() {
try { // Check if the server is using a legacy config.
JsonObject configObject = JsonUtils.loadToClass(Grasscutter.configFile.toPath(), JsonObject.class);
var configObject = JsonUtils.loadToClass(Grasscutter.configFile.toPath(), JsonObject.class);
if (!configObject.has("version")) {
Grasscutter.getLogger().info("Updating legacy ..");
Grasscutter.saveConfig(null);
@@ -41,9 +51,9 @@ public class ConfigContainer {
return;
// Create a new configuration instance.
ConfigContainer updated = new ConfigContainer();
var updated = new ConfigContainer();
// Update all configuration fields.
Field[] fields = ConfigContainer.class.getDeclaredFields();
var fields = ConfigContainer.class.getDeclaredFields();
Arrays.stream(fields).forEach(field -> {
try {
field.set(updated, field.get(config));
@@ -56,7 +66,7 @@ public class ConfigContainer {
Grasscutter.saveConfig(updated);
Grasscutter.loadConfig();
} catch (Exception exception) {
Grasscutter.getLogger().warn("Failed to inject the updated ", exception);
Grasscutter.getLogger().warn("Failed to save the updated configuration.", exception);
}
}
@@ -87,6 +97,7 @@ public class ConfigContainer {
public String packets = "./packets/";
public String scripts = "resources:Scripts/";
public String plugins = "./plugins/";
public String cache = "./cache/";
// UNUSED (potentially added later?)
// public String dumps = "./dumps/";
@@ -144,8 +155,9 @@ public class ConfigContainer {
public int accessPort = 0;
/* Entities within a certain range will be loaded for the player */
public int loadEntitiesForPlayerRange = 100;
public boolean enableScriptInBigWorld = false;
public int loadEntitiesForPlayerRange = 300;
/* Start in 'unstable-quests', Lua scripts will be enabled by default. */
public boolean enableScriptInBigWorld = true;
public boolean enableConsole = true;
/* Kcp internal work interval (milliseconds) */
@@ -153,19 +165,39 @@ public class ConfigContainer {
/* Controls whether packets should be logged in console or not */
public ServerDebugMode logPackets = ServerDebugMode.NONE;
/* Show packet payload in console or no (in any case the payload is shown in encrypted view) */
public Boolean isShowPacketPayload = false;
public boolean isShowPacketPayload = false;
/* Show annoying loop packets or no */
public Boolean isShowLoopPackets = false;
public boolean isShowLoopPackets = false;
public boolean cacheSceneEntitiesEveryRun = false;
public GameOptions gameOptions = new GameOptions();
public JoinOptions joinOptions = new JoinOptions();
public ConsoleAccount serverAccount = new ConsoleAccount();
public VisionOptions[] visionOptions = new VisionOptions[] {
new VisionOptions("VISION_LEVEL_NORMAL" , 80 , 20),
new VisionOptions("VISION_LEVEL_LITTLE_REMOTE" , 16 , 40),
new VisionOptions("VISION_LEVEL_REMOTE" , 1000 , 250),
new VisionOptions("VISION_LEVEL_SUPER" , 4000 , 1000),
new VisionOptions("VISION_LEVEL_NEARBY" , 40 , 20),
new VisionOptions("VISION_LEVEL_SUPER_NEARBY" , 20 , 20)
};
}
/* Data containers. */
public static class Dispatch {
public Region[] regions = {};
/* An array of servers. */
public List<Region> regions = List.of();
/* The URL used to make HTTP requests to the dispatch server. */
public String dispatchUrl = "ws://127.0.0.1:1111";
/* A unique key used for encryption. */
public byte[] encryptionKey = Crypto.createSessionKey(32);
/* A unique key used for authentication. */
public String dispatchKey = Utils.base64Encode(
Crypto.createSessionKey(32));
public String defaultName = "Grasscutter";
@@ -174,23 +206,23 @@ public class ConfigContainer {
}
/* Debug options container, used when jar launch argument is -debug | -debugall and override default values
* (see StartupArguments.enableDebug) */
* (see StartupArguments.enableDebug) */
public static class DebugMode {
/* Log level of the main server code (works only with -debug arg) */
public Level serverLoggerLevel = Level.DEBUG;
/* Log level of the third-party services (works only with -debug arg):
javalin, quartz, reflections, jetty, mongodb.driver*/
javalin, quartz, reflections, jetty, mongodb.driver */
public Level servicesLoggersLevel = Level.INFO;
/* Controls whether packets should be logged in console or not */
public ServerDebugMode logPackets = ServerDebugMode.ALL;
/* Show packet payload in console or no (in any case the payload is shown in encrypted view) */
public Boolean isShowPacketPayload = false;
public boolean isShowPacketPayload = false;
/* Show annoying loop packets or no */
public Boolean isShowLoopPackets = false;
public boolean isShowLoopPackets = false;
/* Controls whether http requests should be logged in console or not */
public ServerDebugMode logRequests = ServerDebugMode.ALL;
@@ -208,7 +240,7 @@ public class ConfigContainer {
public Policies.CORS cors = new Policies.CORS();
public static class CORS {
public boolean enabled = false;
public boolean enabled = true;
public String[] allowedOrigins = new String[]{"*"};
}
}
@@ -223,9 +255,13 @@ public class ConfigContainer {
public boolean staminaUsage = true;
public boolean energyUsage = true;
public boolean fishhookTeleport = true;
@SerializedName(value = "questing", alternate = "questOptions")
public Questing questing = new Questing();
public ResinOptions resinOptions = new ResinOptions();
public Rates rates = new Rates();
public HandbookOptions handbook = new HandbookOptions();
public static class InventoryLimits {
public int weapons = 2000;
public int relics = 2000;
@@ -250,6 +286,54 @@ public class ConfigContainer {
public int cap = 160;
public int rechargeTime = 480;
}
public static class Questing {
/* Should questing behavior be used? */
public boolean enabled = true;
}
public static class HandbookOptions {
public boolean enable = false;
public boolean allowCommands = true;
public Limits limits = new Limits();
public Server server = new Server();
public static class Limits {
/* Are rate limits checked? */
public boolean enabled = false;
/* The time for limits to expire. */
public int interval = 3;
/* The maximum amount of normal requests. */
public int maxRequests = 10;
/* The maximum amount of entities to be spawned in one request. */
public int maxEntities = 25;
}
public static class Server {
/* Are the server settings sent to the handbook? */
public boolean enforced = false;
/* The default server address for the handbook's authentication. */
public String address = "127.0.0.1";
/* The default server port for the handbook's authentication. */
public int port = 443;
/* Should the defaults be enforced? */
public boolean canChange = true;
}
}
}
public static class VisionOptions {
public String name;
public int visionRange;
public int gridWidth;
public VisionOptions(String name, int visionRange, int gridWidth) {
this.name = name;
this.visionRange = visionRange;
this.gridWidth = gridWidth;
}
}
public static class JoinOptions {
@@ -268,8 +352,8 @@ public class ConfigContainer {
""";
public String sender = "Lawnmower";
public emu.grasscutter.game.mail.Mail.MailItem[] items = {
new emu.grasscutter.game.mail.Mail.MailItem(13509, 1, 1),
new emu.grasscutter.game.mail.Mail.MailItem(201, 99999, 1)
new emu.grasscutter.game.mail.Mail.MailItem(13509, 1, 1),
new emu.grasscutter.game.mail.Mail.MailItem(201, 99999, 1)
};
}
}
@@ -291,22 +375,21 @@ public class ConfigContainer {
/* Objects. */
@NoArgsConstructor
public static class Region {
public Region() { }
public String Name = "os_usa";
public String Title = "Grasscutter";
public String Ip = "127.0.0.1";
public int Port = 22102;
public Region(
String name, String title,
String address, int port
String name, String title,
String address, int port
) {
this.Name = name;
this.Title = title;
this.Ip = address;
this.Port = port;
}
public String Name = "os_usa";
public String Title = "Grasscutter";
public String Ip = "127.0.0.1";
public int Port = 22102;
}
}

View File

@@ -1,18 +1,15 @@
package emu.grasscutter.config;
import emu.grasscutter.utils.FileUtils;
import java.nio.file.Path;
import java.util.Locale;
import static emu.grasscutter.Grasscutter.config;
import emu.grasscutter.utils.FileUtils;
import java.nio.file.Path;
import java.util.Locale;
/**
* A data container for the server's configuration.
*
* Use `import static emu.grasscutter.Configuration.*;`
* to import all configuration constants.
* <p>Use `import static emu.grasscutter.Configuration.*;` to import all configuration constants.
*/
public final class Configuration extends ConfigContainer {
@@ -26,26 +23,25 @@ public final class Configuration extends ConfigContainer {
public static final Locale LANGUAGE = config.language.language;
public static final Locale FALLBACK_LANGUAGE = config.language.fallback;
public static final String DOCUMENT_LANGUAGE = config.language.document;
private static final String DATA_FOLDER = config.folderStructure.data;
private static final String PLUGINS_FOLDER = config.folderStructure.plugins;
private static final String SCRIPTS_FOLDER = config.folderStructure.scripts;
private static final String PACKETS_FOLDER = config.folderStructure.packets;
public static final Server SERVER = config.server;
public static final Database DATABASE = config.databaseInfo;
public static final Account ACCOUNT = config.account;
public static final HTTP HTTP_INFO = config.server.http;
public static final Game GAME_INFO = config.server.game;
public static final Dispatch DISPATCH_INFO = config.server.dispatch;
public static final DebugMode DEBUG_MODE_INFO = config.server.debugMode;
public static final Encryption HTTP_ENCRYPTION = config.server.http.encryption;
public static final Policies HTTP_POLICIES = config.server.http.policies;
public static final Files HTTP_STATIC_FILES = config.server.http.files;
public static final GameOptions GAME_OPTIONS = config.server.game.gameOptions;
public static final GameOptions.InventoryLimits INVENTORY_LIMITS = config.server.game.gameOptions.inventoryLimits;
public static final GameOptions.InventoryLimits INVENTORY_LIMITS =
config.server.game.gameOptions.inventoryLimits;
public static final GameOptions.HandbookOptions HANDBOOK =
config.server.game.gameOptions.handbook;
private static final String DATA_FOLDER = config.folderStructure.data;
private static final String PLUGINS_FOLDER = config.folderStructure.plugins;
private static final String SCRIPTS_FOLDER = config.folderStructure.scripts;
private static final String PACKETS_FOLDER = config.folderStructure.packets;
/*
* Utilities
@@ -91,6 +87,7 @@ public final class Configuration extends ConfigContainer {
/**
* Fallback method.
*
* @param left Attempt to use.
* @param right Use if left is undefined.
* @return Left or right.
@@ -101,6 +98,7 @@ public final class Configuration extends ConfigContainer {
/**
* {@link Configuration#lr(Object, Object)} for {@link String}s.
*
* @param left Attempt to use.
* @param right Use if left is empty.
* @return Left or right.
@@ -111,6 +109,7 @@ public final class Configuration extends ConfigContainer {
/**
* {@link Configuration#lr(Object, Object)} for {@link Integer}s.
*
* @param left Attempt to use.
* @param right Use if left is 0.
* @return Left or right.

View File

@@ -6,8 +6,6 @@ import emu.grasscutter.tools.Tools;
import emu.grasscutter.utils.FileUtils;
import emu.grasscutter.utils.JsonUtils;
import emu.grasscutter.utils.TsvUtils;
import lombok.val;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -16,11 +14,13 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import lombok.val;
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
*
* @param resourcePath The path to the data file to be loaded.
* @return InputStream of the data file.
@@ -32,7 +32,8 @@ public class DataLoader {
}
/**
* Creates an input stream reader for a data file. If the file isn't found within the /data directory then it will fallback to the default within the jar resources
* Creates an input stream reader for a data file. If the file isn't found within the /data
* directory then it will fallback to the default within the jar resources
*
* @param resourcePath The path to the data file to be loaded.
* @return InputStreamReader of the data file.
@@ -40,7 +41,8 @@ public class DataLoader {
* @throws FileNotFoundException
* @see #load(String, boolean)
*/
public static InputStreamReader loadReader(String resourcePath) throws IOException, FileNotFoundException {
public static InputStreamReader loadReader(String resourcePath)
throws IOException, FileNotFoundException {
try {
InputStream is = load(resourcePath, true);
return new InputStreamReader(is);
@@ -53,20 +55,22 @@ 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?
* @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.
* @throws FileNotFoundException
*/
public static InputStream load(String resourcePath, boolean useFallback) throws FileNotFoundException {
Path path = useFallback
? FileUtils.getDataPath(resourcePath)
: FileUtils.getDataUserPath(resourcePath);
public static InputStream load(String resourcePath, boolean useFallback)
throws FileNotFoundException {
Path path =
useFallback ? FileUtils.getDataPath(resourcePath) : FileUtils.getDataUserPath(resourcePath);
if (Files.exists(path)) {
// Data is in the resource directory
try {
return Files.newInputStream(path);
} catch (IOException e) {
throw new FileNotFoundException(e.getMessage()); // This is evil but so is changing the function signature at this point
throw new FileNotFoundException(
e.getMessage()); // This is evil but so is changing the function signature at this point
}
}
return null;
@@ -79,20 +83,22 @@ public class DataLoader {
}
public static <T> List<T> loadList(String resourcePath, Class<T> classType) throws IOException {
try (InputStreamReader reader = loadReader(resourcePath)) {
try (var reader = loadReader(resourcePath)) {
return JsonUtils.loadToList(reader, classType);
}
}
public static <T1,T2> Map<T1,T2> loadMap(String resourcePath, Class<T1> keyType, Class<T2> valueType) throws IOException {
public static <T1, T2> Map<T1, T2> loadMap(
String resourcePath, Class<T1> keyType, Class<T2> valueType) throws IOException {
try (InputStreamReader reader = loadReader(resourcePath)) {
return JsonUtils.loadToMap(reader, keyType, valueType);
}
}
public static <T> List<T> loadTableToList(String resourcePath, Class<T> classType) throws IOException {
public static <T> List<T> loadTableToList(String resourcePath, Class<T> classType)
throws IOException {
val path = FileUtils.getDataPathTsjJsonTsv(resourcePath);
Grasscutter.getLogger().debug("Loading data table from: "+path);
Grasscutter.getLogger().debug("Loading data table from: " + path);
return switch (FileUtils.getFileExtension(path)) {
case "json" -> JsonUtils.loadToList(path, classType);
case "tsj" -> TsvUtils.loadTsjToListSetField(path, classType);
@@ -107,7 +113,7 @@ public class DataLoader {
if (filenames == null) {
Grasscutter.getLogger().error("We were unable to locate your default data files.");
} //else for (Path file : filenames) {
} // else for (Path file : filenames) {
// String relativePath = String.valueOf(file).split("defaults[\\\\\\/]data[\\\\\\/]")[1];
// checkAndCopyData(relativePath);

View File

@@ -1,154 +1,547 @@
package emu.grasscutter.data;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.binout.*;
import emu.grasscutter.game.quest.QuestEncryptionKey;
import emu.grasscutter.utils.Utils;
import emu.grasscutter.data.binout.config.*;
import emu.grasscutter.data.binout.routes.Route;
import emu.grasscutter.data.custom.TrialAvatarActivityCustomData;
import emu.grasscutter.data.custom.TrialAvatarCustomData;
import emu.grasscutter.data.excels.*;
import it.unimi.dsi.fastutil.ints.Int2IntMap;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntSet;
import emu.grasscutter.data.excels.achievement.AchievementData;
import emu.grasscutter.data.excels.achievement.AchievementGoalData;
import emu.grasscutter.data.excels.activity.ActivityCondExcelConfigData;
import emu.grasscutter.data.excels.activity.ActivityData;
import emu.grasscutter.data.excels.activity.ActivityShopData;
import emu.grasscutter.data.excels.activity.ActivityWatcherData;
import emu.grasscutter.data.excels.avatar.*;
import emu.grasscutter.data.excels.codex.*;
import emu.grasscutter.data.excels.dungeon.*;
import emu.grasscutter.data.excels.monster.MonsterCurveData;
import emu.grasscutter.data.excels.monster.MonsterData;
import emu.grasscutter.data.excels.monster.MonsterDescribeData;
import emu.grasscutter.data.excels.monster.MonsterSpecialNameData;
import emu.grasscutter.data.excels.quest.QuestData;
import emu.grasscutter.data.excels.quest.QuestGlobalVarData;
import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData;
import emu.grasscutter.data.excels.reliquary.ReliquaryLevelData;
import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData;
import emu.grasscutter.data.excels.reliquary.ReliquarySetData;
import emu.grasscutter.data.excels.tower.TowerFloorData;
import emu.grasscutter.data.excels.tower.TowerLevelData;
import emu.grasscutter.data.excels.tower.TowerScheduleData;
import emu.grasscutter.data.excels.trial.*;
import emu.grasscutter.data.excels.weapon.WeaponCurveData;
import emu.grasscutter.data.excels.weapon.WeaponLevelData;
import emu.grasscutter.data.excels.weapon.WeaponPromoteData;
import emu.grasscutter.data.excels.world.WeatherData;
import emu.grasscutter.data.excels.world.WorldAreaData;
import emu.grasscutter.data.excels.world.WorldLevelData;
import emu.grasscutter.data.server.ActivityCondGroup;
import emu.grasscutter.data.server.GadgetMapping;
import emu.grasscutter.data.server.MonsterMapping;
import emu.grasscutter.game.dungeons.DungeonDropEntry;
import emu.grasscutter.game.quest.QuestEncryptionKey;
import emu.grasscutter.game.quest.RewindData;
import emu.grasscutter.game.quest.TeleportData;
import emu.grasscutter.game.quest.enums.QuestCond;
import emu.grasscutter.game.world.GroupReplacementData;
import emu.grasscutter.utils.Utils;
import it.unimi.dsi.fastutil.ints.*;
import java.lang.reflect.Field;
import java.util.*;
import javax.annotation.Nullable;
import lombok.Getter;
import lombok.experimental.Tolerate;
import lombok.Setter;
import lombok.val;
@SuppressWarnings({"unused", "MismatchedQueryAndUpdateOfCollection"})
public final class GameData {
@Getter private static final Map<String, AbilityData> abilityDataMap = new HashMap<>();
@Getter
private static final Int2ObjectMap<ScenePointEntry> scenePointEntryMap =
new Int2ObjectOpenHashMap<>();
public class GameData {
// BinOutputs
@Getter private static final Int2ObjectMap<HomeworldDefaultSaveData> homeworldDefaultSaveData = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<HomeworldDefaultSaveData> homeworldDefaultSaveData =
new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<String> abilityHashes = new Int2ObjectOpenHashMap<>();
@Deprecated(forRemoval = true)
@Getter private static final Map<String, AbilityModifierEntry> abilityModifiers = new HashMap<>();
@Getter private static final Map<String, ConfigGadget> gadgetConfigData = new HashMap<>();
@Getter private static final Map<String, List<TalentData>> talents = new HashMap<>();
@Getter private static final Map<String, ConfigEntityAvatar> avatarConfigData = new HashMap<>();
@Getter private static final Map<String, ConfigEntityGadget> gadgetConfigData = new HashMap<>();
@Getter private static final Map<String, ConfigEntityMonster> monsterConfigData = new HashMap<>();
@Getter private static final Map<String, OpenConfigEntry> openConfigEntries = new HashMap<>();
@Deprecated(forRemoval = true) @Getter private static final Map<String, ScenePointEntry> scenePointEntries = new HashMap<>();
protected static final Map<String, AbilityData> abilityDataMap = new HashMap<>();
protected static final Int2ObjectMap<ScenePointEntry> scenePointEntryMap = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<MainQuestData> mainQuestData = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<QuestEncryptionKey> questsKeys = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<SceneNpcBornData> npcBornData = new Int2ObjectOpenHashMap<>();
private static final Map<String, AbilityEmbryoEntry> abilityEmbryos = new HashMap<>();
// ExcelConfigs
@Getter private static final ArrayList<CodexReliquaryData> codexReliquaryArrayList = new ArrayList<>();
private static final Int2ObjectMap<AchievementData> achievementDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<AchievementGoalData> achievementGoalDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ActivityData> activityDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ActivityShopData> activityShopDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ActivityWatcherData> activityWatcherDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarCostumeData> avatarCostumeDataItemIdMap = new Int2ObjectLinkedOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarCostumeData> avatarCostumeDataMap = new Int2ObjectLinkedOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarCurveData> avatarCurveDataMap = new Int2ObjectLinkedOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarData> avatarDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarFetterLevelData> avatarFetterLevelDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarFlycloakData> avatarFlycloakDataMap = new Int2ObjectLinkedOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarLevelData> avatarLevelDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarSkillData> avatarSkillDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarSkillDepotData> avatarSkillDepotDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<AvatarTalentData> avatarTalentDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<BattlePassMissionData> battlePassMissionDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<BattlePassRewardData> battlePassRewardDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<BlossomRefreshExcelConfigData> blossomRefreshExcelConfigDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ActivityCondExcelConfigData> activityCondExcelConfigDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<DungeonPassConfigData> dungeonPassConfigDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<DungeonChallengeConfigData> dungeonChallengeConfigDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<Int2ObjectMap<Route>> sceneRouteData =
new Int2ObjectOpenHashMap<>();
@Getter
private static final ArrayList<CodexReliquaryData> codexReliquaryArrayList = new ArrayList<>();
private static final Int2ObjectMap<AchievementData> achievementDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AchievementGoalData> achievementGoalDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ActivityData> activityDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ActivityShopData> activityShopDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ActivityWatcherData> activityWatcherDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarCostumeData> avatarCostumeDataItemIdMap =
new Int2ObjectLinkedOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarCostumeData> avatarCostumeDataMap =
new Int2ObjectLinkedOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarReplaceCostumeData> avatarReplaceCostumeDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarCurveData> avatarCurveDataMap =
new Int2ObjectLinkedOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarData> avatarDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarFetterLevelData> avatarFetterLevelDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarFlycloakData> avatarFlycloakDataMap =
new Int2ObjectLinkedOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarLevelData> avatarLevelDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarSkillData> avatarSkillDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarSkillDepotData> avatarSkillDepotDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<AvatarTalentData> avatarTalentDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<BattlePassMissionData> battlePassMissionDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<BattlePassRewardData> battlePassRewardDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<BlossomRefreshExcelConfigData>
blossomRefreshExcelConfigDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<BuffData> buffDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ChapterData> chapterDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ChapterData> chapterDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CityData> cityDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CodexAnimalData> codexAnimalDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CodexMaterialData> codexMaterialDataIdMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CodexQuestData> codexQuestDataIdMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CodexReliquaryData> codexReliquaryDataIdMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CodexWeaponData> codexWeaponDataIdMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CombineData> combineDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CookBonusData> cookBonusDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<CompoundData> compoundDataMap=new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<DailyDungeonData> dailyDungeonDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<DungeonData> dungeonDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<DungeonEntryData> dungeonEntryDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<EnvAnimalGatherConfigData> envAnimalGatherConfigDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<EquipAffixData> equipAffixDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<FetterCharacterCardData> fetterCharacterCardDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ForgeData> forgeDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<FurnitureMakeConfigData> furnitureMakeConfigDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<GadgetData> gadgetDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<GatherData> gatherDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<HomeWorldBgmData> homeWorldBgmDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<HomeWorldLevelData> homeWorldLevelDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<InvestigationMonsterData> investigationMonsterDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CodexAnimalData> codexAnimalDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CodexMaterialData> codexMaterialDataIdMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CodexQuestData> codexQuestDataIdMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CodexReliquaryData> codexReliquaryDataIdMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CodexWeaponData> codexWeaponDataIdMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CombineData> combineDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CookBonusData> cookBonusDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CookRecipeData> cookRecipeDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<CompoundData> compoundDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<DailyDungeonData> dailyDungeonDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<DropTableData> dropTableDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<DropMaterialData> dropMaterialDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<DungeonData> dungeonDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<DungeonEntryData> dungeonEntryDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<EnvAnimalGatherConfigData> envAnimalGatherConfigDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<EquipAffixData> equipAffixDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<MonsterAffixData> monsterAffixDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<FetterCharacterCardData> fetterCharacterCardDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ForgeData> forgeDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<FurnitureMakeConfigData> furnitureMakeConfigDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<GadgetData> gadgetDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<GatherData> gatherDataMap = new Int2ObjectOpenHashMap<>();
@Getter
@Deprecated // This is to prevent people from using this map. This is for the resource loader
// only!
private static final Int2ObjectMap<GuideTriggerData> guideTriggerDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<HomeWorldBgmData> homeWorldBgmDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<HomeWorldLevelData> homeWorldLevelDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<InvestigationMonsterData> investigationMonsterDataMap =
new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ItemData> itemDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<MonsterCurveData> monsterCurveDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<MonsterData> monsterDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<MonsterDescribeData> monsterDescribeDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<MonsterSpecialNameData> monsterSpecialNameDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<MusicGameBasicData> musicGameBasicDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<MonsterCurveData> monsterCurveDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<MonsterData> monsterDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<MonsterDescribeData> monsterDescribeDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<MonsterSpecialNameData> monsterSpecialNameDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<MusicGameBasicData> musicGameBasicDataMap =
new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<NpcData> npcDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<OpenStateData> openStateDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<PersonalLineData> personalLineDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<PlayerLevelData> playerLevelDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ProudSkillData> proudSkillDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<QuestData> questDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ReliquaryAffixData> reliquaryAffixDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ReliquaryMainPropData> reliquaryMainPropDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<ReliquarySetData> reliquarySetDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<RewardData> rewardDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<RewardPreviewData> rewardPreviewDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<SceneData> sceneDataMap = new Int2ObjectLinkedOpenHashMap<>();
@Getter private static final Int2ObjectMap<TowerFloorData> towerFloorDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<TowerLevelData> towerLevelDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<TowerScheduleData> towerScheduleDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<TriggerExcelConfigData> triggerExcelConfigDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<WeaponCurveData> weaponCurveDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<WeaponLevelData> weaponLevelDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<WeaponPromoteData> weaponPromoteDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<WeatherData> weatherDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<WorldAreaData> worldAreaDataMap = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2ObjectMap<WorldLevelData> worldLevelDataMap = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<AvatarPromoteData> avatarPromoteDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<OpenStateData> openStateDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<PersonalLineData> personalLineDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<PlayerLevelData> playerLevelDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ProudSkillData> proudSkillDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<QuestData> questDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<QuestGlobalVarData> questGlobalVarDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ReliquaryAffixData> reliquaryAffixDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ReliquaryMainPropData> reliquaryMainPropDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ReliquarySetData> reliquarySetDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<RewardData> rewardDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<RewardPreviewData> rewardPreviewDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<SceneData> sceneDataMap = new Int2ObjectLinkedOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TalkConfigData> talkConfigDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TowerFloorData> towerFloorDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TowerLevelData> towerLevelDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TowerScheduleData> towerScheduleDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TriggerExcelConfigData> triggerExcelConfigDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TrialAvatarData> trialAvatarDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TrialAvatarActivityData> trialAvatarActivityDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TrialAvatarActivityDataData> trialAvatarActivityDataDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TrialAvatarTemplateData> trialAvatarTemplateDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TrialReliquaryData> trialReliquaryDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<WeaponCurveData> weaponCurveDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<WeaponLevelData> weaponLevelDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<WeaponPromoteData> weaponPromoteDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<WeatherData> weatherDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<WorldAreaData> worldAreaDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<WorldLevelData> worldLevelDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<RewindData> rewindDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<TeleportData> teleportDataMap = new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<RefreshPolicyExcelConfigData> refreshPolicyExcelConfigDataMap =
new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<AvatarPromoteData> avatarPromoteDataMap =
new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<FetterData> fetterDataMap = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<ReliquaryLevelData> reliquaryLevelDataMap = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<ShopGoodsData> shopGoodsDataMap = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<ReliquaryLevelData> reliquaryLevelDataMap =
new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<ShopGoodsData> shopGoodsDataMap =
new Int2ObjectOpenHashMap<>();
// The following are accessed via getMapByResourceDef, and will show as unused
private static final Int2ObjectMap<CodexMaterialData> codexMaterialDataMap = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<CodexQuestData> codexQuestDataMap = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<CodexReliquaryData> codexReliquaryDataMap = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<CodexWeaponData> codexWeaponDataMap = new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<CodexMaterialData> codexMaterialDataMap =
new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<CodexQuestData> codexQuestDataMap =
new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<CodexReliquaryData> codexReliquaryDataMap =
new Int2ObjectOpenHashMap<>();
private static final Int2ObjectMap<CodexWeaponData> codexWeaponDataMap =
new Int2ObjectOpenHashMap<>();
@Getter @Setter private static ConfigGlobalCombat configGlobalCombat = null;
// Custom community server resources
@Getter
private static final Int2ObjectMap<List<DungeonDropEntry>> dungeonDropDataMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<GadgetMapping> gadgetMappingMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<MonsterMapping> monsterMappingMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<ActivityCondGroup> activityCondGroupMap =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Int2ObjectMap<GroupReplacementData> groupReplacements =
new Int2ObjectOpenHashMap<>();
// Cache
@Getter private static final IntList scenePointIdList = new IntArrayList();
@Getter private static final List<OpenStateData> openStateList = new ArrayList<>();
@Getter private static final Map<Integer, List<Integer>> scenePointsPerScene = new HashMap<>();
@Getter private static final Map<String, ScriptSceneData> scriptSceneDataMap = new HashMap<>();
@Getter
private static final Map<String, GuideTriggerData> guideTriggerDataStringMap = new HashMap<>();
@Getter
private static final Map<String, ConfigLevelEntity> configLevelEntityDataMap = new HashMap<>();
@Getter
private static final Int2ObjectMap<IntSet> proudSkillGroupLevels = new Int2ObjectOpenHashMap<>();
@Getter private static final Int2IntMap proudSkillGroupMaxLevels = new Int2IntOpenHashMap();
@Getter
private static final Int2ObjectMap<IntSet> avatarSkillLevels = new Int2ObjectOpenHashMap<>();
@Getter
private static final Map<String, List<QuestData>> beginCondQuestMap =
new HashMap<>(); // cache filled by QuestData
@Getter private static final Map<Integer, Integer> questTalkMap = new HashMap<>();
@Getter
private static final Int2ObjectMap<TrialAvatarCustomData> trialAvatarCustomData =
new Int2ObjectOpenHashMap<>();
@Getter
private static final Map<Integer, TrialAvatarActivityCustomData> trialAvatarActivityCustomData =
new HashMap<>();
@Getter
private static final Map<Integer, TrialAvatarActivityDataData> trialAvatarActivityDataCustomData =
new HashMap<>();
@Getter
private static final Int2IntMap trialAvatarIndexIdTrialActivityDataDataMap =
new Int2IntOpenHashMap();
private static Map<Integer, List<Integer>> fetters = new HashMap<>();
private static Map<Integer, List<ShopGoodsData>> shopGoods = new HashMap<>();
protected static Int2ObjectMap<IntSet> proudSkillGroupLevels = new Int2ObjectOpenHashMap<>();
protected static Int2IntMap proudSkillGroupMaxLevels = new Int2IntOpenHashMap();
protected static Int2ObjectMap<IntSet> avatarSkillLevels = new Int2ObjectOpenHashMap<>();
// Getters with wrong names, remove later
@Deprecated(forRemoval = true) public static Int2ObjectMap<CodexReliquaryData> getcodexReliquaryIdMap() {return codexReliquaryDataIdMap;}
@Deprecated(forRemoval = true) public static Int2ObjectMap<DungeonEntryData> getDungeonEntryDatatMap() {return dungeonEntryDataMap;}
@Deprecated(forRemoval = true) @Tolerate public static ArrayList<CodexReliquaryData> getcodexReliquaryArrayList() {return codexReliquaryArrayList;}
// Getters with different names that stay for now
public static Int2ObjectMap<MainQuestData> getMainQuestDataMap() {return mainQuestData;}
public static Int2ObjectMap<QuestEncryptionKey> getMainQuestEncryptionMap() {return questsKeys;}
public static Int2ObjectMap<SceneNpcBornData> getSceneNpcBornData() {return npcBornData;}
public static Map<String, AbilityEmbryoEntry> getAbilityEmbryoInfo() {return abilityEmbryos;}
public static Int2ObjectMap<MainQuestData> getMainQuestDataMap() {
return mainQuestData;
}
// Getters that get values rather than containers. If Lombok ever gets syntactic sugar for this, we should adopt that.
public static AbilityData getAbilityData(String abilityName) {return abilityDataMap.get(abilityName);}
public static IntSet getAvatarSkillLevels(int avatarSkillId) {return avatarSkillLevels.get(avatarSkillId);}
public static IntSet getProudSkillGroupLevels(int proudSkillGroupId) {return proudSkillGroupLevels.get(proudSkillGroupId);}
public static int getProudSkillGroupMaxLevel(int proudSkillGroupId) {return proudSkillGroupMaxLevels.getOrDefault(proudSkillGroupId, 0);}
public static Int2ObjectMap<QuestEncryptionKey> getMainQuestEncryptionMap() {
return questsKeys;
}
public static Int2ObjectMap<SceneNpcBornData> getSceneNpcBornData() {
return npcBornData;
}
public static Map<String, AbilityEmbryoEntry> getAbilityEmbryoInfo() {
return abilityEmbryos;
}
// Getters that get values rather than containers. If Lombok ever gets syntactic sugar for this,
// we should adopt that.
public static AbilityData getAbilityData(String abilityName) {
return abilityDataMap.get(abilityName);
}
public static IntSet getAvatarSkillLevels(int avatarSkillId) {
return avatarSkillLevels.get(avatarSkillId);
}
public static IntSet getProudSkillGroupLevels(int proudSkillGroupId) {
return proudSkillGroupLevels.get(proudSkillGroupId);
}
public static int getProudSkillGroupMaxLevel(int proudSkillGroupId) {
return proudSkillGroupMaxLevels.getOrDefault(proudSkillGroupId, 0);
}
// Multi-keyed getters
public static AvatarPromoteData getAvatarPromoteData(int promoteId, int promoteLevel) {
@@ -180,26 +573,26 @@ public class GameData {
return Optional.ofNullable(getRelicLevelData(rankLevel, level)).map(d -> d.getExp()).orElse(0);
}
// Generic getter
public static Int2ObjectMap<?> getMapByResourceDef(Class<?> resourceDefinition) {
Int2ObjectMap<?> map = null;
try {
Field field = GameData.class.getDeclaredField(Utils.lowerCaseFirstChar(resourceDefinition.getSimpleName()) + "Map");
Field field =
GameData.class.getDeclaredField(
Utils.lowerCaseFirstChar(resourceDefinition.getSimpleName()) + "Map");
field.setAccessible(true);
map = (Int2ObjectMap<?>) field.get(null);
field.setAccessible(false);
} catch (Exception e) {
Grasscutter.getLogger().error("Error fetching resource map for " + resourceDefinition.getSimpleName(), e);
Grasscutter.getLogger()
.error("Error fetching resource map for " + resourceDefinition.getSimpleName(), e);
}
return map;
}
public static int getWeaponExpRequired(int rankLevel, int level) {
WeaponLevelData levelData = weaponLevelDataMap.get(level);
if (levelData == null) {
@@ -214,12 +607,13 @@ public class GameData {
public static Map<Integer, List<Integer>> getFetterDataEntries() {
if (fetters.isEmpty()) {
fetterDataMap.forEach((k, v) -> {
if (!fetters.containsKey(v.getAvatarId())) {
fetters.put(v.getAvatarId(), new ArrayList<>());
}
fetters.get(v.getAvatarId()).add(k);
});
fetterDataMap.forEach(
(k, v) -> {
if (!fetters.containsKey(v.getAvatarId())) {
fetters.put(v.getAvatarId(), new ArrayList<>());
}
fetters.get(v.getAvatarId()).add(k);
});
}
return fetters;
@@ -227,16 +621,49 @@ public class GameData {
public static Map<Integer, List<ShopGoodsData>> getShopGoodsDataEntries() {
if (shopGoods.isEmpty()) {
shopGoodsDataMap.forEach((k, v) -> {
if (!shopGoods.containsKey(v.getShopType()))
shopGoods.put(v.getShopType(), new ArrayList<>());
shopGoods.get(v.getShopType()).add(v);
});
shopGoodsDataMap.forEach(
(k, v) -> {
if (!shopGoods.containsKey(v.getShopType()))
shopGoods.put(v.getShopType(), new ArrayList<>());
shopGoods.get(v.getShopType()).add(v);
});
}
return shopGoods;
}
/**
* Fetches the route data for a scene by ID.
*
* @param sceneId The ID of the scene to fetch the route data for.
* @return The route data for the scene, or an empty map if the scene has no route data.
*/
public static Int2ObjectMap<Route> getSceneRoutes(int sceneId) {
return sceneRouteData.computeIfAbsent(sceneId, k -> new Int2ObjectOpenHashMap<>());
}
/**
* Fetches the trial data
*
* @param trialAvatarIndexId
* @return
*/
@Nullable public static TrialAvatarActivityDataData getTrialAvatarActivityDataByAvatarIndex(
int trialAvatarIndexId) {
// prefer custom data over official data
val dataId = trialAvatarIndexIdTrialActivityDataDataMap.get(trialAvatarIndexId);
val datamap =
GameData.getTrialAvatarActivityDataCustomData().isEmpty()
? GameData.getTrialAvatarActivityDataDataMap()
: GameData.getTrialAvatarActivityDataCustomData();
return datamap.get(dataId);
}
@Nullable public static List<QuestData> getQuestDataByConditions(
QuestCond questCond, int param0, String questStr) {
return beginCondQuestMap.get(QuestData.questConditionKey(questCond, param0, questStr));
}
public static Int2ObjectMap<AchievementData> getAchievementDataMap() {
AchievementData.divideIntoGroups();
return achievementDataMap;

View File

@@ -1,32 +1,37 @@
package emu.grasscutter.data;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.ResourceLoader.AvatarConfig;
import emu.grasscutter.data.excels.reliquary.ReliquaryAffixData;
import emu.grasscutter.data.excels.reliquary.ReliquaryMainPropData;
import emu.grasscutter.game.managers.blossom.BlossomConfig;
import emu.grasscutter.game.world.SpawnDataEntry;
import emu.grasscutter.utils.objects.WeightedList;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import emu.grasscutter.Grasscutter;
import emu.grasscutter.data.ResourceLoader.AvatarConfig;
import emu.grasscutter.data.excels.ReliquaryAffixData;
import emu.grasscutter.data.excels.ReliquaryMainPropData;
import emu.grasscutter.game.managers.blossom.BlossomConfig;
import emu.grasscutter.game.world.SpawnDataEntry;
import emu.grasscutter.utils.WeightedList;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import lombok.Getter;
import lombok.Setter;
public class GameDepot {
public static final int[] BLOCK_SIZE = new int[]{50,500};//Scales
public static final int[] BLOCK_SIZE = new int[] {50, 500}; // Scales
private static Int2ObjectMap<WeightedList<ReliquaryMainPropData>> relicRandomMainPropDepot = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<List<ReliquaryMainPropData>> relicMainPropDepot = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<List<ReliquaryAffixData>> relicAffixDepot = new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<WeightedList<ReliquaryMainPropData>> relicRandomMainPropDepot =
new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<List<ReliquaryMainPropData>> relicMainPropDepot =
new Int2ObjectOpenHashMap<>();
private static Int2ObjectMap<List<ReliquaryAffixData>> relicAffixDepot =
new Int2ObjectOpenHashMap<>();
@Getter @Setter private static Map<String, AvatarConfig> playerAbilities = new HashMap<>();
@Getter private static HashMap<SpawnDataEntry.GridBlockId, ArrayList<SpawnDataEntry>> spawnLists = new HashMap<>();
@Getter
private static HashMap<SpawnDataEntry.GridBlockId, ArrayList<SpawnDataEntry>> spawnLists =
new HashMap<>();
@Getter @Setter private static BlossomConfig blossomConfig;
public static void load() {
@@ -34,21 +39,27 @@ public class GameDepot {
if (data.getWeight() <= 0 || data.getPropDepotId() <= 0) {
continue;
}
List<ReliquaryMainPropData> list = relicMainPropDepot.computeIfAbsent(data.getPropDepotId(), k -> new ArrayList<>());
List<ReliquaryMainPropData> list =
relicMainPropDepot.computeIfAbsent(data.getPropDepotId(), k -> new ArrayList<>());
list.add(data);
WeightedList<ReliquaryMainPropData> weightedList = relicRandomMainPropDepot.computeIfAbsent(data.getPropDepotId(), k -> new WeightedList<>());
WeightedList<ReliquaryMainPropData> weightedList =
relicRandomMainPropDepot.computeIfAbsent(
data.getPropDepotId(), k -> new WeightedList<>());
weightedList.add(data.getWeight(), data);
}
for (ReliquaryAffixData data : GameData.getReliquaryAffixDataMap().values()) {
if (data.getWeight() <= 0 || data.getDepotId() <= 0) {
continue;
}
List<ReliquaryAffixData> list = relicAffixDepot.computeIfAbsent(data.getDepotId(), k -> new ArrayList<>());
List<ReliquaryAffixData> list =
relicAffixDepot.computeIfAbsent(data.getDepotId(), k -> new ArrayList<>());
list.add(data);
}
// Let the server owner know if theyre missing weights
if (relicMainPropDepot.size() == 0 || relicAffixDepot.size() == 0) {
Grasscutter.getLogger().error("Relic properties are missing weights! Please check your ReliquaryMainPropExcelConfigData or ReliquaryAffixExcelConfigData files in your ExcelBinOutput folder.");
Grasscutter.getLogger()
.error(
"Relic properties are missing weights! Please check your ReliquaryMainPropExcelConfigData or ReliquaryAffixExcelConfigData files in your ExcelBinOutput folder.");
}
}
@@ -68,7 +79,8 @@ public class GameDepot {
return relicAffixDepot.get(depot);
}
public static void addSpawnListById(HashMap<SpawnDataEntry.GridBlockId, ArrayList<SpawnDataEntry>> data) {
public static void addSpawnListById(
HashMap<SpawnDataEntry.GridBlockId, ArrayList<SpawnDataEntry>> data) {
spawnLists.putAll(data);
}
}

View File

@@ -1,12 +1,10 @@
package emu.grasscutter.data;
public abstract class GameResource {
public int getId() {
return 0;
}
public void onLoad() {
}
public int getId() {
return 0;
}
public void onLoad() {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,34 +5,36 @@ import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.stream.Stream;
@Retention(RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResourceType {
/** Names of the file that this Resource loads from */
String[] name();
/** Load priority - dictates which order to load this resource, with "highest" being loaded first */
LoadPriority loadPriority() default LoadPriority.NORMAL;
public enum LoadPriority {
HIGHEST (4),
HIGH (3),
NORMAL (2),
LOW (1),
LOWEST (0);
/** Names of the file that this Resource loads from */
String[] name();
private final int value;
LoadPriority(int value) {
this.value = value;
}
public int value() {
return value;
}
/**
* Load priority - dictates which order to load this resource, with "highest" being loaded first
*/
LoadPriority loadPriority() default LoadPriority.NORMAL;
enum LoadPriority {
HIGHEST(4),
HIGH(3),
NORMAL(2),
LOW(1),
LOWEST(0);
private final int value;
LoadPriority(int value) {
this.value = value;
}
public static List<LoadPriority> getInOrder() {
return Stream.of(LoadPriority.values()).sorted((x, y) -> y.value() - x.value()).toList();
}
}
public int value() {
return value;
}
}
}

View File

@@ -1,5 +1,9 @@
package emu.grasscutter.data.binout;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import emu.grasscutter.game.ability.AbilityLocalIdGenerator;
import emu.grasscutter.game.ability.AbilityLocalIdGenerator.ConfigAbilitySubContainerType;
import java.util.HashMap;
import java.util.Map;
public class AbilityData {
@@ -7,7 +11,153 @@ public class AbilityData {
public Map<String, AbilityModifier> modifiers;
public boolean isDynamicAbility;
public Map<String, Float> abilitySpecials;
public AbilityModifierAction[] onAdded;
public AbilityModifierAction[] onRemoved;
public AbilityModifierAction[] onAbilityStart;
public AbilityModifierAction[] onKill;
public AbilityModifierAction[] onFieldEnter;
public AbilityModifierAction[] onExit;
public AbilityModifierAction[] onAttach;
public AbilityModifierAction[] onDetach;
public AbilityModifierAction[] onAvatarIn;
public AbilityModifierAction[] onAvatarOut;
public AbilityModifierAction[] onTriggerAvatarRay;
public AbilityModifierAction[] onVehicleIn;
public AbilityModifierAction[] onVehicleOut;
// abilityMixins
// onAbilityStart
// onKill
public AbilityMixinData[] abilityMixins;
public final Map<Integer, AbilityModifierAction> localIdToAction = new HashMap<>();
public final Map<Integer, AbilityMixinData> localIdToMixin = new HashMap<>();
private boolean _initialized = false;
public void initialize() {
if (_initialized) return;
_initialized = true;
initializeMixins();
initializeModifiers();
initializeActions();
}
private void initializeActions() {
AbilityLocalIdGenerator generator =
new AbilityLocalIdGenerator(ConfigAbilitySubContainerType.ACTION);
generator.configIndex = 0;
generator.initializeActionLocalIds(onAdded, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onRemoved, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onAbilityStart, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onKill, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onFieldEnter, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onExit, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onAttach, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onDetach, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onAvatarIn, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onAvatarOut, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onTriggerAvatarRay, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onVehicleIn, localIdToAction);
generator.configIndex++;
generator.initializeActionLocalIds(onVehicleOut, localIdToAction);
}
private void initializeMixins() {
if (abilityMixins != null) {
AbilityLocalIdGenerator generator =
new AbilityLocalIdGenerator(ConfigAbilitySubContainerType.MIXIN);
generator.modifierIndex = 0;
generator.configIndex = 0;
generator.initializeMixinsLocalIds(abilityMixins, localIdToMixin);
}
}
private void initializeModifiers() {
if (modifiers == null) return;
var _modifiers =
modifiers.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(Map.Entry::getValue)
.toList();
var modifierIndex = 0;
for (AbilityModifier abilityModifier : _modifiers) {
long configIndex = 0L;
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onAdded, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onRemoved, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onBeingHit, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onAttackLanded, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onHittingOther, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onThinkInterval, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onKill, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onCrash, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onAvatarIn, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onAvatarOut, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onReconnect, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onChangeAuthority, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onVehicleIn, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onVehicleOut, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onZoneEnter, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onZoneExit, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onHeal, localIdToAction);
this.initializeActionSubCategory(
modifierIndex, configIndex++, abilityModifier.onBeingHealed, localIdToAction);
if (abilityModifier.modifierMixins != null) {
var generator = new AbilityLocalIdGenerator(ConfigAbilitySubContainerType.MODIFIER_MIXIN);
generator.modifierIndex = modifierIndex;
generator.configIndex = 0;
generator.initializeMixinsLocalIds(abilityModifier.modifierMixins, localIdToMixin);
}
modifierIndex++;
}
}
private void initializeActionSubCategory(
long modifierIndex,
long configIndex,
AbilityModifierAction[] actions,
Map<Integer, AbilityModifierAction> localIdToAction) {
if (actions == null) return;
var generator = new AbilityLocalIdGenerator(ConfigAbilitySubContainerType.MODIFIER_ACTION);
generator.modifierIndex = modifierIndex;
generator.configIndex = configIndex;
generator.initializeActionLocalIds(actions, localIdToAction);
}
}

View File

@@ -1,23 +1,21 @@
package emu.grasscutter.data.binout;
public class AbilityEmbryoEntry {
private String name;
private String[] abilities;
public AbilityEmbryoEntry() {
private String name;
private String[] abilities;
}
public AbilityEmbryoEntry(String avatarName, String[] array) {
this.name = avatarName;
this.abilities = array;
}
public AbilityEmbryoEntry() {}
public String getName() {
return name;
}
public String[] getAbilities() {
return abilities;
}
public AbilityEmbryoEntry(String avatarName, String[] array) {
this.name = avatarName;
this.abilities = array;
}
public String getName() {
return name;
}
public String[] getAbilities() {
return abilities;
}
}

View File

@@ -0,0 +1,35 @@
package emu.grasscutter.data.binout;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
public class AbilityMixinData implements Serializable {
private static final long serialVersionUID = -2001232313615923575L;
public enum Type {
AttachToGadgetStateMixin,
AttachToStateIDMixin,
ShieldBarMixin,
TileAttackManagerMixin;
}
@SerializedName("$type")
public Type type;
private JsonElement modifierName;
public List<String> getModifierNames() {
if (modifierName.isJsonArray()) {
java.lang.reflect.Type listType = (new TypeToken<List<String>>() {}).getType();
List<String> list = (new Gson()).fromJson(modifierName, listType);
return list;
} else {
return Arrays.asList(modifierName.getAsString());
}
}
}

View File

@@ -1,95 +1,335 @@
package emu.grasscutter.data.binout;
import java.io.Serializable;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.data.common.DynamicFloat;
import emu.grasscutter.game.props.ElementType;
import java.io.Serializable;
import lombok.ToString;
public class AbilityModifier implements Serializable {
private static final long serialVersionUID = -2001232313615923575L;
@SerializedName(value="onAdded", alternate={"KCICDEJLIJD"})
@SerializedName(
value = "onAdded",
alternate = {"KCICDEJLIJD"})
public AbilityModifierAction[] onAdded;
@SerializedName(value="onThinkInterval", alternate={"PBDDACFFPOE"})
public AbilityModifierAction[] onThinkInterval;
public AbilityModifierAction[] onRemoved;
public DynamicFloat duration = DynamicFloat.ZERO;
public static class AbilityModifierAction {
@SerializedName(
value = "onThinkInterval",
alternate = {"PBDDACFFPOE"})
public AbilityModifierAction[] onThinkInterval;
public AbilityModifierAction[] onRemoved;
public AbilityModifierAction[] onBeingHit;
public AbilityModifierAction[] onAttackLanded;
public AbilityModifierAction[] onHittingOther;
public AbilityModifierAction[] onKill;
public AbilityModifierAction[] onCrash;
public AbilityModifierAction[] onAvatarIn;
public AbilityModifierAction[] onAvatarOut;
public AbilityModifierAction[] onReconnect;
public AbilityModifierAction[] onChangeAuthority;
public AbilityModifierAction[] onVehicleIn;
public AbilityModifierAction[] onVehicleOut;
public AbilityModifierAction[] onZoneEnter;
public AbilityModifierAction[] onZoneExit;
public AbilityModifierAction[] onHeal;
public AbilityModifierAction[] onBeingHealed;
public DynamicFloat duration = DynamicFloat.ZERO;
public DynamicFloat thinkInterval = DynamicFloat.ZERO;
public String stacking;
public AbilityMixinData[] modifierMixins;
public ElementType elementType;
public DynamicFloat elementDurability = DynamicFloat.ZERO;
@ToString
public static class AbilityModifierAction implements Serializable {
public enum Type {
ActCameraRadialBlur, ActCameraShake, AddAvatarSkillInfo, AddChargeBarValue,
AddClimateMeter, AddElementDurability, AddGlobalValue, AddGlobalValueToTarget,
AddRegionalPlayVarValue, ApplyModifier, AttachAbilityStateResistance, AttachBulletAimPoint,
AttachEffect, AttachEffectFirework, AttachElementTypeResistance, AttachModifier,
AttachUIEffect, AvatarCameraParam, AvatarEnterCameraShot, AvatarEnterFocus,
AvatarEnterViewBias, AvatarExitCameraShot, AvatarExitClimb, AvatarExitFocus,
AvatarExitViewBias, AvatarShareCDSkillStart, AvatarSkillStart, BroadcastNeuronStimulate,
CalcDvalinS04RebornPoint, CallLuaTask, ChangeEnviroWeather, ChangeFollowDampTime,
ChangeGadgetUIInteractHint, ChangePlayMode, ChangeTag, ChangeUGCRayTag,
ClearEndura, ClearGlobalPos, ClearGlobalValue, ClearLocalGadgets,
ClearLockTarget, ClearPos, ConfigAbilityAction, ControlEmotion,
CopyGlobalValue, CreateGadget, CreateMovingPlatform, CreateTile,
DamageByAttackValue, DebugLog, DestroyTile, DoBlink,
DoTileAction, DoWatcherSystemAction, DoWidgetSystemAction, DropSubfield,
DummyAction, DungeonFogEffects, ElementAttachForActivityGacha, EnableAIStealthy,
EnableAfterImage, EnableAvatarFlyStateTrail, EnableAvatarMoveOnWater, EnableBulletCollisionPluginTrigger,
EnableGadgetIntee, EnableHeadControl, EnableHitBoxByName, EnableMainInterface,
EnablePartControl, EnablePositionSynchronization, EnablePushColliderName, EnableRocketJump,
EnableSceneTransformByName, EnterCameraLock, EntityDoSkill, EquipAffixStart,
ExecuteGadgetLua, FireAISoundEvent, FireChargeBarEffect, FireEffect,
FireEffectFirework, FireEffectForStorm, FireFishingEvent, FireHitEffect,
FireSubEmitterEffect, FireUIEffect, FixedMonsterRushMove, ForceAirStateFly,
ForceEnableShakeOffButton, GenerateElemBall, GetFightProperty, GetInteractIdToGlobalValue,
GetPos, HealHP, HideUIBillBoard, IgnoreMoveColToRockCol,
KillGadget, KillPlayEntity, KillSelf, KillServerGadget,
LoseHP, ModifyAvatarSkillCD, ModifyVehicleSkillCD, PlayEmoSync,
Predicated, PushDvalinS01Process, PushInterActionByConfigPath, PushPos,
Randomed, ReTriggerAISkillInitialCD, RefreshUICombatBarLayout, RegisterAIActionPoint,
ReleaseAIActionPoint, RemoveAvatarSkillInfo, RemoveModifier, RemoveModifierByAbilityStateResistanceID,
RemoveServerBuff, RemoveUniqueModifier, RemoveVelocityForce, Repeated,
ResetAIAttackTarget, ResetAIResistTauntLevel, ResetAIThreatBroadcastRange, ResetAnimatorTrigger,
ReviveDeadAvatar, ReviveElemEnergy, ReviveStamina, SectorCityManeuver,
SendEffectTrigger, SendEffectTriggerToLineEffect, SendEvtElectricCoreMoveEnterP1, SendEvtElectricCoreMoveInterrupt,
ServerLuaCall, ServerLuaTriggerEvent, ServerMonsterLog, SetAIHitFeeling,
SetAISkillCDAvailableNow, SetAISkillCDMultiplier, SetAISkillGCD, SetAnimatorBool,
SetAnimatorFloat, SetAnimatorInt, SetAnimatorTrigger, SetAvatarCanShakeOff,
SetAvatarHitBuckets, SetCanDieImmediately, SetChargeBarValue, SetDvalinS01FlyState,
SetEmissionScaler, SetEntityScale, SetExtraAbilityEnable, SetExtraAbilityState,
SetGlobalDir, SetGlobalPos, SetGlobalValue, SetGlobalValueByTargetDistance,
SetGlobalValueToOverrideMap, SetKeepInAirVelocityForce, SetMaterialParamFloatByTransform, SetNeuronEnable,
SetOverrideMapValue, SetPartControlTarget, SetPoseBool, SetPoseFloat,
SetPoseInt, SetRandomOverrideMapValue, SetRegionalPlayVarValue, SetSelfAttackTarget,
SetSkillAnchor, SetSpecialCamera, SetSurroundAnchor, SetSystemValueToOverrideMap,
SetTargetNumToGlobalValue, SetUICombatBarAsh, SetUICombatBarSpark, SetVelocityIgnoreAirGY,
SetWeaponAttachPointRealName, SetWeaponBindState, ShowExtraAbility, ShowProgressBarAction,
ShowReminder, ShowScreenEffect, ShowTextMap, ShowUICombatBar,
StartDither, SumTargetWeightToSelfGlobalValue, Summon, SyncToStageScript,
TriggerAbility, TriggerAttackEvent, TriggerAttackTargetMapEvent, TriggerAudio,
TriggerAuxWeaponTrans, TriggerBullet, TriggerCreateGadgetToEquipPart, TriggerDropEquipParts,
TriggerFaceAnimation, TriggerGadgetInteractive, TriggerHideWeapon, TriggerSetCastShadow,
TriggerSetPassThrough, TriggerSetRenderersEnable, TriggerSetShadowRamp, TriggerSetVisible,
TriggerTaunt, TriggerThrowEquipPart, TriggerUGCGadgetMove, TryFindBlinkPoint,
TryFindBlinkPointByBorn, TryTriggerPlatformStartMove, TurnDirection, TurnDirectionToPos,
UpdateReactionDamage, UseSkillEliteSet, WidgetSkillStart;
ActCameraRadialBlur,
ActCameraShake,
AddAvatarSkillInfo,
AddChargeBarValue,
AddClimateMeter,
AddElementDurability,
AddGlobalValue,
AddGlobalValueToTarget,
AddRegionalPlayVarValue,
ApplyModifier,
AttachAbilityStateResistance,
AttachBulletAimPoint,
AttachEffect,
AttachEffectFirework,
AttachElementTypeResistance,
AttachModifier,
AttachUIEffect,
AvatarCameraParam,
AvatarEnterCameraShot,
AvatarEnterFocus,
AvatarEnterViewBias,
AvatarExitCameraShot,
AvatarExitClimb,
AvatarExitFocus,
AvatarExitViewBias,
AvatarShareCDSkillStart,
AvatarSkillStart,
BroadcastNeuronStimulate,
CalcDvalinS04RebornPoint,
CallLuaTask,
ChangeEnviroWeather,
ChangeFollowDampTime,
ChangeGadgetUIInteractHint,
ChangePlayMode,
ChangeTag,
ChangeUGCRayTag,
ClearEndura,
ClearGlobalPos,
ClearGlobalValue,
ClearLocalGadgets,
ClearLockTarget,
ClearPos,
ConfigAbilityAction,
ControlEmotion,
CopyGlobalValue,
CreateGadget,
CreateMovingPlatform,
CreateTile,
DamageByAttackValue,
DebugLog,
DestroyTile,
DoBlink,
DoTileAction,
DoWatcherSystemAction,
DoWidgetSystemAction,
DropSubfield,
DummyAction,
DungeonFogEffects,
ElementAttachForActivityGacha,
EnableAIStealthy,
EnableAfterImage,
EnableAvatarFlyStateTrail,
EnableAvatarMoveOnWater,
EnableBulletCollisionPluginTrigger,
EnableGadgetIntee,
EnableHeadControl,
EnableHitBoxByName,
EnableMainInterface,
EnablePartControl,
EnablePositionSynchronization,
EnablePushColliderName,
EnableRocketJump,
EnableSceneTransformByName,
EnterCameraLock,
EntityDoSkill,
EquipAffixStart,
ExecuteGadgetLua,
FireAISoundEvent,
FireChargeBarEffect,
FireEffect,
FireEffectFirework,
FireEffectForStorm,
FireFishingEvent,
FireHitEffect,
FireSubEmitterEffect,
FireUIEffect,
FixedMonsterRushMove,
ForceAirStateFly,
ForceEnableShakeOffButton,
GenerateElemBall,
GetFightProperty,
GetInteractIdToGlobalValue,
GetPos,
HealHP,
HideUIBillBoard,
IgnoreMoveColToRockCol,
KillGadget,
KillPlayEntity,
KillSelf,
KillServerGadget,
LoseHP,
ModifyAvatarSkillCD,
ModifyVehicleSkillCD,
PlayEmoSync,
Predicated,
PushDvalinS01Process,
PushInterActionByConfigPath,
PushPos,
Randomed,
ReTriggerAISkillInitialCD,
RefreshUICombatBarLayout,
RegisterAIActionPoint,
ReleaseAIActionPoint,
RemoveAvatarSkillInfo,
RemoveModifier,
RemoveModifierByAbilityStateResistanceID,
RemoveServerBuff,
RemoveUniqueModifier,
RemoveVelocityForce,
Repeated,
ResetAIAttackTarget,
ResetAIResistTauntLevel,
ResetAIThreatBroadcastRange,
ResetAnimatorTrigger,
ReviveDeadAvatar,
ReviveElemEnergy,
ReviveStamina,
SectorCityManeuver,
SendEffectTrigger,
SendEffectTriggerToLineEffect,
SendEvtElectricCoreMoveEnterP1,
SendEvtElectricCoreMoveInterrupt,
ServerLuaCall,
ServerLuaTriggerEvent,
ServerMonsterLog,
SetAIHitFeeling,
SetAISkillCDAvailableNow,
SetAISkillCDMultiplier,
SetAISkillGCD,
SetAnimatorBool,
SetAnimatorFloat,
SetAnimatorInt,
SetAnimatorTrigger,
SetAvatarCanShakeOff,
SetAvatarHitBuckets,
SetCanDieImmediately,
SetChargeBarValue,
SetDvalinS01FlyState,
SetEmissionScaler,
SetEntityScale,
SetExtraAbilityEnable,
SetExtraAbilityState,
SetGlobalDir,
SetGlobalPos,
SetGlobalValue,
SetGlobalValueByTargetDistance,
SetGlobalValueToOverrideMap,
SetKeepInAirVelocityForce,
SetMaterialParamFloatByTransform,
SetNeuronEnable,
SetOverrideMapValue,
SetPartControlTarget,
SetPoseBool,
SetPoseFloat,
SetPoseInt,
SetRandomOverrideMapValue,
SetRegionalPlayVarValue,
SetSelfAttackTarget,
SetSkillAnchor,
SetSpecialCamera,
SetSurroundAnchor,
SetSystemValueToOverrideMap,
SetTargetNumToGlobalValue,
SetUICombatBarAsh,
SetUICombatBarSpark,
SetVelocityIgnoreAirGY,
SetWeaponAttachPointRealName,
SetWeaponBindState,
ShowExtraAbility,
ShowProgressBarAction,
ShowReminder,
ShowScreenEffect,
ShowTextMap,
ShowUICombatBar,
StartDither,
SumTargetWeightToSelfGlobalValue,
Summon,
SyncToStageScript,
TriggerAbility,
TriggerAttackEvent,
TriggerAttackTargetMapEvent,
TriggerAudio,
TriggerAuxWeaponTrans,
TriggerBullet,
TriggerCreateGadgetToEquipPart,
TriggerDropEquipParts,
TriggerFaceAnimation,
TriggerGadgetInteractive,
TriggerHideWeapon,
TriggerSetCastShadow,
TriggerSetPassThrough,
TriggerSetRenderersEnable,
TriggerSetShadowRamp,
TriggerSetVisible,
TriggerTaunt,
TriggerThrowEquipPart,
TriggerUGCGadgetMove,
TryFindBlinkPoint,
TryFindBlinkPointByBorn,
TryTriggerPlatformStartMove,
TurnDirection,
TurnDirectionToPos,
UpdateReactionDamage,
UseSkillEliteSet,
WidgetSkillStart;
}
@SerializedName("$type")
public Type type;
public String target;
@SerializedName(value = "amount", alternate = "PDLLIFICICJ")
@SerializedName(
value = "amount",
alternate = {"PDLLIFICICJ", "cdRatio"})
public DynamicFloat amount = DynamicFloat.ZERO;
@SerializedName(value = "amountByTargetCurrentHPRatio")
public DynamicFloat amountByCasterAttackRatio = DynamicFloat.ZERO;
@SerializedName(value = "unused")
public DynamicFloat amountByCasterCurrentHPRatio = DynamicFloat.ZERO;
@SerializedName(
value = "unknown",
alternate = {"HFNJHOGGFKB", "GEJGGCIOLKN"})
public DynamicFloat amountByCasterMaxHPRatio = DynamicFloat.ZERO;
public DynamicFloat amountByGetDamage = DynamicFloat.ZERO;
@SerializedName(value = "amountByTargetMaxHPRatio")
public DynamicFloat amountByTargetCurrentHPRatio = DynamicFloat.ZERO;
@SerializedName(value = "amountByCasterMaxHPRatio")
public DynamicFloat amountByTargetMaxHPRatio = DynamicFloat.ZERO;
public DynamicFloat limboByTargetMaxHPRatio = DynamicFloat.ZERO;
public DynamicFloat healRatio = DynamicFloat.ONE;
@SerializedName(value = "ignoreAbilityProperty", alternate = "HHFGADCJJDI")
public boolean ignoreAbilityProperty;
public String modifierName;
public boolean enableLockHP;
public boolean disableWhenLoading;
public boolean lethal = true;
public boolean muteHealEffect = false;
public boolean byServer;
public boolean lifeByOwnerIsAlive;
public String campTargetType;
public int campID;
public int gadgetID;
public boolean ownerIsTarget;
public boolean isFromOwner;
public String globalValueKey;
public String abilityFormula;
public String srcTarget, dstTarget;
public String srcKey, dstKey;
public int skillID;
public int param1;
public int param2;
public int param3;
}
//The following should be implemented into DynamicFloat if older resource formats need to be supported
// The following should be implemented into DynamicFloat if older resource formats need to be
// supported
// public static class AbilityModifierValue {
// public boolean isFormula;
// public boolean isDynamic;

View File

@@ -1,37 +1,35 @@
package emu.grasscutter.data.binout;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
import java.util.ArrayList;
import java.util.List;
import emu.grasscutter.data.binout.AbilityModifier.AbilityModifierAction;
public class AbilityModifierEntry {
private String name; // Custom value
public List<AbilityModifierAction> onModifierAdded;
public List<AbilityModifierAction> onThinkInterval;
public List<AbilityModifierAction> onRemoved;
public AbilityModifierEntry(String name) {
this.name = name;
this.onModifierAdded = new ArrayList<>();
this.onThinkInterval = new ArrayList<>();
this.onRemoved = new ArrayList<>();
}
public String getName() {
return name;
}
public List<AbilityModifierAction> onModifierAdded;
public List<AbilityModifierAction> onThinkInterval;
public List<AbilityModifierAction> onRemoved;
private final String name; // Custom value
public List<AbilityModifierAction> getOnAdded() {
return onModifierAdded;
}
public AbilityModifierEntry(String name) {
this.name = name;
this.onModifierAdded = new ArrayList<>();
this.onThinkInterval = new ArrayList<>();
this.onRemoved = new ArrayList<>();
}
public List<AbilityModifierAction> getOnThinkInterval() {
return onThinkInterval;
}
public String getName() {
return name;
}
public List<AbilityModifierAction> getOnRemoved() {
return onRemoved;
}
public List<AbilityModifierAction> getOnAdded() {
return onModifierAdded;
}
public List<AbilityModifierAction> getOnThinkInterval() {
return onThinkInterval;
}
public List<AbilityModifierAction> getOnRemoved() {
return onRemoved;
}
}

View File

@@ -1,15 +1,13 @@
package emu.grasscutter.data.binout;
import javax.annotation.Nullable;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import javax.annotation.Nullable;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ConfigGadget {
// There are more values that can be added that might be useful in the json
@Nullable
ConfigGadgetCombat combat;
@Nullable ConfigGadgetCombat combat;
}

View File

@@ -1,6 +1,5 @@
package emu.grasscutter.data.binout;
import com.google.gson.annotations.SerializedName;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;

View File

@@ -1,35 +1,40 @@
package emu.grasscutter.data.binout;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.utils.Position;
import emu.grasscutter.game.world.Position;
import java.util.List;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import java.util.List;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class HomeworldDefaultSaveData {
@SerializedName(value = "KFHBFNPDJBE", alternate = "PKACPHDGGEI")
List<HomeBlock> homeBlockLists;
@SerializedName(value = "IJNPADKGNKE", alternate = "MINCKHBNING")
Position bornPos;
@SerializedName("IPIIGEMFLHK")
Position bornRot;
@SerializedName("HHOLBNPIHEM")
Position djinPos;
@SerializedName("KNHCJKHCOAN")
HomeFurniture mainhouse;
@SerializedName("NIHOJFEKFPG")
List<HomeFurniture> doorLists;
@SerializedName("EPGELGEFJFK")
List<HomeFurniture> stairLists;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public static class HomeBlock{
public static class HomeBlock {
@SerializedName(value = "FGIJCELCGFI", alternate = "PGDPDIDJEEL")
int blockId;
@@ -43,13 +48,14 @@ public class HomeworldDefaultSaveData {
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public static class HomeFurniture{
public static class HomeFurniture {
@SerializedName(value = "ENHNGKJBJAB", alternate = "KMAAJJHPNBA")
int id;
@SerializedName(value = "NGIEEIOLPPO", alternate = "JFKAHNCPDME")
Position pos;
//@SerializedName(value = "HEOCEHKEBFM", alternate = "LKCKOOGFDBM")
// @SerializedName(value = "HEOCEHKEBFM", alternate = "LKCKOOGFDBM")
Position rot;
}
}

View File

@@ -1,10 +1,13 @@
package emu.grasscutter.data.binout;
import dev.morphia.annotations.Entity;
import emu.grasscutter.data.GameData;
import emu.grasscutter.game.quest.enums.QuestType;
import lombok.Data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import lombok.Data;
public class MainQuestData {
private int id;
@@ -47,27 +50,43 @@ public class MainQuestData {
public SubQuestData[] getSubQuests() {
return subQuests;
}
public List<TalkData> getTalks() {
return talks;
}
public void onLoad() {
this.talks = talks.stream().filter(Objects::nonNull).toList();
if (this.talks == null) this.talks = new ArrayList<>();
if (this.subQuests == null) this.subQuests = new SubQuestData[0];
this.talks = this.talks.stream().filter(Objects::nonNull).toList();
// Apply talk data to the quest talk map.
this.talks.forEach(talkData -> GameData.getQuestTalkMap().put(talkData.getId(), this.getId()));
// Apply additional sub-quest data to sub-quests.
Arrays.stream(this.subQuests)
.forEach(
quest -> {
var questData = GameData.getQuestDataMap().get(quest.getSubId());
if (questData != null) questData.applyFrom(quest);
});
}
@Data
public static class SubQuestData {
private int subId;
private int order;
private boolean isMpBlock;
private boolean isRewind, finishParent;
}
@Data @Entity
@Data
@Entity
public static class TalkData {
private int id;
private String heroTalk;
public TalkData() {}
public TalkData(int id, String heroTalk) {
this.id = id;
this.heroTalk = heroTalk;

View File

@@ -1,72 +1,71 @@
package emu.grasscutter.data.binout;
import emu.grasscutter.data.ResourceLoader.OpenConfigData;
import java.util.ArrayList;
import java.util.List;
import emu.grasscutter.data.ResourceLoader.OpenConfigData;
public class OpenConfigEntry {
private String name;
private String[] addAbilities;
private int extraTalentIndex;
private SkillPointModifier[] skillPointModifiers;
private final String name;
private String[] addAbilities;
private int extraTalentIndex;
private SkillPointModifier[] skillPointModifiers;
public OpenConfigEntry(String name, OpenConfigData[] data) {
this.name = name;
List<String> abilityList = new ArrayList<>();
List<SkillPointModifier> modList = new ArrayList<>();
for (OpenConfigData entry : data) {
if (entry.$type.contains("AddAbility")) {
abilityList.add(entry.abilityName);
} else if (entry.talentIndex > 0) {
this.extraTalentIndex = entry.talentIndex;
} else if (entry.$type.contains("ModifySkillPoint")) {
modList.add(new SkillPointModifier(entry.skillID, entry.pointDelta));
}
}
if (abilityList.size() > 0) {
this.addAbilities = abilityList.toArray(new String[0]);
}
if (modList.size() > 0) {
this.skillPointModifiers = modList.toArray(new SkillPointModifier[0]);
}
}
public OpenConfigEntry(String name, OpenConfigData[] data) {
this.name = name;
public String getName() {
return name;
}
List<String> abilityList = new ArrayList<>();
List<SkillPointModifier> modList = new ArrayList<>();
public String[] getAddAbilities() {
return addAbilities;
}
for (OpenConfigData entry : data) {
if (entry.$type.contains("AddAbility")) {
abilityList.add(entry.abilityName);
} else if (entry.talentIndex > 0) {
this.extraTalentIndex = entry.talentIndex;
} else if (entry.$type.contains("ModifySkillPoint")) {
modList.add(new SkillPointModifier(entry.skillID, entry.pointDelta));
}
}
public int getExtraTalentIndex() {
return extraTalentIndex;
}
public SkillPointModifier[] getSkillPointModifiers() {
return skillPointModifiers;
}
if (abilityList.size() > 0) {
this.addAbilities = abilityList.toArray(new String[0]);
}
public static class SkillPointModifier {
private int skillId;
private int delta;
public SkillPointModifier(int skillId, int delta) {
this.skillId = skillId;
this.delta = delta;
}
if (modList.size() > 0) {
this.skillPointModifiers = modList.toArray(new SkillPointModifier[0]);
}
}
public int getSkillId() {
return skillId;
}
public String getName() {
return name;
}
public int getDelta() {
return delta;
}
}
public String[] getAddAbilities() {
return addAbilities;
}
public int getExtraTalentIndex() {
return extraTalentIndex;
}
public SkillPointModifier[] getSkillPointModifiers() {
return skillPointModifiers;
}
public static class SkillPointModifier {
private int skillId;
private int delta;
public SkillPointModifier(int skillId, int delta) {
this.skillId = skillId;
this.delta = delta;
}
public int getSkillId() {
return skillId;
}
public int getDelta() {
return delta;
}
}
}

View File

@@ -3,13 +3,12 @@ package emu.grasscutter.data.binout;
import com.github.davidmoten.rtreemulti.RTree;
import com.github.davidmoten.rtreemulti.geometry.Geometry;
import emu.grasscutter.scripts.data.SceneGroup;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@@ -17,13 +16,9 @@ public class SceneNpcBornData {
int sceneId;
List<SceneNpcBornEntry> bornPosList;
/**
* Spatial Index For NPC
*/
/** Spatial Index For NPC */
transient RTree<SceneNpcBornEntry, Geometry> index;
/**
* npc groups
*/
/** npc groups */
transient Map<Integer, SceneGroup> groups = new ConcurrentHashMap<>();
}

View File

@@ -1,32 +1,42 @@
package emu.grasscutter.data.binout;
import emu.grasscutter.utils.Position;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.game.world.Position;
import java.util.List;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import java.util.List;
import com.google.gson.annotations.SerializedName;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class SceneNpcBornEntry {
@SerializedName(value="id", alternate={"_id", "ID"})
@SerializedName(
value = "id",
alternate = {"_id", "ID"})
int id;
@SerializedName(value="configId", alternate={"_configId"})
@SerializedName(
value = "configId",
alternate = {"_configId"})
int configId;
@SerializedName(value="pos", alternate={"_pos"})
@SerializedName(
value = "pos",
alternate = {"_pos"})
Position pos;
@SerializedName(value="rot", alternate={"_rot"})
@SerializedName(
value = "rot",
alternate = {"_rot"})
Position rot;
@SerializedName(value="groupId", alternate={"_groupId"})
@SerializedName(
value = "groupId",
alternate = {"_groupId"})
int groupId;
@SerializedName(value="suiteIdList", alternate={"_suiteIdList"})
@SerializedName(
value = "suiteIdList",
alternate = {"_suiteIdList"})
List<Integer> suiteIdList;
}

View File

@@ -4,8 +4,8 @@ import emu.grasscutter.data.common.PointData;
import lombok.Getter;
public class ScenePointEntry {
@Getter final private int sceneId;
@Getter final private PointData pointData;
@Getter private final int sceneId;
@Getter private final PointData pointData;
@Deprecated(forRemoval = true)
public ScenePointEntry(String name, PointData pointData) {

View File

@@ -1,23 +1,18 @@
package emu.grasscutter.data.binout;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.scripts.data.SceneGroup;
import emu.grasscutter.utils.Position;
import lombok.Data;
import java.util.List;
import java.util.Map;
import lombok.Data;
@Data
public class ScriptSceneData {
Map<String,ScriptObject> scriptObjectList;
Map<String, ScriptObject> scriptObjectList;
@Data
public static class ScriptObject {
//private SceneGroup groups;
// private SceneGroup groups;
@SerializedName("dummy_points")
private Map<String, List<Float>> dummyPoints;
}
}

View File

@@ -0,0 +1,27 @@
package emu.grasscutter.data.binout;
import com.google.gson.annotations.SerializedName;
import emu.grasscutter.data.common.DynamicFloat;
import java.io.Serializable;
public class TalentData implements Serializable {
public enum Type {
AddAbility,
ModifySkillCD,
UnlockTalentParam,
AddTalentExtraLevel,
ModifyAbility;
}
@SerializedName("$type")
public Type type;
public String abilityName;
public String talentParam;
public int talentIndex;
public int extraLevel;
public String paramSpecial;
public DynamicFloat paramDelta;
public DynamicFloat paramRatio = new DynamicFloat(1.0f);
}

View File

@@ -0,0 +1,3 @@
package emu.grasscutter.data.binout.config;
public class ConfigEntityAvatar extends ConfigEntityBase {}

View File

@@ -0,0 +1,17 @@
package emu.grasscutter.data.binout.config;
import emu.grasscutter.data.binout.config.fields.ConfigAbilityData;
import emu.grasscutter.data.binout.config.fields.ConfigCombat;
import emu.grasscutter.data.binout.config.fields.ConfigCommon;
import emu.grasscutter.data.binout.config.fields.ConfigGlobalValue;
import java.util.Collection;
import javax.annotation.Nullable;
import lombok.Data;
@Data
public class ConfigEntityBase {
@Nullable ConfigCommon configCommon;
@Nullable ConfigCombat combat;
Collection<ConfigAbilityData> abilities;
ConfigGlobalValue globalValue; // used for SGV in monsters and Gadgets
}

View File

@@ -0,0 +1,13 @@
package emu.grasscutter.data.binout.config;
import lombok.AccessLevel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.FieldDefaults;
@Data
@EqualsAndHashCode(callSuper = true)
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ConfigEntityGadget extends ConfigEntityBase {
// There are more values that can be added that might be useful in the json
}

View File

@@ -0,0 +1,8 @@
package emu.grasscutter.data.binout.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class ConfigEntityMonster extends ConfigEntityBase {}

View File

@@ -0,0 +1,23 @@
package emu.grasscutter.data.binout.config;
import java.util.List;
import lombok.Data;
@Data
public class ConfigGlobalCombat {
private DefaultAbilities defaultAbilities;
// TODO: Add more indices
@Data
public class DefaultAbilities {
private String monterEliteAbilityName;
private List<String> nonHumanoidMoveAbilities;
private List<String> levelDefaultAbilities;
private List<String> levelElementAbilities;
private List<String> levelItemAbilities;
private List<String> levelSBuffAbilities;
private List<String> defaultMPLevelAbilities;
private List<String> defaultAvatarAbilities;
private List<String> defaultTeamAbilities;
}
}

View File

@@ -0,0 +1,14 @@
package emu.grasscutter.data.binout.config;
import emu.grasscutter.data.binout.config.fields.ConfigAbilityData;
import java.util.List;
import lombok.Getter;
public class ConfigLevelEntity {
@Getter private List<ConfigAbilityData> abilities;
@Getter private List<ConfigAbilityData> monsterAbilities;
@Getter private List<ConfigAbilityData> avatarAbilities;
@Getter private List<ConfigAbilityData> teamAbilities;
@Getter private List<Integer> preloadMonsterEntityIDs;
}

View File

@@ -0,0 +1,10 @@
package emu.grasscutter.data.binout.config.fields;
import lombok.Data;
@Data
public class ConfigAbilityData {
public String abilityID;
public String abilityName;
public String abilityOverride;
}

View File

@@ -0,0 +1,8 @@
package emu.grasscutter.data.binout.config.fields;
import lombok.Data;
@Data
public class ConfigAiBeta {
boolean enable;
}

View File

@@ -0,0 +1,12 @@
package emu.grasscutter.data.binout.config.fields;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ConfigCombat {
// There are more values that can be added that might be useful in the json
ConfigCombatProperty property;
}

View File

@@ -0,0 +1,18 @@
package emu.grasscutter.data.binout.config.fields;
import com.google.gson.annotations.SerializedName;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ConfigCombatDie {
@SerializedName(
value = "dieEndTime",
alternate = {"HGGPMFGGBNC"})
double dieEndTime;
double dieForceDisappearTime;
boolean hasAnimatorDie;
}

View File

@@ -0,0 +1,18 @@
package emu.grasscutter.data.binout.config.fields;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class ConfigCombatProperty {
float HP;
boolean isLockHP;
boolean isInvincible;
boolean isGhostToAllied;
float attack;
float defence;
float weight;
boolean useCreatorProperty;
}

View File

@@ -0,0 +1,6 @@
package emu.grasscutter.data.binout.config.fields;
import lombok.Data;
@Data
public class ConfigCommon {}

View File

@@ -0,0 +1,12 @@
package emu.grasscutter.data.binout.config.fields;
import java.util.Map;
import java.util.Set;
import lombok.Data;
/** Contains information about the entities SGVs */
@Data
public class ConfigGlobalValue {
Set<String> serverGlobalValues;
Map<String, Float> initServerGlobalValues;
}

View File

@@ -0,0 +1,22 @@
package emu.grasscutter.data.binout.routes;
// import emu.grasscutter.scripts.constants.IntValueEnum;
import lombok.Getter;
public enum RotAngleType /*implements IntValueEnum */ {
ROT_NONE(-1),
ROT_ANGLE_X(0),
ROT_ANGLE_Y(1),
ROT_ANGLE_Z(2);
@Getter private final int id;
RotAngleType(int id) {
this.id = id;
}
// @Override
public int getValue() {
return id;
}
}

View File

@@ -0,0 +1,7 @@
package emu.grasscutter.data.binout.routes;
public enum RotType {
ROT_NONE,
ROT_ANGLE,
ROT_ROUND
}

View File

@@ -0,0 +1,29 @@
package emu.grasscutter.data.binout.routes;
import emu.grasscutter.net.proto.RouteOuterClass;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import lombok.val;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Route {
private int localId;
private String name;
private RouteType type = RouteType.Unknown;
private RoutePoint[] points;
private float arriveRange; // optional
private RotType rotType; // optional
private RotAngleType rotAngleType; // optional
public RouteOuterClass.Route toProto() {
val builder = RouteOuterClass.Route.newBuilder().setRouteType(type.getValue());
if (points != null) {
for (var routePoint : points) {
builder.addRoutePoints(routePoint.toProto().setArriveRange(arriveRange));
}
}
return builder.build();
}
}

View File

@@ -0,0 +1,31 @@
package emu.grasscutter.data.binout.routes;
import emu.grasscutter.game.world.Position;
import emu.grasscutter.net.proto.RoutePointOuterClass;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import lombok.val;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class RoutePoint {
private Position pos;
private int speedLevel; // optional
private float waitTime; // optional
private float targetVelocity; // optional
private boolean hasReachEvent; // optional
// rotRoundReachDir //optional Pos with optional values
// rotRoundLeaveDir //optional Pos with optional values
public RoutePointOuterClass.RoutePoint.Builder toProto() {
val builder = RoutePointOuterClass.RoutePoint.newBuilder().setPosition(pos.toProto());
if (waitTime != 0) {
builder.setTime(waitTime);
} else if (targetVelocity != 0) {
builder.setVelocity(targetVelocity);
}
return builder;
}
}

View File

@@ -0,0 +1,22 @@
package emu.grasscutter.data.binout.routes;
// import emu.grasscutter.scripts.constants.IntValueEnum;
import lombok.Getter;
public enum RouteType /*implements IntValueEnum*/ {
Unknown(-1),
OneWay(0),
Reciprocate(1),
Loop(2);
@Getter private final int id;
RouteType(int id) {
this.id = id;
}
// @Override
public int getValue() {
return id;
}
}

View File

@@ -0,0 +1,13 @@
package emu.grasscutter.data.binout.routes;
import javax.annotation.Nullable;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class SceneRoutes {
private int sceneId;
@Nullable private Route[] routes;
}

View File

@@ -0,0 +1,9 @@
package emu.grasscutter.data.common;
import java.util.List;
public interface BaseTrialActivityData {
List<Integer> getAvatarIndexIdList();
List<Integer> getRewardIdList();
}

Some files were not shown because too many files have changed in this diff Show More