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 * Added arg numbers check Co-authored-by: Luke H-W --- .../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 000000000..c90422d24 --- /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 ", + 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 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 000000000..163d0f5d7 --- /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 announceConfigItemMap = new HashMap<>(); + + private int loadConfig() { + try (var fileReader = new InputStreamReader(DataLoader.load("Announcement.json"))) { + List 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 getOnlinePlayers() { + return getServer().getWorlds().stream() + .map(World::getPlayers) + .flatMap(Collection::stream) + .toList(); + } + + public void broadcast(List 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 c7eae0a12..5b5325cbf 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 000000000..9d00efa8a --- /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 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 000000000..c3303a81f --- /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 000000000..9f75d819e --- /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 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 000000000..18fbfa4a9 --- /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 029cac71e..2df916487 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 ", + "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 ", "weapons": "Cleared weapons for %s.", diff --git a/src/main/resources/languages/zh-CN.json b/src/main/resources/languages/zh-CN.json index f2693641f..c50f59b73 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": "用法:a ", + "send_success": "成功地发送了一则公告,你可以通过/a revoke %s来撤销。", + "refresh_success": "成功地刷新了公告配置。(共%s个)", + "revoke_done": "尝试撤回公告 %s。", + "description": "向所有在线玩家发送公告,或者管理服务器的公告。", + "not_found": "找不到公告 %s。" + }, "clear": { "command_usage": "用法:clear \nall: 所有, wp: 武器, art: 圣遗物, mat: 材料", "weapons": "已清除 %s 的武器。",