Archiviert
1
0

Merge pull request 'Omni Replay' (#236) from replaying into master

Reviewed-on: SteamWar/BungeeCore#236
Reviewed-by: YoyoNow <jwsteam@nidido.de>
Dieser Commit ist enthalten in:
Lixfel 2021-12-28 19:45:04 +01:00
Commit edbeabcac9
15 geänderte Dateien mit 362 neuen und 15 gelöschten Zeilen

Datei anzeigen

@ -128,6 +128,7 @@ public class BungeeCore extends Plugin {
new ListCommand(); new ListCommand();
new StatCommand(); new StatCommand();
new VerifyCommand(); new VerifyCommand();
new ReplayCommand();
new GDPRQuery(); new GDPRQuery();
// Punishment Commands: // Punishment Commands:
@ -180,7 +181,7 @@ public class BungeeCore extends Plugin {
} }
errorLogger.unregister(); errorLogger.unregister();
Statement.close(); Statement.closeAll();
} }
public static BungeeCore get() { public static BungeeCore get() {

Datei anzeigen

@ -28,7 +28,9 @@ import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.text.DateFormat;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@ -71,6 +73,8 @@ public class Message {
if(params[i] instanceof Message) { if(params[i] instanceof Message) {
Message msg = (Message) params[i]; Message msg = (Message) params[i];
params[i] = parse(msg.getMessage(), sender, msg.getParams()); params[i] = parse(msg.getMessage(), sender, msg.getParams());
} else if(params[i] instanceof Date) {
params[i] = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale).format((Date) params[i]);
} }
} }
return format.format(params); return format.format(params);

Datei anzeigen

@ -59,6 +59,7 @@ public class SubserverSystem {
* For a test arena: -1 * For a test arena: -1
* For an event arena: EventFightID of the fight * For an event arena: EventFightID of the fight
* *
* @param replayID
* @param serverName * @param serverName
* The name of the server (for event and test arenas) * The name of the server (for event and test arenas)
* or null (autogenerated arena name modus.getDisplayName() + number) * or null (autogenerated arena name modus.getDisplayName() + number)
@ -82,7 +83,7 @@ public class SubserverSystem {
* @return * @return
* The new started subserver. * The new started subserver.
*/ */
public static synchronized Subserver startArena(ArenaMode modus, String map, int eventFightID, int checkSchemID, int prepareSchemID, String serverName, String mapName, UUID player1, UUID player2, boolean ranked){ public static synchronized Subserver startArena(ArenaMode modus, String map, int eventFightID, int checkSchemID, int prepareSchemID, int replayID, String serverName, String mapName, UUID player1, UUID player2, boolean ranked){
//Generate missing parameters //Generate missing parameters
Node node = eventFightID > 0 ? Node.local : Node.getNode(); Node node = eventFightID > 0 ? Node.local : Node.getNode();
int port = arenaPorts.freePort(); int port = arenaPorts.freePort();
@ -113,6 +114,7 @@ public class SubserverSystem {
"logPath=" + mapName, "config=" + modus.getConfig(), "logPath=" + mapName, "config=" + modus.getConfig(),
"fightID=" + eventFightID, "ranked=" + ranked, "fightID=" + eventFightID, "ranked=" + ranked,
"checkSchemID=" + checkSchemID, "prepareSchemID=" + prepareSchemID, "checkSchemID=" + checkSchemID, "prepareSchemID=" + prepareSchemID,
"replay=" + replayID,
player1 != null && eventFightID != -1 ? "blueLeader=" + player1 : null, player1 != null && eventFightID != -1 ? "blueLeader=" + player1 : null,
player2 != null ? "redLeader=" + player2 : null player2 != null ? "redLeader=" + player2 : null
); );
@ -139,7 +141,7 @@ public class SubserverSystem {
eventFight.getFightID(), eventFight.getFightID(),
0, 0,
0, 0,
serverName, 0, serverName,
serverName + eventFight.getStartTime().toLocalDateTime().format(DateTimeFormatter.ISO_TIME), serverName + eventFight.getStartTime().toLocalDateTime().format(DateTimeFormatter.ISO_TIME),
null, null,
null, null,
@ -147,7 +149,7 @@ public class SubserverSystem {
} }
public static void startTestServer(ProxiedPlayer p, ArenaMode m, String map, int checkSchemId, int prepareSchemId){ public static void startTestServer(ProxiedPlayer p, ArenaMode m, String map, int checkSchemId, int prepareSchemId){
startArena(m, map, -1, checkSchemId, prepareSchemId, p.getName() + "s Bau", p.getName(), p.getUniqueId(), null, false).sendPlayer(p); startArena(m, map, -1, checkSchemId, prepareSchemId, 0, p.getName() + "s Bau", p.getName(), p.getUniqueId(), null, false).sendPlayer(p);
} }
private static synchronized void sendToBau(ProxiedPlayer p, UUID owner, String prototype, String worldFolder, String serverJar, String worldDir, String worldName, String xmx, String serverName){ private static synchronized void sendToBau(ProxiedPlayer p, UUID owner, String prototype, String worldFolder, String serverJar, String worldDir, String worldName, String xmx, String serverName){

Datei anzeigen

@ -76,7 +76,7 @@ public class ChallengeCommand extends BasicCommand {
challenges.remove(target); challenges.remove(target);
challenges.remove(player); challenges.remove(player);
Subserver arena = SubserverSystem.startArena(mode, map, 0, 0, 0, null, null, player.getUniqueId(), target.getUniqueId(), false); Subserver arena = SubserverSystem.startArena(mode, map, 0, 0, 0, 0, null, null, player.getUniqueId(), target.getUniqueId(), false);
arena.sendPlayer(player); arena.sendPlayer(player);
arena.sendPlayer(target); arena.sendPlayer(target);

Datei anzeigen

@ -164,7 +164,7 @@ public class FightCommand extends BasicCommand {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
createArena(sender, "/fight ", args, 0, false, (player, mode, map) -> { createArena(sender, "/fight ", args, 0, false, (player, mode, map) -> {
Subserver arena = SubserverSystem.startArena(mode, map, 0, 0, 0, null, null, player.getUniqueId(), null, false); Subserver arena = SubserverSystem.startArena(mode, map, 0, 0, 0, 0, null, null, player.getUniqueId(), null, false);
arena.sendPlayer(player); arena.sendPlayer(player);
Message.broadcast("FIGHT_BROADCAST", "FIGHT_BROADCAST_HOVER" Message.broadcast("FIGHT_BROADCAST", "FIGHT_BROADCAST_HOVER"
, new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/join " + player.getName()), mode.getDisplayName(), player.getName()); , new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/join " + player.getName()), mode.getDisplayName(), player.getName());

Datei anzeigen

@ -36,7 +36,7 @@ public class HistoricCommand extends BasicCommand {
@Override @Override
public void execute(CommandSender sender, String[] args) { public void execute(CommandSender sender, String[] args) {
FightCommand.createArena(sender, "/historic ", args, 0, true, (player, mode, map) -> { FightCommand.createArena(sender, "/historic ", args, 0, true, (player, mode, map) -> {
Subserver arena = SubserverSystem.startArena(mode, map, 0, 0, 0, null, null, player.getUniqueId(), null, false); Subserver arena = SubserverSystem.startArena(mode, map, 0, 0, 0, 0, null, null, player.getUniqueId(), null, false);
arena.sendPlayer(player); arena.sendPlayer(player);
Message.broadcast("HISTORIC_BROADCAST", "HISTORIC_BROADCAST_HOVER" Message.broadcast("HISTORIC_BROADCAST", "HISTORIC_BROADCAST_HOVER"
, new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/join " + player.getName()), mode.getDisplayName(), player.getName()); , new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/join " + player.getName()), mode.getDisplayName(), player.getName());

Datei anzeigen

@ -168,7 +168,7 @@ public class RankedCommand extends BasicCommand {
removeFromAll(wp1.player); removeFromAll(wp1.player);
removeFromAll(wp2.player); removeFromAll(wp2.player);
Subserver arena = SubserverSystem.startArena(mode, mode.getRandomMap(), 0, 0, 0, null, null, wp1.player.getUniqueId(), wp2.player.getUniqueId(), true); Subserver arena = SubserverSystem.startArena(mode, mode.getRandomMap(), 0, 0, 0, 0, null, null, wp1.player.getUniqueId(), wp2.player.getUniqueId(), true);
arena.sendPlayer(wp1.player); arena.sendPlayer(wp1.player);
arena.sendPlayer(wp2.player); arena.sendPlayer(wp2.player);

Datei anzeigen

@ -0,0 +1,64 @@
package de.steamwar.bungeecore.commands;
import de.steamwar.bungeecore.ArenaMode;
import de.steamwar.bungeecore.Message;
import de.steamwar.bungeecore.SubserverSystem;
import de.steamwar.bungeecore.inventory.SWItem;
import de.steamwar.bungeecore.inventory.SWListInv;
import de.steamwar.bungeecore.inventory.SWStreamInv;
import de.steamwar.bungeecore.sql.Fight;
import de.steamwar.bungeecore.sql.SchematicType;
import de.steamwar.bungeecore.sql.SteamwarUser;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ReplayCommand extends BasicCommand {
public ReplayCommand() {
super("replay", null);
}
@Override
public void execute(CommandSender sender, String[] args) {
if(!(sender instanceof ProxiedPlayer))
return;
ProxiedPlayer player = (ProxiedPlayer) sender;
new SWStreamInv<>(player, Message.parse("REPLAY_TITLE", player), (click, fight) -> {
if(!fight.isReplayAvailable()) {
Message.send("REPLAY_UNAVAILABLE", player);
return;
}
ArenaMode mode = fight.getGameMode();
SubserverSystem.startArena(mode, mode.getRandomMap(), 0, 0, 0, fight.getFightID(), null, null, null, null, false).sendPlayer(player);
}, page -> Fight.getPage(page, 45).stream().map(fight -> new SWListInv.SWListEntry<>(getFightItem(player, fight), fight)).collect(Collectors.toList())).open();
}
private SWItem getFightItem(ProxiedPlayer player, Fight fight) {
SchematicType type = fight.getSchemType();
SWItem item = new SWItem(type != null ? type.getMaterial() : "BARRIER", parseLeader(player, fight.getBlueLeader(), fight.getBluePlayers().size(), fight.getWin() == 1));
List<String> lore = new ArrayList<>();
lore.add(parseLeader(player, fight.getRedLeader(), fight.getRedPlayers().size(), fight.getWin() == 2));
lore.add(Message.parse("REPLAY_TIME", player, fight.getStartTime()));
lore.add("");
lore.add(Message.parse("REPLAY_SERVER", player, fight.getServer()));
if(!fight.isReplayAvailable())
lore.add(Message.parse("REPLAY_UNAVAILABLE", player));
item.setLore(lore);
if(fight.isReplayAvailable())
item.setEnchanted(true);
return item;
}
private String parseLeader(ProxiedPlayer player, SteamwarUser leader, int players, boolean winner) {
return Message.parse(winner ? (players > 1 ? "REPLAY_WINNER" : "REPLAY_SOLO_WINNER") : (players > 1 ? "REPLAY_LOSER" : "REPLAY_SOLO_LOSER"), player, leader.getUserName(), players - 1);
}
}

Datei anzeigen

@ -0,0 +1,51 @@
package de.steamwar.bungeecore.inventory;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import java.util.List;
import java.util.function.Function;
public class SWStreamInv<T> extends SWInventory {
private final SWListInv.ListCallback<T> callback;
private final Function<Integer, List<SWListInv.SWListEntry<T>>> constructor;
private int page;
public SWStreamInv(ProxiedPlayer proxiedPlayer, String title, SWListInv.ListCallback<T> callback, Function<Integer, List<SWListInv.SWListEntry<T>>> constructor) {
super(proxiedPlayer, 54, title);
this.callback = callback;
this.constructor = constructor;
page = 0;
}
@Override
public void open(){
List<SWListInv.SWListEntry<T>> entries = constructor.apply(page);
if(page != 0)
addItem(45, new SWItem("§eSeite zurück", 10), (InvCallback.ClickType click) -> {
page--;
open();
});
else
addItem(45, new SWItem("§7Seite zurück", 8), (InvCallback.ClickType click) -> {});
if(entries.size() == 45)
addItem(53, new SWItem("§eSeite vor", 10), (InvCallback.ClickType click) -> {
page++;
open();
});
else
addItem(53, new SWItem("§7Seite vor", 8), (InvCallback.ClickType click) -> {});
for(int i = 0; i < entries.size(); i++) {
SWListInv.SWListEntry<T> item = entries.get(i);
addItem(i, item.getItem());
setCallback(i, (InvCallback.ClickType click) -> {
close();
callback.clicked(click, item.getObject());
});
}
super.open();
}
}

Datei anzeigen

@ -0,0 +1,128 @@
package de.steamwar.bungeecore.sql;
import de.steamwar.bungeecore.ArenaMode;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class Fight {
private static final Statement getPage = new Statement("SELECT FightID, GameMode, Server, StartTime, Duration, BlueLeader, RedLeader, BlueSchem, RedSchem, Win, WinCondition, ReplayLock, Replay is not NULL AS ReplayAvailable FROM Fight ORDER BY FightID DESC LIMIT ?, ?");
private final int fightID;
private final String gameMode;
private final String server;
private final Timestamp startTime;
private final int duration;
private final int blueLeader;
private final int redLeader;
private final int blueSchem;
private final int redSchem;
private final int win;
private final String winCondition;
private final Timestamp replayLock;
private final boolean replayAvailable;
private final List<FightPlayer> bluePlayers = new ArrayList<>();
private final List<FightPlayer> redPlayers = new ArrayList<>();
private Fight(ResultSet rs) throws SQLException {
fightID = rs.getInt("FightID");
gameMode = rs.getString("GameMode");
server = rs.getString("Server");
startTime = rs.getTimestamp("StartTime");
duration = rs.getInt("Duration");
blueLeader = rs.getInt("BlueLeader");
redLeader = rs.getInt("RedLeader");
blueSchem = rs.getInt("BlueSchem");
redSchem = rs.getInt("RedSchem");
win = rs.getInt("Win");
winCondition = rs.getString("WinCondition");
replayLock = rs.getTimestamp("ReplayLock");
replayAvailable = rs.getBoolean("ReplayAvailable");
}
private void initPlayers(List<FightPlayer> fightPlayers) {
for(FightPlayer fp : fightPlayers) {
if(fp.getFightID() != fightID)
continue;
if(fp.getTeam() == 1)
bluePlayers.add(fp);
else
redPlayers.add(fp);
}
}
public static List<Fight> getPage(int page, int elementsPerPage) {
List<Fight> fights = getPage.select(rs -> {
List<Fight> f = new ArrayList<>();
while(rs.next()){
f.add(new Fight(rs));
}
return f;
}, page * elementsPerPage, elementsPerPage);
List<FightPlayer> fightPlayers = FightPlayer.batchGet(fights.stream().map(f -> f.fightID).collect(Collectors.toSet()));
for(Fight fight : fights) {
fight.initPlayers(fightPlayers);
}
SteamwarUser.batchCache(fightPlayers.stream().map(FightPlayer::getUserID).collect(Collectors.toSet()));
return fights;
}
public SchematicType getSchemType() {
return SchematicType.fromDB(gameMode);
}
public ArenaMode getGameMode() {
SchematicType schemType = getSchemType();
if(schemType == null)
return null;
return ArenaMode.getBySchemType(schemType);
}
public int getFightID() {
return fightID;
}
public Timestamp getStartTime() {
return startTime;
}
public SteamwarUser getBlueLeader() {
return SteamwarUser.get(blueLeader);
}
public SteamwarUser getRedLeader() {
return SteamwarUser.get(redLeader);
}
public int getWin() {
return win;
}
public String getServer() {
return server;
}
public List<FightPlayer> getBluePlayers() {
return bluePlayers;
}
public List<FightPlayer> getRedPlayers() {
return redPlayers;
}
public boolean isReplayAvailable() {
return replayAvailable && replayLock.before(Timestamp.from(Instant.now())) && getGameMode() != null;
}
}

Datei anzeigen

@ -0,0 +1,54 @@
package de.steamwar.bungeecore.sql;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class FightPlayer {
private final int fightID;
private final int userID;
private final int team;
private final String kit;
private final int kills;
private final boolean isOut;
private FightPlayer(ResultSet rs) throws SQLException {
fightID = rs.getInt("FightID");
userID = rs.getInt("UserID");
team = rs.getInt("Team");
kit = rs.getString("Kit");
kills = rs.getInt("Kills");
isOut = rs.getBoolean("IsOut");
}
public static List<FightPlayer> batchGet(Set<Integer> fightIds) {
List<FightPlayer> fightPlayers = new ArrayList<>();
if(fightIds.isEmpty())
return fightPlayers;
try (Statement batch = new Statement("SELECT * FROM FightPlayer WHERE FightID IN (" + fightIds.stream().map(Object::toString).collect(Collectors.joining(", ")) + ")")) {
batch.select(rs -> {
while(rs.next())
fightPlayers.add(new FightPlayer(rs));
return null;
});
}
return fightPlayers;
}
public int getFightID() {
return fightID;
}
public int getTeam() {
return team;
}
public int getUserID() {
return userID;
}
}

Datei anzeigen

@ -30,7 +30,7 @@ import java.io.IOException;
import java.util.*; import java.util.*;
public class SchematicType { public class SchematicType {
public static final SchematicType Normal = new SchematicType("Normal", "", Type.NORMAL, null); //Has to stay publicly availible public static final SchematicType Normal = new SchematicType("Normal", "", null, Type.NORMAL, null); //Has to stay publicly availible
private static final Map<String, SchematicType> fromDB; private static final Map<String, SchematicType> fromDB;
private static final Map<SchematicType, SchematicType> fightType; private static final Map<SchematicType, SchematicType> fightType;
@ -64,15 +64,17 @@ public class SchematicType {
if(tmpFromDB.containsKey(type.toLowerCase())) if(tmpFromDB.containsKey(type.toLowerCase()))
continue; continue;
String material = config.getString("Schematic.Material");
SchematicType checktype = null; SchematicType checktype = null;
if(!config.getStringList("CheckQuestions").isEmpty()) { if(!config.getStringList("CheckQuestions").isEmpty()) {
checktype = new SchematicType("C" + type, "C" + shortcut, Type.CHECK_TYPE, null); checktype = new SchematicType("C" + type, "C" + shortcut, material, Type.CHECK_TYPE, null);
tmpTypes.add(checktype); tmpTypes.add(checktype);
tmpFromDB.put(checktype.toDB(), checktype); tmpFromDB.put(checktype.toDB(), checktype);
CheckCommand.setCheckQuestions(checktype, config); CheckCommand.setCheckQuestions(checktype, config);
} }
SchematicType current = new SchematicType(type, shortcut, config.getKeys().contains("Server") ? Type.FIGHT_TYPE : Type.NORMAL, checktype); SchematicType current = new SchematicType(type, shortcut, material, config.getKeys().contains("Server") ? Type.FIGHT_TYPE : Type.NORMAL, checktype);
if(checktype != null) if(checktype != null)
tmpFightType.put(checktype, current); tmpFightType.put(checktype, current);
tmpFromDB.put(type.toLowerCase(), current); tmpFromDB.put(type.toLowerCase(), current);
@ -86,12 +88,14 @@ public class SchematicType {
private final String name; private final String name;
private final String kuerzel; private final String kuerzel;
private final String material;
private final Type type; private final Type type;
private final SchematicType checkType; private final SchematicType checkType;
private SchematicType(String name, String kuerzel, Type type, SchematicType checkType){ private SchematicType(String name, String kuerzel, String material, Type type, SchematicType checkType){
this.name = name; this.name = name;
this.kuerzel = kuerzel; this.kuerzel = kuerzel;
this.material = material != null && !"".equals(material) ? material : "STONE_BUTTON";
this.type = type; this.type = type;
this.checkType = checkType; this.checkType = checkType;
} }
@ -124,6 +128,10 @@ public class SchematicType {
return name; return name;
} }
public String getMaterial() {
return material;
}
public String getKuerzel() { public String getKuerzel() {
return kuerzel; return kuerzel;
} }

Datei anzeigen

@ -27,7 +27,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
public class Statement { public class Statement implements AutoCloseable {
private static final List<Statement> statements = new ArrayList<>(); private static final List<Statement> statements = new ArrayList<>();
private static Connection con; private static Connection con;
@ -48,7 +48,7 @@ public class Statement {
} }
private static void reset() { private static void reset() {
close(); closeAll();
connect(url, user, password); connect(url, user, password);
try { try {
for (Statement statement : statements) { for (Statement statement : statements) {
@ -59,7 +59,17 @@ public class Statement {
} }
} }
public static void close() { @Override
public void close() {
try {
st.close();
} catch (SQLException e) {
BungeeCore.get().getLogger().log(Level.INFO, "Could not close statement", e);
}
statements.remove(this);
}
public static void closeAll() {
synchronized (statements) { synchronized (statements) {
for (Statement statement : statements) { for (Statement statement : statements) {
try { try {

Datei anzeigen

@ -39,6 +39,7 @@ import java.sql.Timestamp;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
public class SteamwarUser { public class SteamwarUser {
@ -187,6 +188,20 @@ public class SteamwarUser {
}, id); }, id);
} }
public static void batchCache(Set<Integer> ids) {
ids.removeIf(usersById::containsKey);
if(ids.isEmpty())
return;
try (Statement batch = new Statement("SELECT * FROM UserData WHERE id IN (" + ids.stream().map(Object::toString).collect(Collectors.joining(", ")) + ")")) {
batch.select(rs -> {
while (rs.next())
new SteamwarUser(rs);
return null;
});
}
}
public static SteamwarUser get(Long discordId) { public static SteamwarUser get(Long discordId) {
if(usersByDiscord.containsKey(discordId)) if(usersByDiscord.containsKey(discordId))
return usersByDiscord.get(discordId); return usersByDiscord.get(discordId);

Datei anzeigen

@ -337,6 +337,16 @@ REGELN_WS=§eWarShip§8-§7Regelwerk
REGELN_WS_HOVER=§7https://steamwar.de/spielmodi/warship-regelwerk/ REGELN_WS_HOVER=§7https://steamwar.de/spielmodi/warship-regelwerk/
REGELN_WS_URL=https://steamwar.de/spielmodi/warship-regelwerk/ REGELN_WS_URL=https://steamwar.de/spielmodi/warship-regelwerk/
#ReplayCommand
REPLAY_TITLE=Letzte Kämpfe
REPLAY_UNAVAILABLE=§cReplay nicht möglich
REPLAY_SOLO_WINNER=§e§l{0}
REPLAY_WINNER=§e§l{0} §7+§e{1}
REPLAY_SOLO_LOSER=§e{0}
REPLAY_LOSER=§e{0} §7+§e{1}
REPLAY_TIME=§7{0}
REPLAY_SERVER=§7{0}
#ServerTeamchatCommand #ServerTeamchatCommand
STC_USAGE=§8/§7stc §8[§eNachricht an das Team§8] STC_USAGE=§8/§7stc §8[§eNachricht an das Team§8]
STC_FORMAT=§8STC §e{0}» §r{1} STC_FORMAT=§8STC §e{0}» §r{1}