mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-12-13 15:34:40 +01:00
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>
This commit is contained in:
@@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import emu.grasscutter.game.dungeons.DungeonManager;
|
|||||||
import emu.grasscutter.game.dungeons.challenge.DungeonChallenge;
|
import emu.grasscutter.game.dungeons.challenge.DungeonChallenge;
|
||||||
import emu.grasscutter.game.expedition.ExpeditionManager;
|
import emu.grasscutter.game.expedition.ExpeditionManager;
|
||||||
import emu.grasscutter.game.gacha.GachaManager;
|
import emu.grasscutter.game.gacha.GachaManager;
|
||||||
|
import emu.grasscutter.game.managers.AnnouncementManager;
|
||||||
import emu.grasscutter.game.managers.CookingManager;
|
import emu.grasscutter.game.managers.CookingManager;
|
||||||
import emu.grasscutter.game.managers.InventoryManager;
|
import emu.grasscutter.game.managers.InventoryManager;
|
||||||
import emu.grasscutter.game.managers.MultiplayerManager;
|
import emu.grasscutter.game.managers.MultiplayerManager;
|
||||||
@@ -69,6 +70,7 @@ public final class GameServer extends KcpServer {
|
|||||||
@Getter private final BattlePassMissionManager battlePassMissionManager;
|
@Getter private final BattlePassMissionManager battlePassMissionManager;
|
||||||
@Getter private final CombineManger combineManger;
|
@Getter private final CombineManger combineManger;
|
||||||
@Getter private final TowerScheduleManager towerScheduleManager;
|
@Getter private final TowerScheduleManager towerScheduleManager;
|
||||||
|
@Getter private final AnnouncementManager announcementManager;
|
||||||
|
|
||||||
public GameServer() {
|
public GameServer() {
|
||||||
this(getAdapterInetSocketAddress());
|
this(getAdapterInetSocketAddress());
|
||||||
@@ -112,7 +114,7 @@ public final class GameServer extends KcpServer {
|
|||||||
this.towerScheduleManager = new TowerScheduleManager(this);
|
this.towerScheduleManager = new TowerScheduleManager(this);
|
||||||
this.worldDataManager = new WorldDataManager(this);
|
this.worldDataManager = new WorldDataManager(this);
|
||||||
this.battlePassMissionManager = new BattlePassMissionManager(this);
|
this.battlePassMissionManager = new BattlePassMissionManager(this);
|
||||||
|
this.announcementManager = new AnnouncementManager(this);
|
||||||
// Hook into shutdown event.
|
// Hook into shutdown event.
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown));
|
Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/resources/defaults/data/Announcement.json
Normal file
22
src/main/resources/defaults/data/Announcement.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -117,6 +117,14 @@
|
|||||||
"no_account": "Account not found.",
|
"no_account": "Account not found.",
|
||||||
"description": "Modify user accounts"
|
"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": {
|
"clear": {
|
||||||
"command_usage": "Usage: clear <all|wp|art|mat>",
|
"command_usage": "Usage: clear <all|wp|art|mat>",
|
||||||
"weapons": "Cleared weapons for %s.",
|
"weapons": "Cleared weapons for %s.",
|
||||||
|
|||||||
@@ -117,6 +117,14 @@
|
|||||||
"no_account": "账号不存在。",
|
"no_account": "账号不存在。",
|
||||||
"description": "创建或删除账号"
|
"description": "创建或删除账号"
|
||||||
},
|
},
|
||||||
|
"announce": {
|
||||||
|
"command_usage": "用法:a <tpl templateId|refresh|revoke templateId|content>",
|
||||||
|
"send_success": "成功地发送了一则公告,你可以通过/a revoke %s来撤销。",
|
||||||
|
"refresh_success": "成功地刷新了公告配置。(共%s个)",
|
||||||
|
"revoke_done": "尝试撤回公告 %s。",
|
||||||
|
"description": "向所有在线玩家发送公告,或者管理服务器的公告。",
|
||||||
|
"not_found": "找不到公告 %s。"
|
||||||
|
},
|
||||||
"clear": {
|
"clear": {
|
||||||
"command_usage": "用法:clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料",
|
"command_usage": "用法:clear <all|wp|art|mat>\nall: 所有, wp: 武器, art: 圣遗物, mat: 材料",
|
||||||
"weapons": "已清除 %s 的武器。",
|
"weapons": "已清除 %s 的武器。",
|
||||||
|
|||||||
Reference in New Issue
Block a user