diff --git a/build.gradle b/build.gradle index d9f616ca9..70f6a9358 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,9 @@ dependencies { implementation group: 'org.greenrobot', name: 'eventbus-java', version: '3.3.1' implementation group: 'org.danilopianini', name: 'java-quadtree', version: '0.1.9' + implementation group: 'org.quartz-scheduler', name: 'quartz', version: '2.3.2' + implementation group: 'org.quartz-scheduler', name: 'quartz-jobs', version: '2.3.2' + protobuf files('proto/') } diff --git a/proto/CardProductRewardNotify.proto b/proto/CardProductRewardNotify.proto new file mode 100644 index 000000000..109e685e1 --- /dev/null +++ b/proto/CardProductRewardNotify.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +option java_package = "emu.grasscutter.net.proto"; + +message CardProductRewardNotify { + enum CmdId { + option allow_alias = true; + NONE = 0; + ENET_CHANNEL_ID = 0; + ENET_IS_RELIABLE = 1; + CMD_ID = 4105; + } + + string product_id = 1; + uint32 hcoin = 2; + uint32 remain_days = 3; +} diff --git a/src/main/java/emu/grasscutter/game/GenshinPlayer.java b/src/main/java/emu/grasscutter/game/GenshinPlayer.java index eac10a0cc..4c5afb88f 100644 --- a/src/main/java/emu/grasscutter/game/GenshinPlayer.java +++ b/src/main/java/emu/grasscutter/game/GenshinPlayer.java @@ -35,6 +35,7 @@ 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 it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; @@ -81,6 +82,11 @@ public class GenshinPlayer { private int mainCharacterId; private boolean godmode; + private boolean moonCard; + private Date moonCardStartTime; + private int moonCardDuration; + private Set moonCardGetTimes; + @Transient private boolean paused; @Transient private int enterSceneToken; @Transient private SceneLoadState sceneState; @@ -124,6 +130,7 @@ public class GenshinPlayer { this.birthday = new PlayerBirthday(); this.rewardedLevels = new HashSet<>(); + this.moonCardGetTimes = new HashSet<>(); } // On player creation @@ -486,6 +493,95 @@ public class GenshinPlayer { this.regionId = regionId; } + public boolean inMoonCard() { + return moonCard; + } + + public void setMoonCard(boolean moonCard) { + this.moonCard = moonCard; + } + + public void addMoonCardDays(int days) { + this.moonCardDuration += days; + } + + public int getMoonCardDuration() { + return moonCardDuration; + } + + public void setMoonCardDuration(int moonCardDuration) { + this.moonCardDuration = moonCardDuration; + } + + public Date getMoonCardStartTime() { + return moonCardStartTime; + } + + public void setMoonCardStartTime(Date moonCardStartTime) { + this.moonCardStartTime = moonCardStartTime; + } + + public Set getMoonCardGetTimes() { + return moonCardGetTimes; + } + + public void setMoonCardGetTimes(Set moonCardGetTimes) { + this.moonCardGetTimes = moonCardGetTimes; + } + + public int getMoonCardRemainDays() { + Calendar remainCalendar = Calendar.getInstance(); + remainCalendar.setTime(moonCardStartTime); + remainCalendar.add(Calendar.DATE, moonCardDuration); + Date theLastDay = remainCalendar.getTime(); + Date now = DateHelper.onlyYearMonthDay(new Date()); + return (int) ((theLastDay.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)); // By copilot + } + + public void rechargeMoonCard() { + LinkedList items = new LinkedList(); + for (int i = 0; i < 300; i++) { + items.add(new GenshinItem(203)); + } + inventory.addItems(items); + if (!moonCard) { + moonCard = true; + Date now = new Date(); + moonCardStartTime = DateHelper.onlyYearMonthDay(now); + moonCardDuration = 30; + } else { + moonCardDuration += 30; + } + if (!moonCardGetTimes.contains(moonCardStartTime)) { + moonCardGetTimes.add(moonCardStartTime); + } + } + + public void getTodayMoonCard() { + if (!moonCard) { + return; + } + Date now = DateHelper.onlyYearMonthDay(new Date()); + if (moonCardGetTimes.contains(now)) { + return; + } + Date stopTime = new Date(); + Calendar stopCalendar = Calendar.getInstance(); + stopCalendar.setTime(stopTime); + stopCalendar.add(Calendar.DATE, moonCardDuration); + stopTime = stopCalendar.getTime(); + if (now.after(stopTime)) { + moonCard = false; + return; + } + moonCardGetTimes.add(now); + addMoonCardDays(1); + GenshinItem genshinItem = new GenshinItem(201, 90); + getInventory().addItem(genshinItem); + session.send(new PacketItemAddHintNotify(genshinItem, ActionReason.BlessingRedeemReward)); + session.send(new PacketCardProductRewardNotify(getMoonCardRemainDays())); + } + public boolean inGodmode() { return godmode; } diff --git a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java index 4ea9e6bc1..4b71db904 100644 --- a/src/main/java/emu/grasscutter/game/managers/InventoryManager.java +++ b/src/main/java/emu/grasscutter/game/managers/InventoryManager.java @@ -922,6 +922,11 @@ public class InventoryManager { default: break; } + + if (useItem.getItemId() == 1202) { + player.rechargeMoonCard(); + used = 1; + } if (used > 0) { player.getInventory().removeItem(useItem, used); diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index b37b24f33..77f64fb88 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -25,6 +25,7 @@ import emu.grasscutter.server.event.ServerEvent; import emu.grasscutter.server.event.game.ServerTickEvent; import emu.grasscutter.server.event.internal.ServerStartEvent; import emu.grasscutter.server.event.internal.ServerStopEvent; +import emu.grasscutter.task.TaskMap; public final class GameServer extends MihoyoKcpServer { private final InetSocketAddress address; @@ -40,6 +41,7 @@ public final class GameServer extends MihoyoKcpServer { private final MultiplayerManager multiplayerManager; private final DungeonManager dungeonManager; private final CommandMap commandMap; + private final TaskMap taskMap; public GameServer(InetSocketAddress address) { super(address); @@ -57,6 +59,7 @@ public final class GameServer extends MihoyoKcpServer { this.multiplayerManager = new MultiplayerManager(this); this.dungeonManager = new DungeonManager(this); this.commandMap = new CommandMap(true); + this.taskMap = new TaskMap(true); // Schedule game loop. Timer gameLoop = new Timer(); @@ -114,6 +117,10 @@ public final class GameServer extends MihoyoKcpServer { public CommandMap getCommandMap() { return this.commandMap; } + + public TaskMap getTaskMap() { + return this.taskMap; + } public void registerPlayer(GenshinPlayer player) { getPlayers().put(player.getUid(), player); diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketCardProductRewardNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketCardProductRewardNotify.java new file mode 100644 index 000000000..ca5a5fc00 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/packet/send/PacketCardProductRewardNotify.java @@ -0,0 +1,24 @@ +package emu.grasscutter.server.packet.send; + + +import emu.grasscutter.net.packet.GenshinPacket; +import emu.grasscutter.net.packet.PacketOpcodes; +import emu.grasscutter.net.proto.CardProductRewardNotifyOuterClass.CardProductRewardNotify; + +public class PacketCardProductRewardNotify extends GenshinPacket { + + public PacketCardProductRewardNotify(int remainsDay) { + super(PacketOpcodes.CardProductRewardNotify); + + CardProductRewardNotify proto = CardProductRewardNotify.newBuilder() + .setProductId("ys_chn_blessofmoon_tier5") + .setHcoin(90) + .setRemainDays(remainsDay) + .build(); + + // Hard code Product id keep cool 😎 + + this.setData(proto); + } + +} diff --git a/src/main/java/emu/grasscutter/task/Task.java b/src/main/java/emu/grasscutter/task/Task.java new file mode 100644 index 000000000..34638a777 --- /dev/null +++ b/src/main/java/emu/grasscutter/task/Task.java @@ -0,0 +1,30 @@ +package emu.grasscutter.task; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + + +/* +* So what is cron expression? +The format of a Cron expression is as follows. +Second Minute Hour Day Month Week Year +Seconds: 0-59 +Minute: 0-59 +hour: 0-23 +Day: 1-31 +Month: 1-12 +Week: 1-7 (0-6 sometimes) +Year: Specify your own + +If you want to express every second or every minute or something like that, use the * symbol in that position; +if you want to express more than one such as every 15 minutes and every 30 minutes, you can write:`15, 30`. + +For the rest of the wildcard characters, please Google them yourself +*/ + +@Retention(RetentionPolicy.RUNTIME) +public @interface Task { + String taskName() default "NO_NAME"; + String taskCronExpression() default "0 0 0 0 0 ?"; + String triggerName() default "NO_NAME"; +} diff --git a/src/main/java/emu/grasscutter/task/TaskHandler.java b/src/main/java/emu/grasscutter/task/TaskHandler.java new file mode 100644 index 000000000..e1a160a07 --- /dev/null +++ b/src/main/java/emu/grasscutter/task/TaskHandler.java @@ -0,0 +1,11 @@ +package emu.grasscutter.task; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +public interface TaskHandler extends Job { + default void execute(JobExecutionContext context) throws JobExecutionException { + + } +} diff --git a/src/main/java/emu/grasscutter/task/TaskMap.java b/src/main/java/emu/grasscutter/task/TaskMap.java new file mode 100644 index 000000000..3da550a67 --- /dev/null +++ b/src/main/java/emu/grasscutter/task/TaskMap.java @@ -0,0 +1,94 @@ +package emu.grasscutter.task; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.Account; +import emu.grasscutter.game.GenshinPlayer; + +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.impl.StdSchedulerFactory; +import org.quartz.spi.MutableTrigger; +import org.reflections.Reflections; + +import java.util.*; + +@SuppressWarnings({"UnusedReturnValue", "unused"}) +public final class TaskMap { + private final Map tasks = new HashMap<>(); + private final Map annotations = new HashMap<>(); + private final SchedulerFactory schedulerFactory = new StdSchedulerFactory(); + + public TaskMap() { + this(false); + } + + public TaskMap(boolean scan) { + if (scan) this.scan(); + } + + public static TaskMap getInstance() { + return Grasscutter.getGameServer().getTaskMap(); + } + + public TaskMap registerTask(String taskName, TaskHandler task) { + Task annotation = task.getClass().getAnnotation(Task.class); + this.annotations.put(taskName, annotation); + this.tasks.put(taskName, task); + + // register task + try { + Scheduler scheduler = schedulerFactory.getScheduler(); + JobDetail job = JobBuilder + .newJob(task.getClass()) + .withIdentity(taskName) + .build(); + + Trigger convTrigger = TriggerBuilder.newTrigger() + .withIdentity(annotation.triggerName()) + .withSchedule(CronScheduleBuilder.cronSchedule(annotation.taskCronExpression())) + .build(); + + scheduler.scheduleJob(job, convTrigger); + scheduler.start(); + } catch (SchedulerException e) { + e.printStackTrace(); + } + + return this; + } + + public List getHandlersAsList() { + return new LinkedList<>(this.tasks.values()); + } + + public HashMap getHandlers() { + return new LinkedHashMap<>(this.tasks); + } + + public TaskHandler getHandler(String taskName) { + return this.tasks.get(taskName); + } + + private void scan() { + Reflections reflector = Grasscutter.reflector; + Set> classes = reflector.getTypesAnnotatedWith(Task.class); + classes.forEach(annotated -> { + try { + Task taskData = annotated.getAnnotation(Task.class); + Object object = annotated.newInstance(); + if (object instanceof TaskHandler) + this.registerTask(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); + } + }); + } +} diff --git a/src/main/java/emu/grasscutter/task/tasks/MoonCard.java b/src/main/java/emu/grasscutter/task/tasks/MoonCard.java new file mode 100644 index 000000000..26f7b9788 --- /dev/null +++ b/src/main/java/emu/grasscutter/task/tasks/MoonCard.java @@ -0,0 +1,27 @@ +package emu.grasscutter.task.tasks; + +import emu.grasscutter.database.DatabaseManager; +import emu.grasscutter.game.GenshinPlayer; +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 { + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + List players = DatabaseManager.getDatastore().find(GenshinPlayer.class).stream().toList(); + for (GenshinPlayer player : players) { + if (player.isOnline()) { + if (player.inMoonCard()) { + player.getTodayMoonCard(); + } + } + } + } +} diff --git a/src/main/java/emu/grasscutter/utils/DateHelper.java b/src/main/java/emu/grasscutter/utils/DateHelper.java new file mode 100644 index 000000000..7005d9457 --- /dev/null +++ b/src/main/java/emu/grasscutter/utils/DateHelper.java @@ -0,0 +1,16 @@ +package emu.grasscutter.utils; + +import java.util.Date; +import java.util.Calendar; + +public final class DateHelper { + public static Date onlyYearMonthDay(Date now) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(now); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar.getTime(); + } +}