Commit 9bafc2c5 authored by Akka's avatar Akka Committed by GitHub
Browse files

Implement server announcement (#1420)



* implement server announcement

* Update src/main/java/emu/grasscutter/command/commands/AnnounceCommand.java
Co-authored-by: default avatarLuke H-W <Birdulon@users.noreply.github.com>

* Added arg numbers check
Co-authored-by: default avatarLuke H-W <Birdulon@users.noreply.github.com>
parent a80302cd
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));
}
}
}
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; ...@@ -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 { ...@@ -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 { ...@@ -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));
} }
......
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);
}
}
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);
}
}
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));
}
}
[
{
"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
...@@ -117,6 +117,14 @@ ...@@ -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 @@ ...@@ -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 的武器。",
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment