Refactor GeyserSession tracking for better concurrency

This commit is contained in:
Camotoy 2021-11-12 09:02:14 -05:00
parent 62cded2daf
commit 09e3793fb2
No known key found for this signature in database
GPG Key ID: 7EEFB66FE798081F
18 changed files with 169 additions and 112 deletions

View File

@ -49,6 +49,8 @@ import org.geysermc.connector.utils.Direction;
import org.geysermc.platform.spigot.world.manager.GeyserSpigotWorldManager;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class GeyserPistonListener implements Listener {
private final GeyserConnector connector;
@ -86,11 +88,12 @@ public class GeyserPistonListener implements Listener {
Object2IntMap<Vector3i> attachedBlocks = new Object2IntOpenHashMap<>();
boolean blocksFilled = false;
for (GeyserSession session : connector.getPlayers()) {
Player player = Bukkit.getPlayer(session.getPlayerEntity().getUuid());
for (Map.Entry<UUID, GeyserSession> entry : connector.getSessionManager().getSessions().entrySet()) {
Player player = Bukkit.getPlayer(entry.getKey());
if (player == null || !player.getWorld().equals(world)) {
continue;
}
GeyserSession session = entry.getValue();
int dX = Math.abs(location.getBlockX() - player.getLocation().getBlockX()) >> 4;
int dZ = Math.abs(location.getBlockZ() - player.getLocation().getBlockZ()) >> 4;

View File

@ -80,7 +80,7 @@ public class GeyserSpigot1_11CraftingListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
GeyserSession session = null;
for (GeyserSession otherSession : connector.getPlayers()) {
for (GeyserSession otherSession : connector.getSessionManager().getSessions().values()) {
if (otherSession.getName().equals(event.getPlayer().getName())) {
session = otherSession;
break;

View File

@ -29,7 +29,6 @@ import com.nukkitx.math.vector.Vector3f;
import com.nukkitx.protocol.bedrock.data.SoundEvent;
import com.nukkitx.protocol.bedrock.packet.LevelSoundEventPacket;
import lombok.AllArgsConstructor;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockPlaceEvent;
@ -41,32 +40,30 @@ import org.geysermc.platform.spigot.world.manager.GeyserSpigotWorldManager;
@AllArgsConstructor
public class GeyserSpigotBlockPlaceListener implements Listener {
private final GeyserConnector connector;
private final GeyserSpigotWorldManager worldManager;
@EventHandler
public void place(final BlockPlaceEvent event) {
for (GeyserSession session : connector.getPlayers()) {
if (event.getPlayer() == Bukkit.getPlayer(session.getPlayerEntity().getUsername())) {
LevelSoundEventPacket placeBlockSoundPacket = new LevelSoundEventPacket();
placeBlockSoundPacket.setSound(SoundEvent.PLACE);
placeBlockSoundPacket.setPosition(Vector3f.from(event.getBlockPlaced().getX(), event.getBlockPlaced().getY(), event.getBlockPlaced().getZ()));
placeBlockSoundPacket.setBabySound(false);
if (worldManager.isLegacy()) {
placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(worldManager.getBlockAt(session,
event.getBlockPlaced().getX(), event.getBlockPlaced().getY(), event.getBlockPlaced().getZ())));
} else {
String javaBlockId = event.getBlockPlaced().getBlockData().getAsString();
placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(BlockRegistries.JAVA_IDENTIFIERS.get().getOrDefault(javaBlockId, BlockStateValues.JAVA_AIR_ID)));
}
placeBlockSoundPacket.setIdentifier(":");
session.sendUpstreamPacket(placeBlockSoundPacket);
session.setLastBlockPlacePosition(null);
session.setLastBlockPlacedId(null);
break;
}
GeyserSession session = connector.getPlayerByUuid(event.getPlayer().getUniqueId());
if (session == null) {
return;
}
}
LevelSoundEventPacket placeBlockSoundPacket = new LevelSoundEventPacket();
placeBlockSoundPacket.setSound(SoundEvent.PLACE);
placeBlockSoundPacket.setPosition(Vector3f.from(event.getBlockPlaced().getX(), event.getBlockPlaced().getY(), event.getBlockPlaced().getZ()));
placeBlockSoundPacket.setBabySound(false);
if (worldManager.isLegacy()) {
placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(worldManager.getBlockAt(session,
event.getBlockPlaced().getX(), event.getBlockPlaced().getY(), event.getBlockPlaced().getZ())));
} else {
String javaBlockId = event.getBlockPlaced().getBlockData().getAsString();
placeBlockSoundPacket.setExtraData(session.getBlockMappings().getBedrockBlockId(BlockRegistries.JAVA_IDENTIFIERS.get().getOrDefault(javaBlockId, BlockStateValues.JAVA_AIR_ID)));
}
placeBlockSoundPacket.setIdentifier(":");
session.sendUpstreamPacket(placeBlockSoundPacket);
session.setLastBlockPlacePosition(null);
session.setLastBlockPlacedId(null);
}
}

View File

@ -306,7 +306,7 @@ public class GeyserStandaloneGUI {
// Update player table
playerTableModel.getDataVector().removeAllElements();
for (GeyserSession player : GeyserConnector.getInstance().getPlayers()) {
for (GeyserSession player : GeyserConnector.getInstance().getSessionManager().getSessions().values()) {
Vector<String> row = new Vector<>();
row.add(player.getSocketAddress().getHostName());
row.add(player.getPlayerEntity().getUsername());

View File

@ -52,7 +52,6 @@ import org.geysermc.connector.registry.Registries;
import org.geysermc.connector.network.translators.PacketTranslatorRegistry;
import org.geysermc.connector.network.translators.item.ItemTranslator;
import org.geysermc.connector.network.translators.world.WorldManager;
import org.geysermc.connector.network.translators.world.block.entity.SkullBlockEntityTranslator;
import org.geysermc.connector.scoreboard.ScoreboardUpdater;
import org.geysermc.connector.skin.FloodgateSkinUploader;
import org.geysermc.connector.utils.*;
@ -72,10 +71,7 @@ import java.net.UnknownHostException;
import java.security.Key;
import java.text.DecimalFormat;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -100,7 +96,7 @@ public class GeyserConnector {
private static final String IP_REGEX = "\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b";
private final List<GeyserSession> players = new ArrayList<>();
private final SessionManager sessionManager = new SessionManager();
private static GeyserConnector instance;
@ -115,7 +111,7 @@ public class GeyserConnector {
private FloodgateSkinUploader skinUploader;
private final NewsHandler newsHandler;
private boolean shuttingDown = false;
private volatile boolean shuttingDown = false;
private final ScheduledExecutorService generalThreadPool;
@ -123,7 +119,7 @@ public class GeyserConnector {
private final PlatformType platformType;
private final GeyserBootstrap bootstrap;
private Metrics metrics;
private final Metrics metrics;
private GeyserConnector(PlatformType platformType, GeyserBootstrap bootstrap) {
long startupTime = System.currentTimeMillis();
@ -277,7 +273,7 @@ public class GeyserConnector {
if (config.getMetrics().isEnabled()) {
metrics = new Metrics(this, "GeyserMC", config.getMetrics().getUniqueId(), false, java.util.logging.Logger.getLogger(""));
metrics.addCustomChart(new Metrics.SingleLineChart("players", players::size));
metrics.addCustomChart(new Metrics.SingleLineChart("players", sessionManager::size));
// Prevent unwanted words best we can
metrics.addCustomChart(new Metrics.SimplePie("authMode", () -> config.getRemote().getAuthType().toString().toLowerCase()));
metrics.addCustomChart(new Metrics.SimplePie("platform", platformType::getPlatformName));
@ -285,7 +281,7 @@ public class GeyserConnector {
metrics.addCustomChart(new Metrics.SimplePie("version", () -> GeyserConnector.VERSION));
metrics.addCustomChart(new Metrics.AdvancedPie("playerPlatform", () -> {
Map<String, Integer> valueMap = new HashMap<>();
for (GeyserSession session : players) {
for (GeyserSession session : sessionManager.getAllSessions()) {
if (session == null) continue;
if (session.getClientData() == null) continue;
String os = session.getClientData().getDeviceOs().toString();
@ -299,7 +295,7 @@ public class GeyserConnector {
}));
metrics.addCustomChart(new Metrics.AdvancedPie("playerVersion", () -> {
Map<String, Integer> valueMap = new HashMap<>();
for (GeyserSession session : players) {
for (GeyserSession session : sessionManager.getAllSessions()) {
if (session == null) continue;
if (session.getClientData() == null) continue;
String version = session.getClientData().getGameVersion();
@ -359,6 +355,8 @@ public class GeyserConnector {
map.put(release, entry);
return map;
}));
} else {
metrics = null;
}
boolean isGui = false;
@ -392,40 +390,10 @@ public class GeyserConnector {
bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown"));
shuttingDown = true;
if (players.size() >= 1) {
bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.kick.log", players.size()));
// Make a copy to prevent ConcurrentModificationException
final List<GeyserSession> tmpPlayers = new ArrayList<>(players);
for (GeyserSession playerSession : tmpPlayers) {
playerSession.disconnect(LanguageUtils.getPlayerLocaleString("geyser.core.shutdown.kick.message", playerSession.getLocale()));
}
CompletableFuture<Void> future = CompletableFuture.runAsync(new Runnable() {
@Override
public void run() {
// Simulate a long-running Job
try {
while (true) {
if (players.size() == 0) {
return;
}
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
});
// Block and wait for the future to complete
try {
future.get();
bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.kick.done"));
} catch (Exception e) {
// Quietly fail
}
if (sessionManager.size() >= 1) {
bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.kick.log", sessionManager.size()));
sessionManager.disconnectAll("geyser.core.shutdown.kick.message");
bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.kick.done"));
}
generalThreadPool.shutdown();
@ -437,20 +405,11 @@ public class GeyserConnector {
skinUploader.close();
}
newsHandler.shutdown();
players.clear();
this.getCommandManager().getCommands().clear();
bootstrap.getGeyserLogger().info(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.done"));
}
public void addPlayer(GeyserSession player) {
players.add(player);
}
public void removePlayer(GeyserSession player) {
players.remove(player);
}
/**
* Gets a player by their current UUID
*
@ -463,13 +422,7 @@ public class GeyserConnector {
return null;
}
for (GeyserSession session : players) {
if (uuid.equals(session.getPlayerEntity().getUuid())) {
return session;
}
}
return null;
return sessionManager.getSessions().get(uuid);
}
/**
@ -480,8 +433,13 @@ public class GeyserConnector {
*/
@SuppressWarnings("unused") // API usage
public GeyserSession getPlayerByXuid(String xuid) {
for (GeyserSession session : players) {
if (session.getAuthData() != null && session.getAuthData().getXboxUUID().equals(xuid)) {
for (GeyserSession session : sessionManager.getPendingSessions()) {
if (session.getAuthData().getXboxUUID().equals(xuid)) {
return session;
}
}
for (GeyserSession session : sessionManager.getSessions().values()) {
if (session.getAuthData().getXboxUUID().equals(xuid)) {
return session;
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.connector;
import com.google.common.collect.ImmutableList;
import lombok.AccessLevel;
import lombok.Getter;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public final class SessionManager {
/**
* A list of all players who don't currently have a permanent UUID attached yet.
*/
@Getter(AccessLevel.PACKAGE)
private final Set<GeyserSession> pendingSessions = ConcurrentHashMap.newKeySet();
/**
* A list of all players who are currently in-game.
*/
@Getter
private final Map<UUID, GeyserSession> sessions = new ConcurrentHashMap<>();
/**
* Called once the player has successfully authenticated to the Geyser server.
*/
public void addPendingSession(GeyserSession session) {
pendingSessions.add(session);
}
/**
* Called once a player has successfully logged into their Java server.
*/
public void addSession(UUID uuid, GeyserSession session) {
pendingSessions.remove(session);
sessions.put(uuid, session);
}
public void removeSession(GeyserSession session) {
if (sessions.remove(session.getPlayerEntity().getUuid()) == null) {
// Session was likely pending
pendingSessions.remove(session);
}
}
/**
* Creates a new, immutable list containing all pending and active sessions.
*/
public Collection<GeyserSession> getAllSessions() {
return ImmutableList.<GeyserSession>builder() // builderWithExpectedSize is probably not a good idea yet as older Spigot builds probably won't have it.
.addAll(pendingSessions)
.addAll(sessions.values())
.build();
}
public void disconnectAll(String message) {
Collection<GeyserSession> sessions = getAllSessions();
for (GeyserSession session : sessions) {
session.disconnect(LanguageUtils.getPlayerLocaleString(message, session.getLocale()));
}
}
/**
* @return the total amount of sessions, including those pending.
*/
public int size() {
return pendingSessions.size() + sessions.size();
}
}

View File

@ -53,7 +53,7 @@ public class CommandExecutor {
return null;
}
for (GeyserSession session : connector.getPlayers()) {
for (GeyserSession session : connector.getSessionManager().getSessions().values()) {
if (sender.getName().equals(session.getPlayerEntity().getUsername())) {
return session;
}

View File

@ -46,8 +46,8 @@ public class ListCommand extends GeyserCommand {
@Override
public void execute(GeyserSession session, CommandSender sender, String[] args) {
String message = LanguageUtils.getPlayerLocaleString("geyser.commands.list.message", sender.getLocale(),
connector.getPlayers().size(),
connector.getPlayers().stream().map(GeyserSession::getName).collect(Collectors.joining(" ")));
connector.getSessionManager().size(),
connector.getSessionManager().getAllSessions().stream().map(GeyserSession::getName).collect(Collectors.joining(" ")));
sender.sendMessage(message);
}

View File

@ -32,8 +32,6 @@ import org.geysermc.connector.command.GeyserCommand;
import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.ArrayList;
public class ReloadCommand extends GeyserCommand {
private final GeyserConnector connector;
@ -53,9 +51,7 @@ public class ReloadCommand extends GeyserCommand {
sender.sendMessage(message);
for (GeyserSession otherSession : new ArrayList<>(connector.getPlayers())) {
otherSession.disconnect(LanguageUtils.getPlayerLocaleString("geyser.commands.reload.kick", otherSession.getLocale()));
}
connector.getSessionManager().disconnectAll("geyser.commands.reload.kick");
connector.reload();
}
}

View File

@ -114,7 +114,7 @@ public class DumpInfo {
}
this.userPlatforms = new Object2IntOpenHashMap<>();
for (GeyserSession session : GeyserConnector.getInstance().getPlayers()) {
for (GeyserSession session : GeyserConnector.getInstance().getSessionManager().getAllSessions()) {
DeviceOs device = session.getClientData().getDeviceOs();
userPlatforms.put(device, userPlatforms.getOrDefault(device, 0) + 1);
}

View File

@ -124,7 +124,7 @@ public class ConnectorServerEventHandler implements BedrockServerEventHandler {
pong.setPlayerCount(pingInfo.getPlayers().getOnline());
pong.setMaximumPlayerCount(pingInfo.getPlayers().getMax());
} else {
pong.setPlayerCount(connector.getPlayers().size());
pong.setPlayerCount(connector.getSessionManager().getSessions().size());
pong.setMaximumPlayerCount(config.getMaxPlayers());
}

View File

@ -159,7 +159,7 @@ public class QueryPacketHandler {
currentPlayerCount = String.valueOf(pingInfo.getPlayers().getOnline());
maxPlayerCount = String.valueOf(pingInfo.getPlayers().getMax());
} else {
currentPlayerCount = String.valueOf(connector.getPlayers().size());
currentPlayerCount = String.valueOf(connector.getSessionManager().getSessions().size());
maxPlayerCount = String.valueOf(connector.getConfig().getMaxPlayers());
}

View File

@ -59,6 +59,12 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
@Override
public boolean handle(LoginPacket loginPacket) {
if (connector.isShuttingDown()) {
// Don't allow new players in if we're no longer operating
session.disconnect(LanguageUtils.getLocaleStringLog("geyser.core.shutdown.kick.message"));
return true;
}
BedrockPacketCodec packetCodec = BedrockProtocol.getBedrockCodec(loginPacket.getProtocolVersion());
if (packetCodec == null) {
String supportedVersions = BedrockProtocol.getAllSupportedVersions();
@ -86,6 +92,8 @@ public class UpstreamPacketHandler extends LoggingPacketHandler {
playStatus.setStatus(PlayStatusPacket.Status.LOGIN_SUCCESS);
session.sendUpstreamPacket(playStatus);
connector.getSessionManager().addPendingSession(session);
ResourcePacksInfoPacket resourcePacksInfo = new ResourcePacksInfoPacket();
for(ResourcePack resourcePack : ResourcePack.PACKS.values()) {
ResourcePackManifest.Header header = resourcePack.getManifest().getHeader();

View File

@ -486,9 +486,7 @@ public class GeyserSession implements CommandSender {
if (connector.getConfig().getEmoteOffhandWorkaround() != EmoteOffhandWorkaroundOption.NO_EMOTES) {
this.emotes = new HashSet<>();
// Make a copy to prevent ConcurrentModificationException
final List<GeyserSession> tmpPlayers = new ArrayList<>(connector.getPlayers());
tmpPlayers.forEach(player -> this.emotes.addAll(player.getEmotes()));
connector.getSessionManager().getSessions().values().forEach(player -> this.emotes.addAll(player.getEmotes()));
} else {
this.emotes = null;
}
@ -498,7 +496,7 @@ public class GeyserSession implements CommandSender {
connector.getLogger().info(LanguageUtils.getLocaleStringLog("geyser.network.disconnect", address, disconnectReason));
disconnect(disconnectReason.name());
connector.removePlayer(this);
connector.getSessionManager().removeSession(this);
});
}
@ -911,7 +909,6 @@ public class GeyserSession implements CommandSender {
if (!internalConnect) {
downstream.connect();
}
connector.addPlayer(this);
}
public void disconnect(String reason) {
@ -921,7 +918,7 @@ public class GeyserSession implements CommandSender {
downstream.disconnect(reason);
}
if (upstream != null && !upstream.isClosed()) {
connector.getPlayers().remove(this);
connector.getSessionManager().removeSession(this);
upstream.disconnect(reason);
}
}
@ -1442,7 +1439,7 @@ public class GeyserSession implements CommandSender {
public void refreshEmotes(List<UUID> emotes) {
this.emotes.addAll(emotes);
for (GeyserSession player : connector.getPlayers()) {
for (GeyserSession player : connector.getSessionManager().getSessions().values()) {
List<UUID> pieces = new ArrayList<>();
for (UUID piece : emotes) {
if (!player.getEmotes().contains(piece)) {

View File

@ -53,7 +53,7 @@ public class BedrockEmoteTranslator extends PacketTranslator<EmotePacket> {
}
long javaId = session.getPlayerEntity().getEntityId();
for (GeyserSession otherSession : session.getConnector().getPlayers()) {
for (GeyserSession otherSession : session.getConnector().getSessionManager().getSessions().values()) {
if (otherSession != session) {
if (otherSession.isClosed()) continue;
if (otherSession.getEventLoop().inEventLoop()) {

View File

@ -47,6 +47,8 @@ public class JavaLoginSuccessTranslator extends PacketTranslator<LoginSuccessPac
playerEntity.setUsername(profile.getName());
playerEntity.setUuid(profile.getId());
session.getConnector().getSessionManager().addSession(playerEntity.getUuid(), session);
// Check if they are not using a linked account
if (remoteAuthType == AuthType.OFFLINE || playerEntity.getUuid().getMostSignificantBits() == 0) {
SkinManager.handleBedrockSkin(playerEntity, session.getClientData());

View File

@ -33,6 +33,7 @@ import org.geysermc.connector.network.session.GeyserSession;
import org.geysermc.connector.network.session.cache.WorldCache;
import org.geysermc.connector.utils.LanguageUtils;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicInteger;
public final class ScoreboardUpdater extends Thread {
@ -72,9 +73,10 @@ public final class ScoreboardUpdater extends Thread {
long currentTime = System.currentTimeMillis();
// reset score-packets per second every second
Collection<GeyserSession> sessions = connector.getSessionManager().getSessions().values();
if (currentTime - lastPacketsPerSecondUpdate >= 1000) {
lastPacketsPerSecondUpdate = currentTime;
for (GeyserSession session : connector.getPlayers()) {
for (GeyserSession session : sessions) {
ScoreboardSession scoreboardSession = session.getWorldCache().getScoreboardSession();
int oldPps = scoreboardSession.getPacketsPerSecond();
@ -94,7 +96,7 @@ public final class ScoreboardUpdater extends Thread {
if (currentTime - lastUpdate >= FIRST_MILLIS_BETWEEN_UPDATES) {
lastUpdate = currentTime;
for (GeyserSession session : connector.getPlayers()) {
for (GeyserSession session : sessions) {
WorldCache worldCache = session.getWorldCache();
ScoreboardSession scoreboardSession = worldCache.getScoreboardSession();
@ -132,7 +134,7 @@ public final class ScoreboardUpdater extends Thread {
if (timeSpent > 0) {
connector.getLogger().info(String.format(
"Scoreboard updater: took %s ms. Updated %s players",
timeSpent, connector.getPlayers().size()
timeSpent, sessions.size()
));
}
}

View File

@ -109,7 +109,7 @@ public class NewsHandler {
// }
break;
case BROADCAST_TO_OPERATORS:
for (GeyserSession player : GeyserConnector.getInstance().getPlayers()) {
for (GeyserSession player : GeyserConnector.getInstance().getSessionManager().getSessions().values()) {
if (player.getOpPermissionLevel() >= 2) {
session.sendMessage(ChatColor.GREEN + news.getMessage());
}