From 9bafc2c5d5d5431a13e329ca1f2157508d1cf4c6 Mon Sep 17 00:00:00 2001
From: Akka <104902222+Akka0@users.noreply.github.com>
Date: Sat, 2 Jul 2022 21:43:22 +0800
Subject: [PATCH] Implement server announcement (#1420)

* implement server announcement

* Update src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java

Co-authored-by: Luke H-W <Birdulon@users.noreply.github.com>

* Added arg numbers check

Co-authored-by: Luke H-W <Birdulon@users.noreply.github.com>
---
 .../command/commands/AnnounceCommand.java     |  74 ++++++++++++
 .../game/managers/AnnouncementManager.java    | 112 ++++++++++++++++++
 .../grasscutter/server/game/GameServer.java   |   4 +-
 .../send/PacketServerAnnounceNotify.java      |  38 ++++++
 .../PacketServerAnnounceRevokeNotify.java     |  18 +++
 .../task/tasks/AnnouncementTask.java          |  52 ++++++++
 .../resources/defaults/data/Announcement.json |  22 ++++
 src/main/resources/languages/en-US.json       |   8 ++
 src/main/resources/languages/zh-CN.json       |   8 ++
 9 files changed, 335 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java
 create mode 100644 src/main/java/emu/grasscutter/game/managers/AnnouncementManager.java
 create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketServerAnnounceNotify.java
 create mode 100644 src/main/java/emu/grasscutter/server/packet/send/PacketServerAnnounceRevokeNotify.java
 create mode 100644 src/main/java/emu/grasscutter/task/tasks/AnnouncementTask.java
 create mode 100644 src/main/resources/defaults/data/Announcement.json

diff --git a/src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java b/src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java
new file mode 100644
index 00000000..c90422d2
--- /dev/null
+++ b/src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java
@@ -0,0 +1,74 @@
+package emu.grasscutter.command.commands;
+
+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 = "a <tpl templateId|refresh|revoke templateId|content>",
+    permission = "server.announce",
+    aliases = {"a"},
+    description = "commands.announce.description",
+    targetRequirement = Command.TargetRequirement.NONE)
+public final class AnnounceCommand implements CommandHandler {
+
+    @Override
+    public void execute(Player sender, Player targetPlayer, List<String> args) {
+        var manager = Grasscutter.getGameServer().getAnnouncementManager();
+        if (args.size() < 1) {
+            CommandHandler.sendTranslatedMessage(sender, "commands.announce.command_usage");
+            return;
+        }
+
+        switch (args.get(0)){
+            case "tpl":
+                if (args.size() < 2) {
+                    CommandHandler.sendTranslatedMessage(sender, "commands.announce.command_usage");
+                    return;
+                }
+
+                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));
+                    return;
+                }
+
+                manager.broadcast(Collections.singletonList(tpl));
+                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));
+                break;
+
+            case "revoke":
+                if (args.size() < 2) {
+                    CommandHandler.sendTranslatedMessage(sender, "commands.announce.command_usage");
+                    return;
+                }
+
+                var templateId1 = Integer.parseInt(args.get(1));
+                manager.revoke(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)));
+
+                CommandHandler.sendMessage(sender, translate(sender, "commands.announce.send_success", id));
+        }
+
+    }
+}
diff --git a/src/main/java/emu/grasscutter/game/managers/AnnouncementManager.java b/src/main/java/emu/grasscutter/game/managers/AnnouncementManager.java
new file mode 100644
index 00000000..163d0f5d
--- /dev/null
+++ b/src/main/java/emu/grasscutter/game/managers/AnnouncementManager.java
@@ -0,0 +1,112 @@
+package emu.grasscutter.game.managers;
+
+import com.google.gson.reflect.TypeToken;
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.data.DataLoader;
+import emu.grasscutter.game.player.Player;
+import emu.grasscutter.game.world.World;
+import emu.grasscutter.net.proto.AnnounceDataOuterClass;
+import emu.grasscutter.server.game.GameServer;
+import emu.grasscutter.server.packet.send.PacketServerAnnounceNotify;
+import emu.grasscutter.server.packet.send.PacketServerAnnounceRevokeNotify;
+import emu.grasscutter.utils.Utils;
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.Getter;
+import lombok.experimental.FieldDefaults;
+
+import java.io.InputStreamReader;
+import java.util.*;
+
+@Getter
+public class AnnouncementManager {
+
+    public final GameServer server;
+    public AnnouncementManager(GameServer server){
+        this.server = server;
+        loadConfig();
+    }
+    Map<Integer, AnnounceConfigItem> announceConfigItemMap = new HashMap<>();
+
+    private int loadConfig() {
+        try (var fileReader = new InputStreamReader(DataLoader.load("Announcement.json"))) {
+            List<AnnounceConfigItem> announceConfigItems = Grasscutter.getGsonFactory().fromJson(fileReader,
+                TypeToken.getParameterized(List.class, AnnounceConfigItem.class).getType());
+
+            announceConfigItemMap = new HashMap<>();
+            announceConfigItems.forEach(i -> announceConfigItemMap.put(i.getTemplateId(), i));
+
+
+        } catch (Exception e) {
+            Grasscutter.getLogger().error("Unable to load server announce config.", e);
+        }
+
+        return announceConfigItemMap.size();
+    }
+
+    public List<Player> getOnlinePlayers() {
+        return getServer().getWorlds().stream()
+            .map(World::getPlayers)
+            .flatMap(Collection::stream)
+            .toList();
+    }
+
+    public void broadcast(List<AnnounceConfigItem> tpl) {
+        if(tpl == null || tpl.size() == 0){
+            return;
+        }
+
+        var list = tpl.stream()
+            .map(AnnounceConfigItem::toProto)
+            .map(AnnounceDataOuterClass.AnnounceData.Builder::build)
+            .toList();
+
+        getOnlinePlayers().forEach(i -> i.sendPacket(new PacketServerAnnounceNotify(list)));
+    }
+
+    public int refresh() {
+        return loadConfig();
+    }
+
+    public void revoke(int tplId) {
+        getOnlinePlayers().forEach(i -> i.sendPacket(new PacketServerAnnounceRevokeNotify(tplId)));
+    }
+
+    @Data
+    @FieldDefaults(level = AccessLevel.PRIVATE)
+    public class AnnounceConfigItem{
+        int templateId;
+        AnnounceType type;
+        int frequency;
+        String content;
+        Date beginTime;
+        Date endTime;
+        boolean tick;
+        int interval;
+
+        public AnnounceDataOuterClass.AnnounceData.Builder toProto(){
+            var proto = AnnounceDataOuterClass.AnnounceData.newBuilder();
+
+            proto.setConfigId(templateId)
+                // I found the time here is useless
+                .setBeginTime(Utils.getCurrentSeconds() + 1)
+                .setEndTime(Utils.getCurrentSeconds() + 10);
+
+            if(type == AnnounceType.CENTER){
+                proto.setCenterSystemText(content)
+                    .setCenterSystemFrequency(frequency)
+                ;
+            }else{
+                proto.setCountDownText(content)
+                    .setCountDownFrequency(frequency)
+                ;
+            }
+
+            return proto;
+        }
+    }
+
+    public enum AnnounceType{
+        CENTER, COUNTDOWN
+    }
+}
diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java
index c7eae0a1..5b5325cb 100644
--- a/src/main/java/emu/grasscutter/server/game/GameServer.java
+++ b/src/main/java/emu/grasscutter/server/game/GameServer.java
@@ -12,6 +12,7 @@ import emu.grasscutter.game.dungeons.DungeonManager;
 import emu.grasscutter.game.dungeons.challenge.DungeonChallenge;
 import emu.grasscutter.game.expedition.ExpeditionManager;
 import emu.grasscutter.game.gacha.GachaManager;
+import emu.grasscutter.game.managers.AnnouncementManager;
 import emu.grasscutter.game.managers.CookingManager;
 import emu.grasscutter.game.managers.InventoryManager;
 import emu.grasscutter.game.managers.MultiplayerManager;
@@ -69,6 +70,7 @@ public final class GameServer extends KcpServer {
 	@Getter private final BattlePassMissionManager battlePassMissionManager;
 	@Getter private final CombineManger combineManger;
 	@Getter private final TowerScheduleManager towerScheduleManager;
+	@Getter private final AnnouncementManager announcementManager;
 
 	public GameServer() {
 		this(getAdapterInetSocketAddress());
@@ -112,7 +114,7 @@ public final class GameServer extends KcpServer {
 		this.towerScheduleManager = new TowerScheduleManager(this);
 		this.worldDataManager = new WorldDataManager(this);
 		this.battlePassMissionManager = new BattlePassMissionManager(this);
-		
+		this.announcementManager = new AnnouncementManager(this);
 		// Hook into shutdown event.
 		Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown));
 	}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketServerAnnounceNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketServerAnnounceNotify.java
new file mode 100644
index 00000000..9d00efa8
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketServerAnnounceNotify.java
@@ -0,0 +1,38 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.AnnounceDataOuterClass;
+import emu.grasscutter.net.proto.ServerAnnounceNotifyOuterClass;
+import emu.grasscutter.utils.Utils;
+
+import java.util.List;
+
+public class PacketServerAnnounceNotify extends BasePacket {
+
+    public PacketServerAnnounceNotify(List<AnnounceDataOuterClass.AnnounceData> data) {
+        super(PacketOpcodes.ServerAnnounceNotify);
+
+        var proto = ServerAnnounceNotifyOuterClass.ServerAnnounceNotify.newBuilder();
+
+        proto.addAllAnnounceDataList(data);
+
+        this.setData(proto);
+    }
+
+	public PacketServerAnnounceNotify(String msg, int configId) {
+		super(PacketOpcodes.ServerAnnounceNotify);
+
+        var proto = ServerAnnounceNotifyOuterClass.ServerAnnounceNotify.newBuilder();
+
+        proto.addAnnounceDataList(AnnounceDataOuterClass.AnnounceData.newBuilder()
+            .setConfigId(configId)
+            .setBeginTime(Utils.getCurrentSeconds() + 1)
+            .setEndTime(Utils.getCurrentSeconds() + 2)
+            .setCenterSystemText(msg)
+            .setCenterSystemFrequency(1)
+            .build());
+
+        this.setData(proto);
+	}
+}
diff --git a/src/main/java/emu/grasscutter/server/packet/send/PacketServerAnnounceRevokeNotify.java b/src/main/java/emu/grasscutter/server/packet/send/PacketServerAnnounceRevokeNotify.java
new file mode 100644
index 00000000..c3303a81
--- /dev/null
+++ b/src/main/java/emu/grasscutter/server/packet/send/PacketServerAnnounceRevokeNotify.java
@@ -0,0 +1,18 @@
+package emu.grasscutter.server.packet.send;
+
+import emu.grasscutter.net.packet.BasePacket;
+import emu.grasscutter.net.packet.PacketOpcodes;
+import emu.grasscutter.net.proto.ServerAnnounceRevokeNotifyOuterClass;
+
+public class PacketServerAnnounceRevokeNotify extends BasePacket {
+
+	public PacketServerAnnounceRevokeNotify(int tplId) {
+		super(PacketOpcodes.ServerAnnounceRevokeNotify);
+
+        var proto = ServerAnnounceRevokeNotifyOuterClass.ServerAnnounceRevokeNotify.newBuilder();
+
+        proto.addConfigIdList(tplId);
+
+        this.setData(proto);
+	}
+}
diff --git a/src/main/java/emu/grasscutter/task/tasks/AnnouncementTask.java b/src/main/java/emu/grasscutter/task/tasks/AnnouncementTask.java
new file mode 100644
index 00000000..9f75d819
--- /dev/null
+++ b/src/main/java/emu/grasscutter/task/tasks/AnnouncementTask.java
@@ -0,0 +1,52 @@
+package emu.grasscutter.task.tasks;
+
+import emu.grasscutter.Grasscutter;
+import emu.grasscutter.game.managers.AnnouncementManager;
+import emu.grasscutter.task.Task;
+import emu.grasscutter.task.TaskHandler;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+@Task(taskName = "Announcement", taskCronExpression = "0 * * * * ?", triggerName = "AnnouncementTrigger")
+public final class AnnouncementTask extends TaskHandler {
+
+    Map<Integer, Integer> intervalMap = new ConcurrentHashMap<>();
+    @Override
+    public void onEnable() {
+        Grasscutter.getLogger().debug("[Task] Announcement task enabled.");
+    }
+
+    @Override
+    public void onDisable() {
+        Grasscutter.getLogger().debug("[Task] Announcement task disabled.");
+    }
+
+    @Override
+    public synchronized void execute(JobExecutionContext context) throws JobExecutionException {
+        var current = new Date();
+        var announceConfigItems = Grasscutter.getGameServer().getAnnouncementManager().getAnnounceConfigItemMap().values().stream()
+            .filter(AnnouncementManager.AnnounceConfigItem::isTick)
+            .filter(i -> current.after(i.getBeginTime()))
+            .filter(i -> current.before(i.getEndTime()))
+            .collect(Collectors.toMap(AnnouncementManager.AnnounceConfigItem::getTemplateId, y -> y));
+
+        announceConfigItems.values().forEach(i -> intervalMap.compute(i.getTemplateId(), (k,v) -> v == null ? 1 : v + 1));
+
+        var toSend = intervalMap.entrySet().stream()
+            .filter(i -> announceConfigItems.containsKey(i.getKey()))
+            .filter(i -> announceConfigItems.get(i.getKey()).getInterval() >= i.getValue())
+            .map(i -> announceConfigItems.get(i.getKey()))
+            .toList();
+
+        Grasscutter.getGameServer().getAnnouncementManager().broadcast(toSend);
+        Grasscutter.getLogger().debug("Broadcast {} announcement(s) to all online players", toSend.size());
+
+        // clear the interval count
+        toSend.forEach(i -> intervalMap.put(i.getTemplateId(), 0));
+    }
+}
diff --git a/src/main/resources/defaults/data/Announcement.json b/src/main/resources/defaults/data/Announcement.json
new file mode 100644
index 00000000..18fbfa4a
--- /dev/null
+++ b/src/main/resources/defaults/data/Announcement.json
@@ -0,0 +1,22 @@
+[
+  {
+    "templateId" : 1,
+    "type" : "CENTER",
+    "frequency" : 1,
+    "content": "Welcome to grasscutter PS!",
+    "beginTime": "2022-06-01T00:00:00+08:00",
+    "endTime": "2022-06-01T00:08:00+08:00",
+    "tick" : false,
+    "interval": 1
+  },
+  {
+    "templateId" : 2,
+    "type" : "COUNTDOWN",
+    "frequency" : 1,
+    "content": "Welcome to grasscutter PS!",
+    "beginTime": "2022-06-01T00:00:00+08:00",
+    "endTime": "2022-06-01T00:08:00+08:00",
+    "tick" : false,
+    "interval": 1
+  }
+]
\ No newline at end of file
diff --git a/src/main/resources/languages/en-US.json b/src/main/resources/languages/en-US.json
index 029cac71..2df91648 100644
--- a/src/main/resources/languages/en-US.json
+++ b/src/main/resources/languages/en-US.json
@@ -117,6 +117,14 @@
       "no_account": "Account not found.",
       "description": "Modify user accounts"
     },
+    "announce": {
+      "command_usage": "Usage: a <tpl templateId|refresh|revoke templateId|content>",
+      "send_success": "Send an announcement successfully, you can revoke it by /a revoke %s.",
+      "refresh_success": "Refresh announcement config file successfully. (Total %s)",
+      "revoke_done": "Try to revoke announcement %s.",
+      "description": "Send announcement to all online players, or manage server's announcement.",
+      "not_found": "Could not found announcement %s."
+    },
     "clear": {
       "command_usage": "Usage: clear <all|wp|art|mat>",
       "weapons": "Cleared weapons for %s.",
diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json
index f2693641..c50f59b7 100644
--- a/src/main/resources/languages/zh-CN.json
+++ b/src/main/resources/languages/zh-CN.json
@@ -117,6 +117,14 @@
       "no_account": "璐﹀彿涓嶅瓨鍦ㄣ€�",
       "description": "鍒涘缓鎴栧垹闄よ处鍙�"
     },
+    "announce": {
+      "command_usage": "鐢ㄦ硶锛歛 <tpl templateId|refresh|revoke templateId|content>",
+      "send_success": "鎴愬姛鍦板彂閫佷簡涓€鍒欏叕鍛婏紝浣犲彲浠ラ€氳繃/a revoke %s鏉ユ挙閿€銆�",
+      "refresh_success": "鎴愬姛鍦板埛鏂颁簡鍏憡閰嶇疆銆�(鍏�%s涓�)",
+      "revoke_done": "灏濊瘯鎾ゅ洖鍏憡 %s銆�",
+      "description": "鍚戞墍鏈夊湪绾跨帺瀹跺彂閫佸叕鍛婏紝鎴栬€呯鐞嗘湇鍔″櫒鐨勫叕鍛娿€�",
+      "not_found": "鎵句笉鍒板叕鍛� %s銆�"
+    },
     "clear": {
       "command_usage": "鐢ㄦ硶锛歝lear <all|wp|art|mat>\nall: 鎵€鏈�, wp: 姝﹀櫒, art: 鍦i仐鐗�, mat: 鏉愭枡",
       "weapons": "宸叉竻闄� %s 鐨勬鍣ㄣ€�",
-- 
GitLab