ProjectAres/Commons/bukkit/src/main/java/tc/oc/commons/bukkit/users/JoinMessageAnnouncer.java

316 lines
14 KiB
Java

package tc.oc.commons.bukkit.users;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import javax.inject.Inject;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import me.anxuiz.settings.Setting;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TranslatableComponent;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventException;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import tc.oc.api.bukkit.friends.OnlineFriends;
import tc.oc.api.bukkit.users.BukkitUserStore;
import tc.oc.api.bukkit.users.OnlinePlayers;
import tc.oc.api.docs.PlayerId;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.Session;
import tc.oc.api.docs.User;
import tc.oc.api.docs.UserId;
import tc.oc.api.docs.virtual.ServerDoc;
import tc.oc.api.message.MessageListener;
import tc.oc.api.message.MessageService;
import tc.oc.api.minecraft.MinecraftService;
import tc.oc.api.servers.ServerStore;
import tc.oc.api.sessions.SessionChange;
import tc.oc.commons.bukkit.chat.Audiences;
import tc.oc.commons.bukkit.chat.NameStyle;
import tc.oc.commons.bukkit.chat.PlayerComponent;
import tc.oc.commons.bukkit.format.ServerFormatter;
import tc.oc.commons.bukkit.nick.Identity;
import tc.oc.commons.bukkit.nick.IdentityProvider;
import tc.oc.commons.bukkit.settings.SettingManagerProvider;
import tc.oc.commons.core.chat.Audience;
import tc.oc.commons.core.chat.Component;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.util.Lazy;
import tc.oc.minecraft.scheduler.SyncExecutor;
import static com.google.common.base.Preconditions.checkArgument;
import static tc.oc.commons.core.IterableUtils.none;
import static tc.oc.commons.core.util.Nullables.first;
import static tc.oc.commons.core.util.Utils.notEqual;
/**
* Receives {@link SessionChange} messages from the topic exchange and generates
* connect/disconnect/change server announcements. ALL announcements are generated
* from queue messages, even local ones. However, local announcements are synced
* with their respective Bukkit events, to ensure that they appear ordered correctly
* in chat, and names are rendered in the correct state.
*
* The {@link JoinMessageSetting} affects both local and remote announcements,
* except that remote events are never displayed to non-friends.
*
* If join messages are disabled in the plugin config, no announcements will be
* made at all by this service.
*
* When a player changes their nickname, it will appear exactly as if the old identity
* disconnected from the network, and the new identity connected, to viewers who can't
* see through their disguise. If the change is immediate (i.e. nick -i) then both
* events will appear together in chat on the local server. Non-immediate changes will
* only show one message in chat, since the other identity is on a different server.
*/
public class JoinMessageAnnouncer implements MessageListener, Listener, PluginFacet {
private final MessageService queue;
private final OnlineFriends onlineFriends;
private final IdentityProvider identityProvider;
private final SettingManagerProvider playerSettings;
private final JoinMessageConfiguration config;
private final Audiences audiences;
private final ServerStore serverStore;
private final MinecraftService minecraftService;
private final OnlinePlayers onlinePlayers;
private final SyncExecutor syncExecutor;
private final BukkitUserStore userStore;
private final Setting setting;
// Events involving the local server are delayed until the actual Bukkit event,
// so that the player's name is rendered in the correct state. These caches
// hold the queue message during that delay.
private final Cache<UserId, SessionChange> pendingJoins = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).build();
private final Cache<UserId, SessionChange> pendingQuits = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.SECONDS).build();
@Inject JoinMessageAnnouncer(MessageService queue,
OnlineFriends onlineFriends,
IdentityProvider identityProvider,
SettingManagerProvider playerSettings,
JoinMessageConfiguration config,
Audiences audiences,
ServerStore serverStore,
MinecraftService minecraftService,
OnlinePlayers onlinePlayers,
SyncExecutor syncExecutor,
BukkitUserStore userStore) {
this.queue = queue;
this.onlineFriends = onlineFriends;
this.identityProvider = identityProvider;
this.playerSettings = playerSettings;
this.config = config;
this.audiences = audiences;
this.serverStore = serverStore;
this.minecraftService = minecraftService;
this.onlinePlayers = onlinePlayers;
this.syncExecutor = syncExecutor;
this.userStore = userStore;
this.setting = JoinMessageSetting.get();
}
@Override
public boolean isActive() {
return config.enabled();
}
@Override
public void enable() {
queue.subscribe(this, syncExecutor);
queue.bind(SessionChange.class);
}
@Override
public void disable() {
queue.unsubscribe(this);
}
@HandleMessage
public void onSessionChange(SessionChange change) {
checkArgument(change.old_session() != null || change.new_session() != null);
final Server localServer = minecraftService.getLocalServer();
final boolean localBefore = change.old_session() != null && change.old_session().server_id().equals(localServer._id());
final boolean localAfter = change.new_session() != null && change.new_session().server_id().equals(localServer._id());
if(!localBefore && localAfter && onlinePlayers.find(change.new_session().user()) == null) {
// Joining player is not here yet
pendingJoins.put(change.new_session().user(), change);
} else if(localBefore && !localAfter && onlinePlayers.find(change.old_session().user()) != null) {
// Quitting player hasn't left yet
pendingQuits.put(change.old_session().user(), change);
} else {
announce(change);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onJoin(PlayerJoinEvent event) {
event.setJoinMessage(null);
final User user = userStore.getUser(event.getPlayer());
final SessionChange change = pendingJoins.getIfPresent(user);
if(change != null) {
pendingJoins.invalidate(user);
announce(change);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onQuit(PlayerQuitEvent event) throws EventException {
event.setQuitMessage(null);
final User user = userStore.getUser(event.getPlayer());
final SessionChange change = pendingQuits.getIfPresent(user);
event.yield();
if(change != null) {
pendingQuits.invalidate(user);
announce(change);
}
}
private void announce(SessionChange change) {
final ChangedSession finished = new ChangedSession(change.old_session());
final ChangedSession started = new ChangedSession(change.new_session());
final PlayerId playerId = first(change.old_session(), change.new_session()).user();
// If neither session is from the local server, just loop through
// friends of the player, instead of all players online.
final Stream<? extends Player> viewers =
finished.isLocal() || started.isLocal() ? onlinePlayers.all().stream()
: onlineFriends.onlineFriends(playerId);
// Use lazy messages so we can reuse them for multiple viewers,
// without generating the ones we don't need. Depending on the
// situation, we could end up showing one, two, or all three of
// these messages for a single event.
final Lazy<BaseComponent> leaveMessage = Lazy.from(() -> {
final Component c = new Component(ChatColor.YELLOW);
if(!minecraftService.isLocalServer(finished.server)) {
c.extra(ServerFormatter.dark.nameWithDatacenter(finished.server)).extra(" ");
}
return c.extra(new TranslatableComponent("broadcast.leaveMessage", new PlayerComponent(finished.identity, NameStyle.VERBOSE)));
});
final Lazy<BaseComponent> joinMessage = Lazy.from(() -> {
final Component c = new Component(ChatColor.YELLOW);
if(!minecraftService.isLocalServer(started.server)) {
c.extra(ServerFormatter.dark.nameWithDatacenter(started.server)).extra(" ");
}
return c.extra(new TranslatableComponent("broadcast.joinMessage", new PlayerComponent(started.identity, NameStyle.VERBOSE)));
});
final Lazy<BaseComponent> changeMessage = Lazy.from(() -> new Component(ChatColor.YELLOW)
.extra(ServerFormatter.dark.nameWithDatacenter(finished.server))
.extra(" \u00BB ")
.extra(ServerFormatter.dark.nameWithDatacenter(started.server))
.extra(" ")
.extra(new TranslatableComponent("broadcast.changeServerMessage", new PlayerComponent(started.identity, NameStyle.VERBOSE))));
viewers.forEach(viewerPlayer -> {
if(viewerPlayer.getName().equals(playerId.username())) return;
final Viewer viewer = new Viewer(viewerPlayer);
if(!viewer.sendChangeServer(finished, started, changeMessage)) {
viewer.sendJoinLeave(leaveMessage, finished, started);
viewer.sendJoinLeave(joinMessage, started, finished);
}
});
}
class ChangedSession {
final Identity identity;
final Server server;
private ChangedSession(@Nullable Session session) {
identity = session == null ? null : identityProvider.createIdentity(session);
server = session == null ? null : serverStore.byId(session.server_id());
}
boolean isLocal() {
return server != null && minecraftService.isLocalServer(server);
}
boolean isVisible() {
// Can't see a session that does not exist
if(server == null) return false;
// Local sessions are always visible
if(minecraftService.isLocalServer(server)) return true;
// Private server sessions are never visible
if(server.visibility() == ServerDoc.Visibility.PRIVATE) return false;
// Sessions from other networks are (configurably) invisible
if(!(config.crossNetwork() || minecraftService.getLocalServer().network().equals(server.network()))) return false;
// Check family/realm visibility filters
if(!config.families().test(server.family())) return false;
if(none(server.realms(), config.realms())) return false;
return true;
}
boolean isVisibleTo(CommandSender viewer) {
// Remote sessions are never visible to non-friends
return isVisible() && (isLocal() || identity.isFriend(viewer));
}
boolean belongsTo(Identity identity, CommandSender viewer) {
return isVisibleTo(viewer) && this.identity.isSamePerson(identity, viewer);
}
}
class Viewer {
final Player player;
final Audience audience;
final JoinMessageSetting.Options jms;
private Viewer(Player player) {
this.player = player;
this.audience = audiences.get(player);
this.jms = playerSettings.getManager(player).getValue(setting, JoinMessageSetting.Options.class);
}
boolean sendChangeServer(ChangedSession finished, ChangedSession started, Lazy<BaseComponent> message) {
// If both sessions are visible,
// and the sessions are on different servers,
// and the sessions appear to belong to the same person,
// then show a "change server" message.
if(finished.isVisibleTo(player) && started.isVisibleTo(player) &&
notEqual(finished.server, started.server) &&
finished.identity.isSamePerson(started.identity, player)) {
if(jms.isAllowed(started.identity.familiarity(player))) {
audience.sendMessage(message.get());
}
return true;
}
return false;
}
void sendJoinLeave(Lazy<BaseComponent> message, ChangedSession session, ChangedSession other) {
// If session A is visible,
// and session B does not appear to be another session belonging to the same person as session A,
// and the viewer's setting allows messages about the owner of session A,
// then inform the viewer about the start/finish of session A.
if(session.isVisibleTo(player) &&
!other.belongsTo(session.identity, player) &&
jms.isAllowed(session.identity.familiarity(player))) {
audience.sendMessage(message.get());
}
}
}
}