316 lines
14 KiB
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());
|
|
}
|
|
}
|
|
}
|
|
}
|