Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better spectate #840

Merged
merged 10 commits into from
Nov 13, 2021
Merged
2 changes: 2 additions & 0 deletions core/src/main/java/tc/oc/pgm/api/Modules.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
import tc.oc.pgm.modules.MultiTradeMatchModule;
import tc.oc.pgm.modules.PlayerTimeMatchModule;
import tc.oc.pgm.modules.SoundsMatchModule;
import tc.oc.pgm.modules.SpectateMatchModule;
import tc.oc.pgm.modules.ToolRepairMatchModule;
import tc.oc.pgm.modules.ToolRepairModule;
import tc.oc.pgm.modules.WorldTimeModule;
Expand Down Expand Up @@ -161,6 +162,7 @@ static void registerAll() {
register(MapmakerMatchModule.class, MapmakerMatchModule::new);
register(TNTRenderMatchModule.class, TNTRenderMatchModule::new);
register(PlayerTimeMatchModule.class, PlayerTimeMatchModule::new);
register(SpectateMatchModule.class, SpectateMatchModule::new);

// FIXME: Disabled due to lag - look into future optimization
// register(ProjectileTrailMatchModule.class, ProjectileTrailMatchModule::new);
Expand Down
16 changes: 16 additions & 0 deletions core/src/main/java/tc/oc/pgm/api/player/MatchPlayer.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package tc.oc.pgm.api.player;

import java.util.List;
import java.util.UUID;
import javax.annotation.Nullable;
import org.bukkit.GameMode;
Expand Down Expand Up @@ -250,6 +251,21 @@ default boolean isLegacy() {
@Override
PlayerInventory getInventory();

/**
* Get the current spectator target of the {@link MatchPlayer} if any
*
* @return the current spectator target if any
*/
@Nullable
MatchPlayer getSpectatorTarget();

/**
* Get the players currently spectating the {@link MatchPlayer}, if any
*
* @return the players currently spectating, if any
*/
List<MatchPlayer> getSpectators();

@Deprecated
void internalSetParty(Party party);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public class DeathMessageMatchModule implements MatchModule, Listener {
private final Logger logger;

public DeathMessageMatchModule(Match match) {
logger = match.getLogger();
this.logger = match.getLogger();
}

@EventHandler(priority = EventPriority.LOWEST)
Expand All @@ -41,14 +41,14 @@ public void handleDeathBroadcast(MatchPlayerDeathEvent event) {
for (MatchPlayer viewer : event.getMatch().getPlayers()) {
switch (viewer.getSettings().getValue(SettingKey.DEATH)) {
case DEATH_OWN:
if (event.isInvolved(viewer)) {
if (event.isInvolved(viewer) || event.isInvolved(viewer.getSpectatorTarget())) {
viewer.sendMessage(message);
} else if (event.isTeamKill() && viewer.getBukkit().hasPermission(Permissions.STAFF)) {
viewer.sendMessage(message.decoration(TextDecoration.ITALIC, true));
}
break;
case DEATH_ALL:
if (event.isInvolved(viewer)) {
if (event.isInvolved(viewer) || event.isInvolved(viewer.getSpectatorTarget())) {
viewer.sendMessage(message.decoration(TextDecoration.BOLD, true));
} else {
viewer.sendMessage(message);
Expand Down
18 changes: 17 additions & 1 deletion core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import tc.oc.pgm.kits.Kit;
import tc.oc.pgm.kits.MaxHealthKit;
import tc.oc.pgm.kits.WalkSpeedKit;
import tc.oc.pgm.modules.SpectateMatchModule;
import tc.oc.pgm.util.Audience;
import tc.oc.pgm.util.ClassLogger;
import tc.oc.pgm.util.TimeUtils;
Expand Down Expand Up @@ -206,7 +207,10 @@ public boolean canInteract() {

@Override
public boolean canSee(MatchPlayer other) {
if (!other.isVisible()) return false;
@Nullable MatchPlayer spectatorTarget = this.getSpectatorTarget();
boolean isSpectatorTarget =
spectatorTarget != null && spectatorTarget.getId().equals(other.getId());
if (!other.isVisible() && !isSpectatorTarget) return false;
if (other.isParticipating()) return true;
if (other.isVanished() && !getBukkit().hasPermission(Permissions.VANISH)) return false;
return isObserving()
Expand Down Expand Up @@ -449,6 +453,18 @@ public void tick(Match match, Tick tick) {
return audience;
}

@Nullable
@Override
public MatchPlayer getSpectatorTarget() {
Player bukkit = getBukkit();
return bukkit == null ? null : match.getPlayer(bukkit.getSpectatorTarget());
}

@Override
public List<MatchPlayer> getSpectators() {
return match.needModule(SpectateMatchModule.class).getSpectating(this);
}

@Override
@Nullable
public Party getFilterableParent() {
Expand Down
89 changes: 89 additions & 0 deletions core/src/main/java/tc/oc/pgm/modules/SpectateMatchModule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package tc.oc.pgm.modules;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import org.bukkit.entity.Entity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import tc.oc.pgm.api.match.Match;
import tc.oc.pgm.api.match.MatchModule;
import tc.oc.pgm.api.match.MatchScope;
import tc.oc.pgm.api.player.MatchPlayer;
import tc.oc.pgm.community.events.PlayerVanishEvent;
import tc.oc.pgm.events.ListenerScope;
import tc.oc.pgm.events.PlayerLeaveMatchEvent;

@ListenerScope(MatchScope.LOADED)
public class SpectateMatchModule implements MatchModule, Listener {

private final Match match;

// Stores which players (the list) is spectating a player(the uuid)
private final Multimap<UUID, UUID> spectators = ArrayListMultimap.create();

public SpectateMatchModule(Match match) {
this.match = match;
}

@EventHandler
public void onPlayerLeave(PlayerLeaveMatchEvent event) {
event.getPlayer().getSpectators().forEach(p -> p.getBukkit().setSpectatorTarget(null));
}

@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
spectators.values().removeIf(event.getPlayer().getUniqueId()::equals);
}

@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerVanish(PlayerVanishEvent event) {
if (!event.isVanished()) return;
event.getPlayer().getSpectators().forEach(p -> p.getBukkit().setSpectatorTarget(null));
}

@EventHandler
public void onPlayerTeleport(PlayerTeleportEvent event) {
if (event.getCause() == PlayerTeleportEvent.TeleportCause.UNKNOWN) {
MatchPlayer player = match.getPlayer(event.getPlayer());
if (player != null) {
MatchPlayer spectating = player.getSpectatorTarget();
if (spectating != null) { // Player is going into spectate
spectators.get(spectating.getId()).add(player.getId());
KingOfSquares marked this conversation as resolved.
Show resolved Hide resolved
} else {
spectators.entries().removeIf(e -> e.getValue().equals(player.getId()));
}
}
}
}

/**
* Get the {@link MatchPlayer}s currently spectating the {@link MatchPlayer} that holds the given
* {@link UUID}, if any.
*/
public List<MatchPlayer> getSpectating(UUID player) {
final Collection<UUID> list = spectators.get(player);
if (list == null) return ImmutableList.of();
return Collections.unmodifiableList(
list.stream().map(match::getPlayer).filter(Objects::nonNull).collect(Collectors.toList()));
KingOfSquares marked this conversation as resolved.
Show resolved Hide resolved
}

/** Get the {@link MatchPlayer}s currently spectating the given {@link MatchPlayer}, if any. */
public List<MatchPlayer> getSpectating(MatchPlayer player) {
return getSpectating(player.getId());
}

/** Get the {@link MatchPlayer}s currently spectating the given {@link Entity}, if any. */
public List<MatchPlayer> getSpectating(Entity entity) {
return getSpectating(entity.getUniqueId());
}
}
20 changes: 14 additions & 6 deletions core/src/main/java/tc/oc/pgm/spawns/states/Dead.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.Component.translatable;
import static tc.oc.pgm.util.text.PlayerComponent.player;

import java.util.List;
import javax.annotation.Nullable;
Expand All @@ -21,6 +22,7 @@
import tc.oc.pgm.spawns.SpawnModule;
import tc.oc.pgm.spawns.events.DeathKitApplyEvent;
import tc.oc.pgm.util.TimeUtils;
import tc.oc.pgm.util.named.NameStyle;
import tc.oc.pgm.util.nms.NMSHacks;

/** Player is waiting to respawn after dying in-game */
Expand Down Expand Up @@ -140,19 +142,25 @@ public void requestSpawn() {
}

@Override
protected Component getTitle() {
return translatable("deathScreen.title", NamedTextColor.RED);
protected Component getTitle(boolean spectator) {
return translatable(
"deathScreen.title" + (spectator ? ".spectator" : ""),
NamedTextColor.RED,
player(this.player.getId(), NameStyle.COLOR));
}

@Override
protected Component getSubtitle() {
protected Component getSubtitle(boolean spectator) {
long ticks = ticksUntilRespawn();
if (ticks > 0) {
return translatable(
spawnRequested ? "death.respawn.confirmed.time" : "death.respawn.unconfirmed.time",
spawnRequested
? "death.respawn.confirmed.time"
: "death.respawn.unconfirmed.time" + (spectator ? ".spectator" : ""),
NamedTextColor.GREEN,
text(String.format("%.1f", (ticks / (float) 20)), NamedTextColor.AQUA));
} else {
return super.getSubtitle();
return super.getSubtitle(spectator);
}
}

Expand All @@ -179,7 +187,7 @@ public void sendMessage() {
? "death.respawn.confirmed.time"
: "death.respawn.unconfirmed.time",
text((int) (ticks / (float) 20), NamedTextColor.AQUA))
: super.getSubtitle())
: super.getSubtitle(false))
.color(NamedTextColor.GREEN));
}
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/tc/oc/pgm/spawns/states/Joining.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void enterState() {
}

@Override
protected Component getTitle() {
protected Component getTitle(boolean spectator) {
return empty();
}

Expand Down
22 changes: 13 additions & 9 deletions core/src/main/java/tc/oc/pgm/spawns/states/Spawning.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,22 +100,26 @@ protected boolean trySpawn() {
public void sendMessage() {}

public void updateTitle() {
player.showTitle(
title(
getTitle(),
getSubtitle().color(NamedTextColor.GREEN),
Title.Times.of(Duration.ZERO, fromTicks(3), fromTicks(3))));
Title.Times times = Title.Times.of(Duration.ZERO, fromTicks(3), fromTicks(3));

player.showTitle(title(getTitle(false), getSubtitle(false), times));

Title spectatorTitle = title(getTitle(true), getSubtitle(true), times);
player.getSpectators().forEach(p -> p.showTitle(spectatorTitle));
}

protected abstract Component getTitle();
protected abstract Component getTitle(boolean spectator);

protected Component getSubtitle() {
protected Component getSubtitle(boolean spectator) {
if (!spawnRequested) {
return translatable("death.respawn.unconfirmed");
return translatable(
"death.respawn.unconfirmed" + (spectator ? ".spectator" : ""), NamedTextColor.GREEN);
} else if (options.message != null) {
return options.message;
} else {
return translatable("death.respawn.confirmed.waiting");
return translatable(
"death.respawn.confirmed.waiting" + (spectator ? ".spectator" : ""),
NamedTextColor.GREEN);
}
}
}
13 changes: 12 additions & 1 deletion util/src/main/i18n/templates/death.properties
Original file line number Diff line number Diff line change
Expand Up @@ -670,4 +670,15 @@ death.respawn.confirmed.time = Respawning in {0}s

death.respawn.confirmed.waiting = You will respawn as soon as possible...

death.respawn.confirmed.waiting.flagDropped = You will respawn when the flag is dropped...
death.respawn.confirmed.waiting.flagDropped = You will respawn when the flag is dropped...

# {0} = A player name
deathScreen.title.spectator = {0} died!

death.respawn.unconfirmed.spectator = They can left click to respawn

death.respawn.unconfirmed.time.spectator = They can respawn in {0}s

death.respawn.confirmed.waiting.spectator = They will respawn as soon as possible...

death.respawn.confirmed.waiting.flagDropped.spectator = They will respawn when the flag is dropped...
16 changes: 12 additions & 4 deletions util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,18 @@ static void sendPacket(Player bukkitPlayer, Object packet) {
}
}

@SuppressWarnings("unchecked")
static void sendPacketToViewers(Entity entity, Object packet) {
sendPacketToViewers(entity, packet, false);
}

static void sendPacketToViewers(Entity entity, Object packet, boolean excludeSpectators) {
EntityTrackerEntry entry = getTrackerEntry(entity);
for (EntityPlayer viewer : ((Set<EntityPlayer>) entry.trackedPlayers)) {
if (excludeSpectators) {
Entity spectatorTarget = viewer.getBukkitEntity().getSpectatorTarget();
if (spectatorTarget != null && spectatorTarget.getUniqueId().equals(entity.getUniqueId()))
continue;
}
viewer.playerConnection.sendPacket((Packet) packet);
}
}
Expand Down Expand Up @@ -821,9 +829,9 @@ static void playDeathAnimation(Player player) {

Packet<?> teleport = teleportEntityPacket(player.getEntityId(), location);

sendPacketToViewers(player, metadata);
sendPacketToViewers(player, useBed);
sendPacketToViewers(player, teleport);
sendPacketToViewers(player, metadata, true);
sendPacketToViewers(player, useBed, true);
sendPacketToViewers(player, teleport, true);
}

static org.bukkit.enchantments.Enchantment getEnchantment(String key) {
Expand Down