diff --git a/README.md b/README.md index d3020e3ff..f327996fc 100644 --- a/README.md +++ b/README.md @@ -14,19 +14,19 @@ EN | [中文](README_zh-CN.md) * Friends list * Teleportation * Gacha system -* Co-op *partially* work +* Co-op *partially* works * Spawning monsters via console * Inventory features (recieving items/characters, upgrading items/characters, etc) ## Quick setup guide -**Note:** for support please join our [Discord](https://discord.gg/T5vZU6UyeG) +**Note:** For support please join our [Discord](https://discord.gg/T5vZU6UyeG). ### Requirements -* Java SE - 17 ([mirror link](https://github.com/adoptium/temurin17-binaries/releases/tag/jdk-17.0.3+7) since Oracle required an account to download old builds) +* Java SE - 17 ([link](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html)) - **Note:** If you just want to **run it**, then **jre** is fine + **Note:** If you just want to **run it**, then **jre** only is fine. * MongoDB (recommended 4.0+) @@ -34,12 +34,12 @@ EN | [中文](README_zh-CN.md) ### Running -**Note:** If you update from an older version, delete `config.json` for regeneration +**Note:** If you updated from an older version, delete `config.json` to regenerate it. 1. Get `grasscutter.jar` - Download from [actions](https://nightly.link/Grasscutters/Grasscutter/workflows/build/stable/Grasscutter.zip) - [Build by yourself](#Building) -2. Create a `resources` folder in the directory where grasscutter.jar is located and bring your `BinOutput` and `ExcelBinOutput` folders into it *(Check the [wiki](https://github.com/Grasscutters/Grasscutter/wiki) for more details how to get those.)* +2. Create a `resources` folder in the directory where grasscutter.jar is located and move your `BinOutput` and `ExcelBinOutput` folders there *(Check the [wiki](https://github.com/Grasscutters/Grasscutter/wiki) for more details how to get those.)* 3. Run Grasscutter with `java -jar grasscutter.jar`. **Make sure mongodb service is running as well.** ### Connecting with the client @@ -96,7 +96,7 @@ chmod +x gradlew ./gradlew jar # Compile ``` -You can find the output jar in the root of the project folder +You can find the output jar in the root of the project folder. ## Commands @@ -106,33 +106,33 @@ There is a dummy user named "Server" in every player's friends list that you can | Commands | Usage | Permission node | Availability | description | Alias | | -------------- | ------------------------------------------------- | ------------------------- | ------------ | ------------------------------------------------------------ | ----------------------------------------------- | -| account | account [uid] | | Server only | Creates an account with the specified username and the in-game uid for that account. The uid will be auto generated if not set. | | +| account | account [UID] | | Server only | Creates an account with the specified username and the in-game UID for that account. The UID will be auto generated if not set. | | | broadcast | broadcast | server.broadcast | Both side | Sends a message to all the players. | b | | changescene | changescene | player.changescene | Client only | Switch scenes by scene ID. | scene | -| clearartifacts | clearartifacts | player.clearartifacts | Client only | Deletes all unequipped and unlocked level 0 artifacts, including yellow rarity ones from your inventory. | clearart | -| clearweapons | clearweapons | player.clearweapons | Client only | Deletes all unequipped and unlocked weapons, including yellow rarity ones from your inventory. | clearwpns | +| clearartifacts | clearartifacts | player.clearartifacts | Client only | Deletes all unequipped and unlocked level 0 artifacts, including 5-star rarity ones from your inventory. | clearart | +| clearweapons | clearweapons | player.clearweapons | Client only | Deletes all unequipped and unlocked weapons, including 5-star rarity ones from your inventory. | clearwpns | | drop | drop [amount] | server.drop | Client only | Drops an item around you. | `d` `dropitem` | | give | give [player] [amount] [level] | player.give | Both side | Gives item(s) to you or the specified player. | `g` `item` `giveitem` | | givechar | givechar | player.givechar | Both side | Gives the player a specified character. | givec | | giveall | giveall [uid] [amount] | player.giveall | Both side | Gives all items. | givea | | godmode | godmode [uid] | player.godmode | Client only | Prevents you from taking damage. | | -| heal | heal | player.heal | Client only | Heal all characters in your current team. | h | +| heal | heal | player.heal | Client only | Heals all characters in your current team. | h | | help | help [command] | | Both side | Sends the help message or shows information about a specified command. | | | kick | kick | server.kick | Both side | Kicks the specified player from the server. (WIP) | k | -| killall | killall [playerUid] [sceneId] | server.killall | Both side | Kill all entities in the current scene or specified scene of the corresponding player. | | -| list | list | | Both side | List online players. | | +| killall | killall [playerUid] [sceneId] | server.killall | Both side | Kills all entities in the current scene or specified scene of the corresponding player. | | +| list | list | | Both side | Lists online players. | | | permission | permission | * | Both side | Grants or removes a permission for a user. | | -| position | position | | Client only | Get coordinates. | pos | -| reload | reload | server.reload | Both side | Reload server config | | -| resetconst | resetconst [all] | player.resetconstellation | Client only | Resets the constellation level on your current active character, will need to relog after using the command to see any changes. | resetconstellation | +| position | position | | Client only | Sends your current coordinates. | pos | +| reload | reload | server.reload | Both side | Reloads the server config | | +| resetconst | resetconst [all] | player.resetconstellation | Client only | Resets the constellation level on your currently selected character, will need to relog after using the command to see any changes. | resetconstellation | | restart | | | Both side | Restarts the current session | | | say | say | server.sendmessage | Both side | Sends a message to a player as the server | `sendservmsg` `sendservermessage` `sendmessage` | -| setfetterlevel | setfetterlevel | player.setfetterlevel | Client only | Sets your fetter level for your current active character | setfetterlvl | -| setstats | setstats | player.setstats | Client only | Set fight property for your current active character | stats | +| setfetterlevel | setfetterlevel | player.setfetterlevel | Client only | Sets the friendship level for your currently selected character | setfetterlvl | +| setstats | setstats | player.setstats | Client only | Sets a stat for your currently selected character | stats | | setworldlevel | setworldlevel | player.setworldlevel | Client only | Sets your world level (Relog to see proper effects) | setworldlvl | | spawn | spanw [level] [amount] | server.spawn | Client only | Spawns an entity near you | | | stop | stop | server.stop | Both side | Stops the server | | -| talent | talent | player.settalent | Client only | Set talent level for your current active character | | +| talent | talent | player.settalent | Client only | Sets talent level for your currently selected character | | | teleport | teleport | player.teleport | Client only | Change the player's position. | tp | | weather | weather | player.weather | Client only | Changes the weather | w | @@ -146,4 +146,4 @@ character falling from a very high destination, exact location that you marked. * If compiling wasn't successful, please check your JDK installation (JDK 17 and validated JDK's bin PATH variable) * My client doesn't connect, doesn't login, 4206, etc... - Mostly your proxy daemon setup is *the issue*, if using Fiddler make sure it running on another port except 8888 -* Startup sequence: Mongodb > Grasscutter > Proxy daemon (mitmdump, fiddler, etc.) > Client +* Startup sequence: Mongodb -> Grasscutter -> Proxy daemon (mitmdump, fiddler, etc.) -> Client diff --git a/data/Drop.json b/data/Drop.json new file mode 100644 index 000000000..1fd5dd69e --- /dev/null +++ b/data/Drop.json @@ -0,0 +1,14 @@ +[ + { + "monsterId": 21010101, + "dropDataList": [ + { + "itemId": 112005, + "minCount": 1, + "maxCount": 3, + "minWeight": 0, + "maxWeight": 10000 + } + ] + } +] diff --git a/data/Shop.json b/data/Shop.json new file mode 100644 index 000000000..9670b0bfc --- /dev/null +++ b/data/Shop.json @@ -0,0 +1,54 @@ +[ + { + "shopId": 1004, + "items": [ + { + "goodsId": 1004202, + "goodsItem": { + "Id": 202, + "Count": 1000000 + }, + "scoin": 1, + "buyLimit": 500, + "beginTime": 1575129600, + "endTime": 2051193600, + "minLevel": 1, + "maxLevel": 99, + "costItemList": [ + { + "Id": 223, + "Count": 100 + } + ] + }, + { + "goodsId": 10048006, + "goodsItem": { + "Id": 108006, + "Count": 20 + }, + "scoin": 100, + "hcoin": 100, + "mcoin": 100, + "buyLimit": 50000, + "beginTime": 1575129600, + "endTime": 2051193600, + "minLevel": 1, + "maxLevel": 99 + }, + { + "goodsId": 10048033, + "goodsItem": { + "Id": 108033, + "Count": 20 + }, + "scoin": 1, + "buyLimit": 50000, + "beginTime": 1575129600, + "endTime": 2051193600, + "minLevel": 1, + "maxLevel": 99 + } + ] + } +] \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/Config.java b/src/main/java/emu/grasscutter/Config.java index ee5fe65c8..3e6e16d20 100644 --- a/src/main/java/emu/grasscutter/Config.java +++ b/src/main/java/emu/grasscutter/Config.java @@ -33,6 +33,7 @@ public final class Config { public Boolean FrontHTTPS = true; public boolean AutomaticallyCreateAccounts = false; + public String[] defaultPermissions = new String[] { "" }; public RegionInfo[] GameServers = {}; diff --git a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java index 68da1569a..ab09b3afa 100644 --- a/src/main/java/emu/grasscutter/command/commands/AccountCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/AccountCommand.java @@ -1,5 +1,6 @@ package emu.grasscutter.command.commands; +import emu.grasscutter.Grasscutter; import emu.grasscutter.command.Command; import emu.grasscutter.command.CommandHandler; import emu.grasscutter.database.DatabaseHelper; @@ -7,8 +8,7 @@ import emu.grasscutter.game.player.Player; import java.util.List; -@Command(label = "account", usage = "account [uid]", - description = "Modify user accounts") +@Command(label = "account", usage = "account [uid]", description = "Modify user accounts") public final class AccountCommand implements CommandHandler { @Override @@ -46,9 +46,10 @@ public final class AccountCommand implements CommandHandler { CommandHandler.sendMessage(null, "Account already exists."); return; } else { - CommandHandler.sendMessage(null, "Account created with UID " + account.getPlayerUid() + "."); - account.addPermission("*"); // Grant the player superuser permissions. + account.addPermission("*"); account.save(); // Save account to database. + + CommandHandler.sendMessage(null, "Account created with UID " + account.getPlayerUid() + "."); } return; case "delete": diff --git a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java index 3e2466686..9636fd845 100644 --- a/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/KillAllCommand.java @@ -4,6 +4,7 @@ import emu.grasscutter.Grasscutter; 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; @@ -53,10 +54,12 @@ public final class KillAllCommand implements CommandHandler { return; } - mainScene.getEntities().values().stream() + // Separate into list to avoid concurrency issue + List toKill = mainScene.getEntities().values().stream() .filter(entity -> entity instanceof EntityMonster) - .forEach(entity -> mainScene.killEntity(entity, 0)); - CommandHandler.sendMessage(sender, "Killing all monsters in scene " + mainScene.getId()); + .toList(); + toKill.stream().forEach(entity -> mainScene.killEntity(entity, 0)); + CommandHandler.sendMessage(sender, "Killing " + toKill.size() + " monsters in scene " + mainScene.getId()); } catch (NumberFormatException ignored) { CommandHandler.sendMessage(sender, "Invalid arguments."); } diff --git a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java index 26dbb903b..d6f983df4 100644 --- a/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java +++ b/src/main/java/emu/grasscutter/command/commands/ReloadCommand.java @@ -16,6 +16,8 @@ public final class ReloadCommand implements CommandHandler { CommandHandler.sendMessage(sender, "Reloading config."); Grasscutter.loadConfig(); Grasscutter.getGameServer().getGachaManager().load(); + Grasscutter.getGameServer().getDropManager().load(); + Grasscutter.getGameServer().getShopManager().load(); Grasscutter.getDispatchServer().loadQueries(); CommandHandler.sendMessage(sender, "Reload complete."); } diff --git a/src/main/java/emu/grasscutter/game/drop/DropData.java b/src/main/java/emu/grasscutter/game/drop/DropData.java new file mode 100644 index 000000000..dd4d07b40 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/drop/DropData.java @@ -0,0 +1,57 @@ +package emu.grasscutter.game.drop; + +public class DropData { + private int minWeight; + private int maxWeight; + private int itemId; + private int minCount; + private int maxCount; + private boolean share = false; + + public int getItemId() { + return itemId; + } + + public void setItemId(int itemId) { + this.itemId = itemId; + } + + public int getMinCount() { + return minCount; + } + + + public int getMaxCount() { + return maxCount; + } + + + public int getMinWeight() { + return minWeight; + } + + public int getMaxWeight() { + return maxWeight; + } + + + public boolean isShare() { + return share; + } + + public void setIsShare(boolean share) { + this.share = share; + } + + public boolean isGive() { + return give; + } + + private boolean give = false; + + public boolean isExp() { + return exp; + } + + private boolean exp = false; +} diff --git a/src/main/java/emu/grasscutter/game/drop/DropInfo.java b/src/main/java/emu/grasscutter/game/drop/DropInfo.java new file mode 100644 index 000000000..d9ccfc4c5 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/drop/DropInfo.java @@ -0,0 +1,16 @@ +package emu.grasscutter.game.drop; + +import java.util.List; + +public class DropInfo { + public int getMonsterId() { + return monsterId; + } + + public List getDropDataList() { + return dropDataList; + } + + private int monsterId; + private List dropDataList; +} diff --git a/src/main/java/emu/grasscutter/game/drop/DropManager.java b/src/main/java/emu/grasscutter/game/drop/DropManager.java new file mode 100644 index 000000000..3bea18c71 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/drop/DropManager.java @@ -0,0 +1,99 @@ +package emu.grasscutter.game.drop; + +import com.google.gson.reflect.TypeToken; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.data.GameData; +import emu.grasscutter.data.def.ItemData; +import emu.grasscutter.game.entity.EntityItem; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.server.game.GameServer; +import emu.grasscutter.utils.Position; +import emu.grasscutter.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.io.FileReader; +import java.util.Collection; +import java.util.List; + +public class DropManager { + public GameServer getGameServer() { + return gameServer; + } + + private final GameServer gameServer; + + public Int2ObjectMap> getDropData() { + return dropData; + } + + private final Int2ObjectMap> dropData; + + public DropManager(GameServer gameServer) { + this.gameServer = gameServer; + this.dropData = new Int2ObjectOpenHashMap<>(); + this.load(); + } + + public synchronized void load() { + try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "Drop.json")) { + getDropData().clear(); + List banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, DropInfo.class).getType()); + if(banners.size() > 0) { + for (DropInfo di : banners) { + getDropData().put(di.getMonsterId(), di.getDropDataList()); + } + Grasscutter.getLogger().info("Drop data successfully loaded."); + } else { + Grasscutter.getLogger().error("Unable to load drop data. Drop data size is 0."); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + private void processDrop(DropData dd, EntityMonster em, Player gp) { + int target = Utils.randomRange(1, 10000); + if (target >= dd.getMinWeight() && target < dd.getMaxWeight()) { + ItemData itemData = GameData.getItemDataMap().get(dd.getItemId()); + int num = Utils.randomRange(dd.getMinCount(), dd.getMaxCount()); + if (!dd.isGive()) { + if (itemData.isEquip()) { + for (int i = 0; i < num; i++) { + float range = (5f + (.1f * num)); + Position pos = em.getPosition().clone().addX((float) (Math.random() * range) - (range / 2)).addY(3f).addZ((float) (Math.random() * range) - (range / 2)); + EntityItem entity = new EntityItem(em.getScene(), gp, itemData, pos, num, dd.isShare()); + if (!dd.isShare()) + em.getScene().addEntityToSingleClient(gp, entity); + else + em.getScene().addEntity(entity); + } + } else { + Position pos = em.getPosition().clone().addY(3f); + EntityItem entity = new EntityItem(em.getScene(), gp, itemData, pos, num, dd.isShare()); + if (!dd.isShare()) + em.getScene().addEntityToSingleClient(gp, entity); + else + em.getScene().addEntity(entity); + } + } + } + } + + public void callDrop(EntityMonster em) { + int id = em.getMonsterData().getId(); + if (getDropData().containsKey(id)) { + for (DropData dd : getDropData().get(id)) { + if (dd.isShare()) + processDrop(dd, em, null); + else { + for (Player gp : em.getScene().getPlayers()) { + processDrop(dd, em, gp); + } + } + } + } + } +} diff --git a/src/main/java/emu/grasscutter/game/entity/EntityItem.java b/src/main/java/emu/grasscutter/game/entity/EntityItem.java index 5b1e05fc7..e23c1cb33 100644 --- a/src/main/java/emu/grasscutter/game/entity/EntityItem.java +++ b/src/main/java/emu/grasscutter/game/entity/EntityItem.java @@ -6,7 +6,6 @@ import emu.grasscutter.game.player.Player; import emu.grasscutter.game.props.EntityIdType; import emu.grasscutter.game.props.PlayerProperty; import emu.grasscutter.game.world.Scene; -import emu.grasscutter.game.world.World; import emu.grasscutter.net.proto.AbilitySyncStateInfoOuterClass.AbilitySyncStateInfo; import emu.grasscutter.net.proto.AnimatorParameterValueInfoPairOuterClass.AnimatorParameterValueInfoPair; import emu.grasscutter.net.proto.EntityAuthorityInfoOuterClass.EntityAuthorityInfo; @@ -30,14 +29,30 @@ public class EntityItem extends EntityGadget { private final GameItem item; private final long guid; - + + private final boolean share; + public EntityItem(Scene scene, Player player, ItemData itemData, Position pos, int count) { super(scene); this.id = getScene().getWorld().getNextEntityId(EntityIdType.GADGET); this.pos = new Position(pos); this.rot = new Position(); - this.guid = player.getNextGameGuid(); + this.guid = player == null ? scene.getWorld().getHost().getNextGameGuid() : player.getNextGameGuid(); this.item = new GameItem(itemData, count); + this.share = true; + } + + // In official game, some drop items are shared to all players, and some other items are independent to all players + // For example, if you killed a monster in MP mode, all players could get drops but rarity and number of them are different + // but if you broke regional mine, when someone picked up the drop then it disappeared + public EntityItem(Scene scene, Player player, ItemData itemData, Position pos, int count, boolean share) { + super(scene); + this.id = getScene().getWorld().getNextEntityId(EntityIdType.GADGET); + this.pos = new Position(pos); + this.rot = new Position(); + this.guid = player == null ? scene.getWorld().getHost().getNextGameGuid() : player.getNextGameGuid(); + this.item = new GameItem(itemData, count); + this.share = share; } @Override @@ -81,6 +96,10 @@ public class EntityItem extends EntityGadget { return null; } + public boolean isShare() { + return share; + } + @Override public SceneEntityInfo toProto() { EntityAuthorityInfo authority = EntityAuthorityInfo.newBuilder() diff --git a/src/main/java/emu/grasscutter/game/friends/Friendship.java b/src/main/java/emu/grasscutter/game/friends/Friendship.java index ef0bed109..79439bb3a 100644 --- a/src/main/java/emu/grasscutter/game/friends/Friendship.java +++ b/src/main/java/emu/grasscutter/game/friends/Friendship.java @@ -8,7 +8,7 @@ import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.player.Player; import emu.grasscutter.net.proto.FriendBriefOuterClass.FriendBrief; import emu.grasscutter.net.proto.FriendOnlineStateOuterClass.FriendOnlineState; -import emu.grasscutter.net.proto.HeadImageOuterClass.HeadImage; +import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; @Entity(value = "friendships", useDiscriminator = false) public class Friendship { @@ -92,7 +92,7 @@ public class Friendship { .setUid(getFriendProfile().getUid()) .setNickname(getFriendProfile().getName()) .setLevel(getFriendProfile().getPlayerLevel()) - .setAvatarId(HeadImage.newBuilder().setAvatarId(getFriendProfile().getAvatarId()).getAvatarId()) + .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(getFriendProfile().getAvatarId())) .setWorldLevel(getFriendProfile().getWorldLevel()) .setSignature(getFriendProfile().getSignature()) .setOnlineState(getFriendProfile().isOnline() ? FriendOnlineState.FRIEND_ONLINE : FriendOnlineState.FREIEND_DISCONNECT) diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java index ed5492b62..9b54c924f 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaBanner.java @@ -16,8 +16,8 @@ public class GachaBanner { private int sortId; private int[] rateUpItems1; private int[] rateUpItems2; - private int minItemType = 1; - private int maxItemType = 2; + private int baseYellowWeight = 60; // Max 10000 + private int basePurpleWeight = 510; // Max 10000 private int eventChance = 50; // Chance to win a featured event item private int softPity = 75; private int hardPity = 90; @@ -63,6 +63,14 @@ public class GachaBanner { return sortId; } + public int getBaseYellowWeight() { + return baseYellowWeight; + } + + public int getBasePurpleWeight() { + return basePurpleWeight; + } + public int[] getRateUpItems1() { return rateUpItems1; } @@ -70,14 +78,6 @@ public class GachaBanner { public int[] getRateUpItems2() { return rateUpItems2; } - - public int getMinItemType() { - return minItemType; - } - - public int getMaxItemType() { - return maxItemType; - } public int getSoftPity() { return softPity - 1; diff --git a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java index fa26569e4..5cd484e9a 100644 --- a/src/main/java/emu/grasscutter/game/gacha/GachaManager.java +++ b/src/main/java/emu/grasscutter/game/gacha/GachaManager.java @@ -15,6 +15,7 @@ import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; import emu.grasscutter.data.def.ItemData; import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.gacha.GachaBanner.BannerType; import emu.grasscutter.game.inventory.GameItem; import emu.grasscutter.game.inventory.ItemType; import emu.grasscutter.game.inventory.MaterialType; @@ -125,8 +126,8 @@ public class GachaManager { int itemId = 0; int bonusYellowChance = gachaInfo.getPity5() >= banner.getSoftPity() ? 100 * (gachaInfo.getPity5() - banner.getSoftPity() - 1): 0; - int yellowChance = 60 + (int) Math.floor(100f * (gachaInfo.getPity5() / (banner.getSoftPity() - 1D))) + bonusYellowChance; - int purpleChance = 10000 - (510 + (int) Math.floor(790f * (gachaInfo.getPity4() / 8f))); + int yellowChance = banner.getBaseYellowWeight() + (int) Math.floor(100f * (gachaInfo.getPity5() / (banner.getSoftPity() - 1D))) + bonusYellowChance; + int purpleChance = 10000 - (banner.getBasePurpleWeight() + (int) Math.floor(790f * (gachaInfo.getPity4() / 8f))); if (random <= yellowChance || gachaInfo.getPity5() >= banner.getHardPity()) { if (banner.getRateUpItems1().length > 0) { @@ -142,7 +143,7 @@ public class GachaManager { } if (itemId == 0) { - int typeChance = this.randomRange(banner.getMinItemType(), banner.getMaxItemType()); + int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2); if (typeChance == 1) { itemId = getRandom(this.yellowAvatars); } else { @@ -163,7 +164,7 @@ public class GachaManager { } if (itemId == 0) { - int typeChance = this.randomRange(banner.getMinItemType(), banner.getMaxItemType()); + int typeChance = this.randomRange(banner.getBannerType() == BannerType.WEAPON ? 2 : 1, banner.getBannerType() == BannerType.EVENT ? 1 : 2); if (typeChance == 1) { itemId = getRandom(this.purpleAvatars); } else { diff --git a/src/main/java/emu/grasscutter/game/inventory/Inventory.java b/src/main/java/emu/grasscutter/game/inventory/Inventory.java index bb8dfad46..5ca0d022f 100644 --- a/src/main/java/emu/grasscutter/game/inventory/Inventory.java +++ b/src/main/java/emu/grasscutter/game/inventory/Inventory.java @@ -109,6 +109,16 @@ public class Inventory implements Iterable { return result; } + + public boolean addItem(GameItem item, ActionReason reason, boolean forceNotify) { + boolean result = addItem(item); + + if (reason != null && (forceNotify || result)) { + getPlayer().sendPacket(new PacketItemAddHintNotify(item, reason)); + } + + return result; + } public void addItems(Collection items) { this.addItems(items, null); @@ -239,6 +249,9 @@ public class Inventory implements Iterable { case 202: // Mora getPlayer().setMora(player.getMora() + count); break; + case 203: // Genesis Crystals + getPlayer().setCrystals(player.getCrystals() + count); + break; } } diff --git a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java index 904d2cbdd..055d3da6d 100644 --- a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java +++ b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java @@ -923,6 +923,7 @@ public class InventoryManager { break; } + // Welkin if (useItem.getItemId() == 1202) { player.rechargeMoonCard(); used = 1; diff --git a/src/main/java/emu/grasscutter/game/player/Player.java b/src/main/java/emu/grasscutter/game/player/Player.java index 5970c3f13..75c9c21ad 100644 --- a/src/main/java/emu/grasscutter/game/player/Player.java +++ b/src/main/java/emu/grasscutter/game/player/Player.java @@ -1,20 +1,16 @@ package emu.grasscutter.game.player; -import java.time.Instant; -import java.util.*; - import dev.morphia.annotations.*; import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; -import emu.grasscutter.command.CommandHandler; import emu.grasscutter.data.GameData; import emu.grasscutter.data.def.PlayerLevelData; import emu.grasscutter.database.DatabaseHelper; -import emu.grasscutter.game.avatar.AvatarProfileData; -import emu.grasscutter.game.avatar.AvatarStorage; import emu.grasscutter.game.Account; import emu.grasscutter.game.CoopRequest; import emu.grasscutter.game.avatar.Avatar; +import emu.grasscutter.game.avatar.AvatarProfileData; +import emu.grasscutter.game.avatar.AvatarStorage; import emu.grasscutter.game.entity.EntityItem; import emu.grasscutter.game.entity.GameEntity; import emu.grasscutter.game.friends.FriendsList; @@ -25,28 +21,30 @@ import emu.grasscutter.game.inventory.Inventory; import emu.grasscutter.game.mail.Mail; import emu.grasscutter.game.props.ActionReason; import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.game.shop.ShopLimit; import emu.grasscutter.game.world.Scene; import emu.grasscutter.game.world.World; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.proto.AbilityInvokeEntryOuterClass.AbilityInvokeEntry; import emu.grasscutter.net.proto.CombatInvokeEntryOuterClass.CombatInvokeEntry; -import emu.grasscutter.net.proto.HeadImageOuterClass.HeadImage; import emu.grasscutter.net.proto.InteractTypeOuterClass.InteractType; import emu.grasscutter.net.proto.MpSettingTypeOuterClass.MpSettingType; import emu.grasscutter.net.proto.OnlinePlayerInfoOuterClass.OnlinePlayerInfo; -import emu.grasscutter.net.proto.PlayerApplyEnterMpReasonOuterClass.PlayerApplyEnterMpReason; import emu.grasscutter.net.proto.PlayerApplyEnterMpResultNotifyOuterClass; import emu.grasscutter.net.proto.PlayerLocationInfoOuterClass.PlayerLocationInfo; import emu.grasscutter.net.proto.PlayerWorldLocationInfoOuterClass; +import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; +import emu.grasscutter.net.proto.SocialShowAvatarInfoOuterClass; import emu.grasscutter.server.game.GameServer; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.*; -import emu.grasscutter.utils.Position; import emu.grasscutter.utils.DateHelper; +import emu.grasscutter.utils.Position; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import java.time.Instant; import java.util.*; @Entity(value = "players", useDiscriminator = false) @@ -84,6 +82,7 @@ public class Player { private ArrayList shownAvatars; private Set rewardedLevels; private ArrayList mail; + private ArrayList shopLimit; private int sceneId; private int regionId; @@ -95,6 +94,9 @@ public class Player { private int moonCardDuration; private Set moonCardGetTimes; + private List showAvatarList; + private boolean showAvatars; + @Transient private boolean paused; @Transient private int enterSceneToken; @Transient private SceneLoadState sceneState; @@ -141,6 +143,8 @@ public class Player { this.birthday = new PlayerBirthday(); this.rewardedLevels = new HashSet<>(); this.moonCardGetTimes = new HashSet<>(); + + this.shopLimit = new ArrayList<>(); } // On player creation @@ -294,6 +298,15 @@ public class Player { this.setProperty(PlayerProperty.PROP_PLAYER_SCOIN, mora); this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_SCOIN)); } + + public int getCrystals() { + return this.getProperty(PlayerProperty.PROP_PLAYER_MCOIN); + } + + public void setCrystals(int crystals) { + this.setProperty(PlayerProperty.PROP_PLAYER_MCOIN, crystals); + this.sendPacket(new PacketPlayerPropNotify(this, PlayerProperty.PROP_PLAYER_MCOIN)); + } private int getExpRequired(int level) { PlayerLevelData levelData = GameData.getPlayerLevelDataMap().get(level); @@ -504,6 +517,22 @@ public class Player { this.regionId = regionId; } + public void setShowAvatars(boolean showAvatars) { + this.showAvatars = showAvatars; + } + + public boolean isShowAvatars() { + return showAvatars; + } + + public void setShowAvatarList(List showAvatarList) { + this.showAvatarList = showAvatarList; + } + + public List getShowAvatarList() { + return showAvatarList; + } + public boolean inMoonCard() { return moonCard; } @@ -550,11 +579,7 @@ public class Player { } public void rechargeMoonCard() { - LinkedList items = new LinkedList(); - for (int i = 0; i < 300; i++) { - items.add(new GameItem(203)); - } - inventory.addItems(items); + inventory.addItem(new GameItem(203, 300)); if (!moonCard) { moonCard = true; Date now = new Date(); @@ -592,6 +617,35 @@ public class Player { session.send(new PacketCardProductRewardNotify(getMoonCardRemainDays())); } + public List getShopLimit() { + return shopLimit; + } + + public int getGoodsLimitNum(int goodsId) { + for (ShopLimit sl : getShopLimit()) { + if (sl.getShopGoodId() == goodsId) + return sl.getHasBought(); + } + return 0; + } + + public void addShopLimit(int goodsId, int boughtCount) { + boolean found = false; + for (ShopLimit sl : getShopLimit()) { + if (sl.getShopGoodId() == goodsId){ + sl.setHasBought(sl.getHasBought() + boughtCount); + found = true; + } + } + if (!found) { + ShopLimit sl = new ShopLimit(); + sl.setShopGoodId(goodsId); + sl.setHasBought(boughtCount); + shopLimit.add(sl); + } + this.save(); + } + public boolean inGodmode() { return godmode; } @@ -714,19 +768,30 @@ public class Player { return; } - // Delete - entity.getScene().removeEntity(entity); - // Handle if (entity instanceof EntityItem) { // Pick item EntityItem drop = (EntityItem) entity; + if (!drop.isShare()) // check drop owner to avoid someone picked up item in others' world + { + int dropOwner = (int)(drop.getGuid() >> 32); + if (dropOwner != getUid()) + return; + } + entity.getScene().removeEntity(entity); GameItem item = new GameItem(drop.getItemData(), drop.getCount()); // Add to inventory boolean success = getInventory().addItem(item, ActionReason.SubfieldDrop); if (success) { - this.sendPacket(new PacketGadgetInteractRsp(drop, InteractType.INTERACT_PICK_ITEM)); + + if (!drop.isShare()) // not shared drop + this.sendPacket(new PacketGadgetInteractRsp(drop, InteractType.INTERACT_PICK_ITEM)); + else + this.getScene().broadcastPacket(new PacketGadgetInteractRsp(drop, InteractType.INTERACT_PICK_ITEM)); } + } else { + // Delete directly + entity.getScene().removeEntity(entity); } return; @@ -754,7 +819,7 @@ public class Player { .setMpSettingType(this.getMpSetting()) .setNameCardId(this.getNameCardId()) .setSignature(this.getSignature()) - .setAvatarId(HeadImage.newBuilder().setAvatarId(this.getHeadImage()).getAvatarId()); + .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(this.getHeadImage())); if (this.getWorld() != null) { onlineInfo.setCurPlayerNumInWorld(this.getWorld().getPlayers().indexOf(this) + 1); @@ -787,15 +852,49 @@ public class Player { } public SocialDetail.Builder getSocialDetail() { + List socialShowAvatarInfoList = new ArrayList<>(); + if (this.isOnline()) { + if (this.getShowAvatarList() != null) { + for (int avatarId : this.getShowAvatarList()) { + socialShowAvatarInfoList.add( + socialShowAvatarInfoList.size(), + SocialShowAvatarInfoOuterClass.SocialShowAvatarInfo.newBuilder() + .setAvatarId(avatarId) + .setLevel(getAvatars().getAvatarById(avatarId).getLevel()) + .setCostumeId(getAvatars().getAvatarById(avatarId).getCostume()) + .build() + ); + } + } + } else { + List showAvatarList = DatabaseHelper.getPlayerById(id).getShowAvatarList(); + AvatarStorage avatars = DatabaseHelper.getPlayerById(id).getAvatars(); + avatars.loadFromDatabase(); + if (showAvatarList != null) { + for (int avatarId : showAvatarList) { + socialShowAvatarInfoList.add( + socialShowAvatarInfoList.size(), + SocialShowAvatarInfoOuterClass.SocialShowAvatarInfo.newBuilder() + .setAvatarId(avatarId) + .setLevel(avatars.getAvatarById(avatarId).getLevel()) + .setCostumeId(avatars.getAvatarById(avatarId).getCostume()) + .build() + ); + } + } + } + SocialDetail.Builder social = SocialDetail.newBuilder() .setUid(this.getUid()) - .setAvatarId(HeadImage.newBuilder().setAvatarId(this.getHeadImage()).getAvatarId()) + .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(this.getHeadImage())) .setNickname(this.getNickname()) .setSignature(this.getSignature()) .setLevel(this.getLevel()) .setBirthday(this.getBirthday().getFilledProtoWhenNotEmpty()) .setWorldLevel(this.getWorldLevel()) .setNameCardId(this.getNameCardId()) + .setIsShowAvatar(this.isShowAvatars()) + .addAllShowAvatarInfoList(socialShowAvatarInfoList) .setFinishAchievementNum(0); return social; } @@ -901,6 +1000,8 @@ public class Player { session.send(new PacketPlayerStoreNotify(this)); session.send(new PacketAvatarDataNotify(this)); + getTodayMoonCard(); // The timer works at 0:0, some users log in after that, use this method to check if they have received a reward today or not. If not, send the reward. + session.send(new PacketPlayerEnterSceneNotify(this)); // Enter game world session.send(new PacketPlayerLevelRewardUpdateNotify(rewardedLevels)); session.send(new PacketOpenStateUpdateNotify()); diff --git a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java index 768a3a7ca..b0e3b819b 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopInfo.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopInfo.java @@ -1,5 +1,153 @@ package emu.grasscutter.game.shop; -public class ShopInfo { +import emu.grasscutter.data.common.ItemParamData; +import java.util.ArrayList; +import java.util.List; + +public class ShopInfo { + private int goodsId = 0; + private ItemParamData goodsItem; + private int scoin = 0; + private List costItemList; + private int boughtNum = 0; + private int buyLimit = 0; + private int beginTime = 0; + private int endTime = 1924992000; + private int nextRefreshTime = 1924992000; + private int minLevel = 0; + private int maxLevel = 61; + private List preGoodsIdList = new ArrayList<>(); + private int mcoin = 0; + private int hcoin = 0; + private int disableType = 0; + private int secondarySheetId = 0; + + public int getHcoin() { + return hcoin; + } + + public void setHcoin(int hcoin) { + this.hcoin = hcoin; + } + + public List getPreGoodsIdList() { + return preGoodsIdList; + } + + public void setPreGoodsIdList(List preGoodsIdList) { + this.preGoodsIdList = preGoodsIdList; + } + + public int getMcoin() { + return mcoin; + } + + public void setMcoin(int mcoin) { + this.mcoin = mcoin; + } + + public int getDisableType() { + return disableType; + } + + public void setDisableType(int disableType) { + this.disableType = disableType; + } + + public int getSecondarySheetId() { + return secondarySheetId; + } + + public void setSecondarySheetId(int secondarySheetId) { + this.secondarySheetId = secondarySheetId; + } + + public int getGoodsId() { + return goodsId; + } + + public void setGoodsId(int goodsId) { + this.goodsId = goodsId; + } + + public ItemParamData getGoodsItem() { + return goodsItem; + } + + public void setGoodsItem(ItemParamData goodsItem) { + this.goodsItem = goodsItem; + } + + public int getScoin() { + return scoin; + } + + public void setScoin(int scoin) { + this.scoin = scoin; + } + + public List getCostItemList() { + return costItemList; + } + + public void setCostItemList(List costItemList) { + this.costItemList = costItemList; + } + + public int getBoughtNum() { + return boughtNum; + } + + public void setBoughtNum(int boughtNum) { + this.boughtNum = boughtNum; + } + + public int getBuyLimit() { + return buyLimit; + } + + public void setBuyLimit(int buyLimit) { + this.buyLimit = buyLimit; + } + + public int getBeginTime() { + return beginTime; + } + + public void setBeginTime(int beginTime) { + this.beginTime = beginTime; + } + + public int getEndTime() { + return endTime; + } + + public void setEndTime(int endTime) { + this.endTime = endTime; + } + + public int getNextRefreshTime() { + return nextRefreshTime; + } + + public void setNextRefreshTime(int nextRefreshTime) { + this.nextRefreshTime = nextRefreshTime; + } + + public int getMinLevel() { + return minLevel; + } + + public void setMinLevel(int minLevel) { + this.minLevel = minLevel; + } + + public int getMaxLevel() { + return maxLevel; + } + + public void setMaxLevel(int maxLevel) { + this.maxLevel = maxLevel; + } } diff --git a/src/main/java/emu/grasscutter/game/shop/ShopLimit.java b/src/main/java/emu/grasscutter/game/shop/ShopLimit.java new file mode 100644 index 000000000..ded87102e --- /dev/null +++ b/src/main/java/emu/grasscutter/game/shop/ShopLimit.java @@ -0,0 +1,25 @@ +package emu.grasscutter.game.shop; + +import dev.morphia.annotations.Entity; + +@Entity +public class ShopLimit { + public int getShopGoodId() { + return shopGoodId; + } + + public void setShopGoodId(int shopGoodId) { + this.shopGoodId = shopGoodId; + } + + public int getHasBought() { + return hasBought; + } + + public void setHasBought(int hasBought) { + this.hasBought = hasBought; + } + + private int shopGoodId; + private int hasBought; +} diff --git a/src/main/java/emu/grasscutter/game/shop/ShopManager.java b/src/main/java/emu/grasscutter/game/shop/ShopManager.java index a21888f25..63436e517 100644 --- a/src/main/java/emu/grasscutter/game/shop/ShopManager.java +++ b/src/main/java/emu/grasscutter/game/shop/ShopManager.java @@ -1,12 +1,46 @@ package emu.grasscutter.game.shop; +import com.google.gson.reflect.TypeToken; +import emu.grasscutter.Grasscutter; import emu.grasscutter.server.game.GameServer; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; + +import java.io.FileReader; +import java.util.Collection; +import java.util.List; public class ShopManager { private final GameServer server; - + + public Int2ObjectMap> getShopData() { + return shopData; + } + + private final Int2ObjectMap> shopData; + public ShopManager(GameServer server) { this.server = server; + this.shopData = new Int2ObjectOpenHashMap<>(); + this.load(); + } + + public synchronized void load() { + try (FileReader fileReader = new FileReader(Grasscutter.getConfig().DATA_FOLDER + "Shop.json")) { + getShopData().clear(); + List banners = Grasscutter.getGsonFactory().fromJson(fileReader, TypeToken.getParameterized(Collection.class, ShopTable.class).getType()); + if(banners.size() > 0) { + for (ShopTable shopTable : banners) { + getShopData().put(shopTable.getShopId(), shopTable.getItems()); + } + Grasscutter.getLogger().info("Shop data successfully loaded."); + } else { + Grasscutter.getLogger().error("Unable to load shop data. Shop data size is 0."); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } public GameServer getServer() { diff --git a/src/main/java/emu/grasscutter/game/shop/ShopTable.java b/src/main/java/emu/grasscutter/game/shop/ShopTable.java new file mode 100644 index 000000000..7396cd028 --- /dev/null +++ b/src/main/java/emu/grasscutter/game/shop/ShopTable.java @@ -0,0 +1,25 @@ +package emu.grasscutter.game.shop; + +import java.util.ArrayList; +import java.util.List; + +public class ShopTable { + private int shopId; + private List items = new ArrayList<>(); + + public int getShopId() { + return shopId; + } + + public void setShopId(int shopId) { + this.shopId = shopId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/src/main/java/emu/grasscutter/game/world/Scene.java b/src/main/java/emu/grasscutter/game/world/Scene.java index d9b01e0ef..543079153 100644 --- a/src/main/java/emu/grasscutter/game/world/Scene.java +++ b/src/main/java/emu/grasscutter/game/world/Scene.java @@ -1,28 +1,12 @@ package emu.grasscutter.game.world; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -import org.danilopianini.util.SpatialIndex; - import emu.grasscutter.Grasscutter; import emu.grasscutter.data.GameData; import emu.grasscutter.data.GameDepot; -import emu.grasscutter.data.GameResource; import emu.grasscutter.data.def.MonsterData; import emu.grasscutter.data.def.SceneData; import emu.grasscutter.data.def.WorldLevelData; -import emu.grasscutter.game.entity.EntityAvatar; -import emu.grasscutter.game.entity.EntityClientGadget; -import emu.grasscutter.game.entity.EntityGadget; -import emu.grasscutter.game.entity.EntityMonster; -import emu.grasscutter.game.entity.GameEntity; +import emu.grasscutter.game.entity.*; import emu.grasscutter.game.player.Player; import emu.grasscutter.game.player.TeamInfo; import emu.grasscutter.game.props.ClimateType; @@ -37,10 +21,12 @@ import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; import emu.grasscutter.server.packet.send.PacketLifeStateChangeNotify; import emu.grasscutter.server.packet.send.PacketSceneEntityAppearNotify; import emu.grasscutter.server.packet.send.PacketSceneEntityDisappearNotify; -import emu.grasscutter.utils.Utils; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import org.danilopianini.util.SpatialIndex; + +import java.util.*; public class Scene { private final World world; @@ -228,6 +214,11 @@ public class Scene { this.addEntityDirectly(entity); this.broadcastPacket(new PacketSceneEntityAppearNotify(entity)); } + + public synchronized void addEntityToSingleClient(Player player, GameEntity entity) { + this.addEntityDirectly(entity); + player.sendPacket(new PacketSceneEntityAppearNotify(entity)); + } public synchronized void addEntities(Collection entities) { for (GameEntity entity : entities) { @@ -310,6 +301,12 @@ public class Scene { public void killEntity(GameEntity target, int attackerId) { // Packet this.broadcastPacket(new PacketLifeStateChangeNotify(attackerId, target, LifeState.LIFE_DEAD)); + + // Reward drop + if (target instanceof EntityMonster) { + Grasscutter.getGameServer().getDropManager().callDrop((EntityMonster) target); + } + this.removeEntity(target); // Death event diff --git a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java index 1ac721ce2..99c5e4f8d 100644 --- a/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java +++ b/src/main/java/emu/grasscutter/server/dispatch/DispatchServer.java @@ -339,6 +339,10 @@ public final class DispatchServer { // added. account = DatabaseHelper.createAccountWithId(requestData.account, 0); + for (String permission : Grasscutter.getConfig().getDispatchOptions().defaultPermissions) { + account.addPermission(permission); + } + if (account != null) { responseData.message = "OK"; responseData.data.account.uid = account.getId(); diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index 50889fd56..2b22b6a95 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -1,15 +1,11 @@ package emu.grasscutter.server.game; -import java.net.InetSocketAddress; -import java.time.OffsetDateTime; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - import emu.grasscutter.GameConstants; import emu.grasscutter.Grasscutter; import emu.grasscutter.command.CommandMap; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.Account; +import emu.grasscutter.game.drop.DropManager; import emu.grasscutter.game.dungeons.DungeonManager; import emu.grasscutter.game.gacha.GachaManager; import emu.grasscutter.game.managers.ChatManager; @@ -27,6 +23,11 @@ import emu.grasscutter.server.event.internal.ServerStartEvent; import emu.grasscutter.server.event.internal.ServerStopEvent; import emu.grasscutter.task.TaskMap; +import java.net.InetSocketAddress; +import java.time.OffsetDateTime; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + public final class GameServer extends KcpServer { private final InetSocketAddress address; private final GameServerPacketHandler packetHandler; @@ -42,6 +43,7 @@ public final class GameServer extends KcpServer { private final DungeonManager dungeonManager; private final CommandMap commandMap; private final TaskMap taskMap; + private final DropManager dropManager; public GameServer(InetSocketAddress address) { super(address); @@ -60,6 +62,7 @@ public final class GameServer extends KcpServer { this.dungeonManager = new DungeonManager(this); this.commandMap = new CommandMap(true); this.taskMap = new TaskMap(true); + this.dropManager = new DropManager(this); // Schedule game loop. Timer gameLoop = new Timer(); @@ -109,6 +112,10 @@ public final class GameServer extends KcpServer { public MultiplayerManager getMultiplayerManager() { return multiplayerManager; } + + public DropManager getDropManager() { + return dropManager; + } public DungeonManager getDungeonManager() { return dungeonManager; @@ -196,6 +203,7 @@ public final class GameServer extends KcpServer { @Override public void onStartFinish() { + Grasscutter.getLogger().info("Grasscutter is FREE software. If you have paid for this, you may have been scammed. Homepage: https://github.com/Grasscutters/Grasscutter"); Grasscutter.getLogger().info("Game Server started on port " + address.getPort()); ServerStartEvent event = new ServerStartEvent(ServerEvent.Type.GAME, OffsetDateTime.now()); event.call(); } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java new file mode 100644 index 000000000..1df08de43 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerBuyGoodsReq.java @@ -0,0 +1,67 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.data.GameData; +import emu.grasscutter.game.inventory.GameItem; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.props.PlayerProperty; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.BuyGoodsReqOuterClass; +import emu.grasscutter.net.proto.ItemParamOuterClass; +import emu.grasscutter.net.proto.ShopGoodsOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketBuyGoodsRsp; +import emu.grasscutter.server.packet.send.PacketStoreItemChangeNotify; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Optional; + +@Opcodes(PacketOpcodes.BuyGoodsReq) +public class HandlerBuyGoodsReq extends PacketHandler { + + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + BuyGoodsReqOuterClass.BuyGoodsReq buyGoodsReq = BuyGoodsReqOuterClass.BuyGoodsReq.parseFrom(payload); + + for (ShopGoodsOuterClass.ShopGoods sg : buyGoodsReq.getGoodsListList()) { + if (sg.getScoin() > 0 && session.getPlayer().getMora() < buyGoodsReq.getBoughtNum() * sg.getScoin()) { + return; + } + if (sg.getHcoin() > 0 && session.getPlayer().getPrimogems() < buyGoodsReq.getBoughtNum() * sg.getHcoin()) { + return; + } + if (sg.getMcoin() > 0 && session.getPlayer().getCrystals() < buyGoodsReq.getBoughtNum() * sg.getMcoin()) { + return; + } + + HashMap itemsCache = new HashMap<>(); + for (ItemParamOuterClass.ItemParam p : sg.getCostItemListList()) { + Optional invItem = session.getPlayer().getInventory().getItems().values().stream().filter(x -> x.getItemId() == p.getItemId()).findFirst(); + if (invItem.isEmpty() || invItem.get().getCount() < p.getCount()) + return; + itemsCache.put(invItem.get(), p.getCount() * buyGoodsReq.getBoughtNum()); + } + + session.getPlayer().setMora(session.getPlayer().getMora() - buyGoodsReq.getBoughtNum() * sg.getScoin()); + session.getPlayer().setPrimogems(session.getPlayer().getPrimogems() - buyGoodsReq.getBoughtNum() * sg.getHcoin()); + session.getPlayer().setCrystals(session.getPlayer().getCrystals() - buyGoodsReq.getBoughtNum() * sg.getMcoin()); + + if (!itemsCache.isEmpty()) { + for (GameItem gi : itemsCache.keySet()) { + session.getPlayer().getInventory().removeItem(gi, itemsCache.get(gi)); + } + itemsCache.clear(); + } + + session.getPlayer().addShopLimit(sg.getGoodsId(), buyGoodsReq.getBoughtNum()); + GameItem item = new GameItem(GameData.getItemDataMap().get(sg.getGoodsItem().getItemId())); + item.setCount(buyGoodsReq.getBoughtNum() * sg.getGoodsItem().getCount()); + session.getPlayer().getInventory().addItem(item, ActionReason.Shop, true); // fix: not notify when got virtual item from shop + session.send(new PacketBuyGoodsRsp(buyGoodsReq.getShopType(), session.getPlayer().getGoodsLimitNum(sg.getGoodsId()), sg)); + } + + session.getPlayer().save(); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetShopReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetShopReq.java index 9b7c6e96d..1dbe1c3ff 100644 --- a/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetShopReq.java +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerGetShopReq.java @@ -1,9 +1,9 @@ package emu.grasscutter.server.packet.recv; import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.GetShopReqOuterClass.GetShopReq; -import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.server.game.GameSession; import emu.grasscutter.server.packet.send.PacketGetShopRsp; @@ -12,8 +12,7 @@ public class HandlerGetShopReq extends PacketHandler { @Override public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { GetShopReq req = GetShopReq.parseFrom(payload); - - // TODO - session.send(new PacketGetShopRsp(req.getShopType())); + + session.send(new PacketGetShopRsp(session.getPlayer(), req.getShopType())); } } diff --git a/src/main/java/emu/grasscutter/server/packet/recv/HandlerUpdatePlayerShowAvatarListReq.java b/src/main/java/emu/grasscutter/server/packet/recv/HandlerUpdatePlayerShowAvatarListReq.java new file mode 100644 index 000000000..d04fb7308 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/recv/HandlerUpdatePlayerShowAvatarListReq.java @@ -0,0 +1,22 @@ +package emu.grasscutter.server.packet.recv; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.net.packet.Opcodes; +import emu.grasscutter.net.packet.PacketHandler; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.UpdatePlayerShowAvatarListReqOuterClass; +import emu.grasscutter.server.game.GameSession; +import emu.grasscutter.server.packet.send.PacketUpdatePlayerShowAvatarListRsp; + +@Opcodes(PacketOpcodes.UpdatePlayerShowAvatarListReq) +public class HandlerUpdatePlayerShowAvatarListReq extends PacketHandler { + @Override + public void handle(GameSession session, byte[] header, byte[] payload) throws Exception { + UpdatePlayerShowAvatarListReqOuterClass.UpdatePlayerShowAvatarListReq req = UpdatePlayerShowAvatarListReqOuterClass.UpdatePlayerShowAvatarListReq.parseFrom(payload); + + session.getPlayer().setShowAvatars(req.getIsShowAvatar()); + session.getPlayer().setShowAvatarList(req.getShowAvatarIdListList()); + + session.send(new PacketUpdatePlayerShowAvatarListRsp(req.getIsShowAvatar(), req.getShowAvatarIdListList())); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketBuyGoodsRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketBuyGoodsRsp.java new file mode 100644 index 000000000..07371440d --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketBuyGoodsRsp.java @@ -0,0 +1,22 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.BuyGoodsRspOuterClass; +import emu.grasscutter.net.proto.ShopGoodsOuterClass; + +public class PacketBuyGoodsRsp extends BasePacket { + public PacketBuyGoodsRsp(int shopType, int boughtNum, ShopGoodsOuterClass.ShopGoods sg) { + super(PacketOpcodes.BuyGoodsRsp); + + BuyGoodsRspOuterClass.BuyGoodsRsp buyGoodsRsp = BuyGoodsRspOuterClass.BuyGoodsRsp.newBuilder() + .setShopType(shopType) + .setBoughtNum(boughtNum) + .addGoodsList(ShopGoodsOuterClass.ShopGoods.newBuilder() + .mergeFrom(sg) + .setBoughtNum(boughtNum) + ).build(); + + this.setData(buyGoodsRsp); + } +} diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketChangeAvatarRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketChangeAvatarRsp.java index f5efe6808..0fd69fe26 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketChangeAvatarRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketChangeAvatarRsp.java @@ -11,7 +11,7 @@ public class PacketChangeAvatarRsp extends BasePacket { super(PacketOpcodes.ChangeAvatarRsp); ChangeAvatarRsp p = ChangeAvatarRsp.newBuilder() - .setRetcode(RetcodeOuterClass.Retcode.RET_SVR_ERROR_VALUE) + .setRetcode(RetcodeOuterClass.Retcode.RET_SUCC_VALUE) .setCurGuid(guid) .build(); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetAllMailRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetAllMailRsp.java index 8c8fb4b07..59af30f42 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetAllMailRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetAllMailRsp.java @@ -14,32 +14,23 @@ import emu.grasscutter.net.proto.MailTextContentOuterClass.MailTextContent; import java.time.Instant; import java.util.ArrayList; -import java.util.Base64; import java.util.List; public class PacketGetAllMailRsp extends BasePacket { public PacketGetAllMailRsp(Player player, boolean isGiftMail) { super(PacketOpcodes.GetAllMailRsp); + GetAllMailRsp.Builder proto = GetAllMailRsp.newBuilder(); if (isGiftMail) { - // TODO: Gift Mail - // Make sure to send the stupid empty packet - Base64.Decoder decoder = Base64.getDecoder(); - byte[] rsp = decoder.decode("IAE="); - try { - GetAllMailRsp var = GetAllMailRsp.parseFrom(rsp); - this.setData(var.toBuilder().build()); - } catch (Exception e) { - } - + proto.setIsGiftMail(true); } else { + proto.setIsGiftMail(false); + if (player.getAllMail().size() != 0) { // Make sure the player has mail - GetAllMailRsp.Builder proto = GetAllMailRsp.newBuilder(); List mailDataList = new ArrayList(); for (Mail message : player.getAllMail()) { - if(message.stateValue == 1) { // Make sure it isn't a gift if (message.expireTime > (int) Instant.now().getEpochSecond()) { // Make sure the message isn't expired (The game won't show expired mail, but I don't want to send unnecessary information). if(mailDataList.size() <= 1000) { // Make sure that there isn't over 1000 messages in the mailbox. (idk what will happen if there is but the game probably won't like it.) @@ -79,17 +70,8 @@ public class PacketGetAllMailRsp extends BasePacket { proto.addAllMailList(mailDataList); proto.setIsTruncated(mailDataList.size() <= 1000 ? false : true); // When enabled this will send a notification to the user telling them their inbox is full and they should delete old messages when opening the mailbox. - - this.setData(proto.build()); - } else { - // Make sure to send the stupid empty packet - Base64.Decoder decoder = Base64.getDecoder(); - byte[] rsp = decoder.decode("IAE="); - try { - GetAllMailRsp var = GetAllMailRsp.parseFrom(rsp); - this.setData(var.toBuilder().build()); - } catch (Exception e) {} } } + this.setData(proto.build()); } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java index b2628e36d..dbaa4b316 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetPlayerFriendListRsp.java @@ -8,7 +8,7 @@ import emu.grasscutter.net.packet.PacketOpcodes; import emu.grasscutter.net.proto.FriendBriefOuterClass.FriendBrief; import emu.grasscutter.net.proto.FriendOnlineStateOuterClass.FriendOnlineState; import emu.grasscutter.net.proto.GetPlayerFriendListRspOuterClass.GetPlayerFriendListRsp; -import emu.grasscutter.net.proto.HeadImageOuterClass.HeadImage; +import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; import emu.grasscutter.net.proto.PlatformTypeOuterClass; public class PacketGetPlayerFriendListRsp extends BasePacket { @@ -20,7 +20,7 @@ public class PacketGetPlayerFriendListRsp extends BasePacket { .setUid(GameConstants.SERVER_CONSOLE_UID) .setNickname("Server") .setLevel(1) - .setAvatarId(HeadImage.newBuilder().setAvatarId(GameConstants.MAIN_CHARACTER_FEMALE).getAvatarId()) + .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(GameConstants.MAIN_CHARACTER_FEMALE)) .setWorldLevel(0) .setSignature("") .setLastActiveTime((int) (System.currentTimeMillis() / 1000f)) diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java index 64d415763..4f57c72e6 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketGetShopRsp.java @@ -1,19 +1,60 @@ package emu.grasscutter.server.packet.send; +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.player.Player; +import emu.grasscutter.game.shop.ShopInfo; +import emu.grasscutter.game.shop.ShopManager; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.GetShopRspOuterClass.GetShopRsp; +import emu.grasscutter.net.proto.GetShopRspOuterClass; +import emu.grasscutter.net.proto.ItemParamOuterClass; +import emu.grasscutter.net.proto.ShopGoodsOuterClass.ShopGoods; import emu.grasscutter.net.proto.ShopOuterClass.Shop; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + public class PacketGetShopRsp extends BasePacket { - - public PacketGetShopRsp(int shopType) { + + public PacketGetShopRsp(Player inv, int shopType) { super(PacketOpcodes.GetShopRsp); - GetShopRsp proto = GetShopRsp.newBuilder() - .setShop(Shop.newBuilder().setShopType(shopType)) - .build(); - - this.setData(proto); + // TODO: CityReputationLevel + Shop.Builder shop = Shop.newBuilder() + .setShopType(shopType) + .setCityId(1) //mock + .setCityReputationLevel(10); //mock + + ShopManager manager = Grasscutter.getGameServer().getShopManager(); + if (manager.getShopData().get(shopType) != null) { + List list = manager.getShopData().get(shopType); + List goodsList = new ArrayList<>(); + for (ShopInfo info : list) { + ShopGoods.Builder goods = ShopGoods.newBuilder() + .setGoodsId(info.getGoodsId()) + .setGoodsItem(ItemParamOuterClass.ItemParam.newBuilder().setItemId(info.getGoodsItem().getId()).setCount(info.getGoodsItem().getCount()).build()) + .setScoin(info.getScoin()) + .setHcoin(info.getHcoin()) + .setBoughtNum(inv.getGoodsLimitNum(info.getGoodsId())) + .setBuyLimit(info.getBuyLimit()) + .setBeginTime(info.getBeginTime()) + .setEndTime(info.getEndTime()) + .setNextRefreshTime(info.getNextRefreshTime()) + .setMinLevel(info.getMinLevel()) + .setMaxLevel(info.getMaxLevel()) + .addAllPreGoodsIdList(info.getPreGoodsIdList()) + .setMcoin(info.getMcoin()) + .setDisableType(info.getDisableType()) + .setSecondarySheetId(info.getSecondarySheetId()); + if (info.getCostItemList() != null) { + goods.addAllCostItemList(info.getCostItemList().stream().map(x -> ItemParamOuterClass.ItemParam.newBuilder().setItemId(x.getId()).setCount(x.getCount()).build()).collect(Collectors.toList())); + } + goodsList.add(goods.build()); + } + shop.addAllGoodsList(goodsList); + } + + this.setData(GetShopRspOuterClass.GetShopRsp.newBuilder().setShop(shop).build()); } } diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketSetPlayerHeadImageRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketSetPlayerHeadImageRsp.java index 9b736dc3e..4cbfd4b46 100644 --- a/src/main/java/emu/grasscutter/server/packet/send/PacketSetPlayerHeadImageRsp.java +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketSetPlayerHeadImageRsp.java @@ -3,7 +3,7 @@ package emu.grasscutter.server.packet.send; import emu.grasscutter.game.player.Player; import emu.grasscutter.net.packet.BasePacket; import emu.grasscutter.net.packet.PacketOpcodes; -import emu.grasscutter.net.proto.HeadImageOuterClass.HeadImage; +import emu.grasscutter.net.proto.ProfilePictureOuterClass.ProfilePicture; import emu.grasscutter.net.proto.SetPlayerHeadImageRspOuterClass.SetPlayerHeadImageRsp; public class PacketSetPlayerHeadImageRsp extends BasePacket { @@ -12,7 +12,7 @@ public class PacketSetPlayerHeadImageRsp extends BasePacket { super(PacketOpcodes.SetPlayerHeadImageRsp); SetPlayerHeadImageRsp proto = SetPlayerHeadImageRsp.newBuilder() - .setAvatarId(HeadImage.newBuilder().setAvatarId(player.getHeadImage()).getAvatarId()) + .setProfilePicture(ProfilePicture.newBuilder().setAvatarId(player.getHeadImage())) .build(); this.setData(proto); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketUpdatePlayerShowAvatarListRsp.java b/src/main/java/emu/grasscutter/server/packet/send/PacketUpdatePlayerShowAvatarListRsp.java new file mode 100644 index 000000000..1785c9f45 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketUpdatePlayerShowAvatarListRsp.java @@ -0,0 +1,22 @@ +package emu.grasscutter.server.packet.send; + +import emu.grasscutter.net.packet.BasePacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.UpdatePlayerShowAvatarListRspOuterClass.UpdatePlayerShowAvatarListRsp; + +import java.util.List; + +public class PacketUpdatePlayerShowAvatarListRsp extends BasePacket { + + public PacketUpdatePlayerShowAvatarListRsp(boolean isShowAvatar, List avatarIds) { + super(PacketOpcodes.UpdatePlayerShowAvatarListRsp); + + UpdatePlayerShowAvatarListRsp proto = UpdatePlayerShowAvatarListRsp.newBuilder() + .setIsShowAvatar(isShowAvatar) + .addAllShowAvatarIdList(avatarIds) + .setRetcode(0) + .build(); + + this.setData(proto); + } +} diff --git a/src/main/java/emu/grasscutter/task/Task.java b/src/main/java/emu/grasscutter/task/Task.java index 34638a777..1f35d16ce 100644 --- a/src/main/java/emu/grasscutter/task/Task.java +++ b/src/main/java/emu/grasscutter/task/Task.java @@ -1,5 +1,7 @@ package emu.grasscutter.task; +import org.quartz.JobDataMap; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -27,4 +29,6 @@ public @interface Task { String taskName() default "NO_NAME"; String taskCronExpression() default "0 0 0 0 0 ?"; String triggerName() default "NO_NAME"; + boolean executeImmediatelyAfterReset() default false; + boolean executeImmediately() default false; } diff --git a/src/main/java/emu/grasscutter/task/TaskHandler.java b/src/main/java/emu/grasscutter/task/TaskHandler.java index e1a160a07..ea6fe22cd 100644 --- a/src/main/java/emu/grasscutter/task/TaskHandler.java +++ b/src/main/java/emu/grasscutter/task/TaskHandler.java @@ -1,11 +1,26 @@ package emu.grasscutter.task; -import org.quartz.Job; -import org.quartz.JobExecutionContext; -import org.quartz.JobExecutionException; +import org.quartz.*; -public interface TaskHandler extends Job { - default void execute(JobExecutionContext context) throws JobExecutionException { - +@PersistJobDataAfterExecution +public class TaskHandler implements Job { + + public void restartExecute() throws JobExecutionException { + execute(null); } + + public void onEnable() { + + } + + public void onDisable() { + + } + + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + // TODO Auto-generated method stub + + } + } diff --git a/src/main/java/emu/grasscutter/task/TaskMap.java b/src/main/java/emu/grasscutter/task/TaskMap.java index 077ff1f59..fe067e795 100644 --- a/src/main/java/emu/grasscutter/task/TaskMap.java +++ b/src/main/java/emu/grasscutter/task/TaskMap.java @@ -1,20 +1,9 @@ package emu.grasscutter.task; import emu.grasscutter.Grasscutter; -import emu.grasscutter.game.Account; -import emu.grasscutter.game.player.Player; -import org.quartz.CronScheduleBuilder; -import org.quartz.CronTrigger; -import org.quartz.JobBuilder; -import org.quartz.JobDetail; -import org.quartz.Scheduler; -import org.quartz.SchedulerException; -import org.quartz.SchedulerFactory; -import org.quartz.Trigger; -import org.quartz.TriggerBuilder; +import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; -import org.quartz.spi.MutableTrigger; import org.reflections.Reflections; import java.util.*; @@ -23,6 +12,7 @@ import java.util.*; public final class TaskMap { private final Map tasks = new HashMap<>(); private final Map annotations = new HashMap<>(); + private final Map afterReset = new HashMap<>(); private final SchedulerFactory schedulerFactory = new StdSchedulerFactory(); public TaskMap() { @@ -37,6 +27,46 @@ public final class TaskMap { return Grasscutter.getGameServer().getTaskMap(); } + public void resetNow() { + // Unregister all tasks + for (TaskHandler task : this.tasks.values()) { + unregisterTask(task); + } + + // Run all afterReset tasks + for (TaskHandler task : this.afterReset.values()) { + try { + task.restartExecute(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Remove all afterReset tasks + this.afterReset.clear(); + + // Register all tasks + for (TaskHandler task : this.tasks.values()) { + registerTask(task.getClass().getAnnotation(Task.class).taskName(), task); + } + } + + public TaskMap unregisterTask(TaskHandler task) { + this.tasks.remove(task.getClass().getAnnotation(Task.class).taskName()); + this.annotations.remove(task.getClass().getAnnotation(Task.class).taskName()); + + try { + Scheduler scheduler = schedulerFactory.getScheduler(); + scheduler.deleteJob(new JobKey(task.getClass().getAnnotation(Task.class).taskName())); + } catch (SchedulerException e) { + e.printStackTrace(); + } + + task.onDisable(); + + return this; + } + public TaskMap registerTask(String taskName, TaskHandler task) { Task annotation = task.getClass().getAnnotation(Task.class); this.annotations.put(taskName, annotation); @@ -56,7 +86,11 @@ public final class TaskMap { .build(); scheduler.scheduleJob(job, convTrigger); - scheduler.start(); + + if (annotation.executeImmediately()) { + task.execute(null); + } + task.onEnable(); } catch (SchedulerException e) { e.printStackTrace(); } @@ -83,12 +117,24 @@ public final class TaskMap { try { Task taskData = annotated.getAnnotation(Task.class); Object object = annotated.newInstance(); - if (object instanceof TaskHandler) + if (object instanceof TaskHandler) { this.registerTask(taskData.taskName(), (TaskHandler) object); - else Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a TaskHandler!"); + if (taskData.executeImmediatelyAfterReset()) { + this.afterReset.put(taskData.taskName(), (TaskHandler) object); + } + } else { + Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a TaskHandler!"); + } } catch (Exception exception) { Grasscutter.getLogger().error("Failed to register task handler for " + annotated.getSimpleName(), exception); } }); + try { + Scheduler scheduler = schedulerFactory.getScheduler(); + scheduler.start(); + } catch (SchedulerException e) { + e.printStackTrace(); + } + } } diff --git a/src/main/java/emu/grasscutter/task/tasks/MoonCard.java b/src/main/java/emu/grasscutter/task/tasks/MoonCard.java index 45c04586e..51088f46d 100644 --- a/src/main/java/emu/grasscutter/task/tasks/MoonCard.java +++ b/src/main/java/emu/grasscutter/task/tasks/MoonCard.java @@ -1,27 +1,35 @@ package emu.grasscutter.task.tasks; -import emu.grasscutter.database.DatabaseManager; -import emu.grasscutter.game.player.Player; +import emu.grasscutter.Grasscutter; import emu.grasscutter.task.Task; import emu.grasscutter.task.TaskHandler; -import java.util.List; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; @Task(taskName = "MoonCard", taskCronExpression = "0 0 0 * * ?", triggerName = "MoonCardTrigger") // taskCronExpression: Fixed time period: 0:0:0 every day (twenty-four hour system) -public final class MoonCard implements TaskHandler { +public class MoonCard extends TaskHandler { + @Override - public void execute(JobExecutionContext context) throws JobExecutionException { - List players = DatabaseManager.getDatastore().find(Player.class).stream().toList(); - for (Player player : players) { + public void onEnable() { + Grasscutter.getLogger().info("[Task] MoonCard task enabled."); + } + + @Override + public void onDisable() { + Grasscutter.getLogger().info("[Task] MoonCard task disabled."); + } + + @Override + public synchronized void execute(JobExecutionContext context) throws JobExecutionException { + Grasscutter.getGameServer().getPlayers().forEach((uid, player) -> { if (player.isOnline()) { if (player.inMoonCard()) { player.getTodayMoonCard(); } } - } + }); } } diff --git a/start.cmd b/start.cmd index f44a5f00d..9cee1a999 100644 --- a/start.cmd +++ b/start.cmd @@ -33,7 +33,7 @@ if not "%JAVA_PATH%" == "DO_NOT_CHECK_PATH" ( goto :EXIT ) ) else set JAVA_PATH= -if not exist "%SERVER_PATH%grasscutter.jar" ( +if not exist "%SERVER_PATH%%SERVER_JAR_NAME%" ( call :LOG [ERROR] Server jar not found. goto :EXIT ) @@ -129,7 +129,7 @@ del /f /q "%temp%\db.vbs" >nul 2>nul :GAME call :LOG [INFO] Starting server... -"%JAVA_PATH%java.exe" -jar "%SERVER_PATH%grasscutter.jar" +"%JAVA_PATH%java.exe" -jar "%SERVER_PATH%%SERVER_JAR_NAME%" call :LOG [INFO] Server stopped :EXIT