From bed045575c35f4064751474a8db3f2d0633ecda6 Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Sun, 23 Apr 2023 20:00:38 +0200 Subject: [PATCH 01/23] Fix prevented team force due to combatlog & improve api (#1175) Signed-off-by: Pablo Herrera --- .../tc/oc/pgm/blitz/BlitzMatchModule.java | 2 +- .../pgm/events/PlayerParticipationEvent.java | 12 ++++++++++- .../events/PlayerParticipationStartEvent.java | 11 +--------- .../events/PlayerParticipationStopEvent.java | 21 +++++++++++++++++-- .../java/tc/oc/pgm/filters/Filterable.java | 8 +++++++ .../main/java/tc/oc/pgm/match/MatchImpl.java | 17 ++++++++------- .../tracker/trackers/CombatLogTracker.java | 2 ++ 7 files changed, 52 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java b/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java index 65e3f74e56..a05b34e589 100644 --- a/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java @@ -91,7 +91,7 @@ public void handleDeath(final MatchPlayerDeathEvent event) { } } - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void handleLeave(final PlayerPartyChangeEvent event) { int lives = this.lifeManager.getLives(event.getPlayer().getId()); if (event.getOldParty() instanceof Competitor && lives > 0) { diff --git a/core/src/main/java/tc/oc/pgm/events/PlayerParticipationEvent.java b/core/src/main/java/tc/oc/pgm/events/PlayerParticipationEvent.java index cfc813c8c4..f9e52f69dd 100644 --- a/core/src/main/java/tc/oc/pgm/events/PlayerParticipationEvent.java +++ b/core/src/main/java/tc/oc/pgm/events/PlayerParticipationEvent.java @@ -5,20 +5,25 @@ import net.kyori.adventure.text.Component; import org.bukkit.event.Cancellable; import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.party.Competitor; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.api.player.event.MatchPlayerEvent; +import tc.oc.pgm.join.JoinRequest; public abstract class PlayerParticipationEvent extends MatchPlayerEvent implements Cancellable { private final Competitor competitor; + private final JoinRequest request; private boolean cancelled; private @Nullable Component cancelReason; - protected PlayerParticipationEvent(MatchPlayer player, Competitor competitor) { + protected PlayerParticipationEvent( + MatchPlayer player, Competitor competitor, JoinRequest request) { super(player); this.competitor = competitor; + this.request = request; } /** NOTE: this Competitor MAY not be in the match at this point */ @@ -26,6 +31,11 @@ public Competitor getCompetitor() { return competitor; } + @NotNull + public JoinRequest getRequest() { + return request; + } + @Override public boolean isCancelled() { return cancelled; diff --git a/core/src/main/java/tc/oc/pgm/events/PlayerParticipationStartEvent.java b/core/src/main/java/tc/oc/pgm/events/PlayerParticipationStartEvent.java index 05789aa1fc..07b03ca044 100644 --- a/core/src/main/java/tc/oc/pgm/events/PlayerParticipationStartEvent.java +++ b/core/src/main/java/tc/oc/pgm/events/PlayerParticipationStartEvent.java @@ -15,17 +15,8 @@ *

If cancellation is not required, {@link PlayerPartyChangeEvent} should be used instead. */ public class PlayerParticipationStartEvent extends PlayerParticipationEvent { - - private final JoinRequest request; - public PlayerParticipationStartEvent( @NotNull MatchPlayer player, @NotNull Competitor competitor, @NotNull JoinRequest request) { - super(player, competitor); - this.request = request; - } - - @NotNull - public JoinRequest getRequest() { - return request; + super(player, competitor, request); } } diff --git a/core/src/main/java/tc/oc/pgm/events/PlayerParticipationStopEvent.java b/core/src/main/java/tc/oc/pgm/events/PlayerParticipationStopEvent.java index f8d1950397..5a1eca6233 100644 --- a/core/src/main/java/tc/oc/pgm/events/PlayerParticipationStopEvent.java +++ b/core/src/main/java/tc/oc/pgm/events/PlayerParticipationStopEvent.java @@ -1,7 +1,10 @@ package tc.oc.pgm.events; +import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.party.Competitor; +import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.join.JoinRequest; /** * Called immediately before a player leaves a {@link Competitor}. This differs from {@link @@ -13,7 +16,21 @@ *

If cancellation is not required, {@link PlayerPartyChangeEvent} should be used instead. */ public class PlayerParticipationStopEvent extends PlayerParticipationEvent { - public PlayerParticipationStopEvent(MatchPlayer player, Competitor competitor) { - super(player, competitor); + + private final @Nullable Party nextParty; + + public PlayerParticipationStopEvent( + MatchPlayer player, Competitor competitor, JoinRequest request, @Nullable Party nextParty) { + super(player, competitor, request); + this.nextParty = nextParty; + } + + /** + * The next party the player will try to join after ending this participation. + * + * @return the next party, or null if the player is disconnecting + */ + public @Nullable Party getNextParty() { + return nextParty; } } diff --git a/core/src/main/java/tc/oc/pgm/filters/Filterable.java b/core/src/main/java/tc/oc/pgm/filters/Filterable.java index 20f55af7f1..9e7d9d7171 100644 --- a/core/src/main/java/tc/oc/pgm/filters/Filterable.java +++ b/core/src/main/java/tc/oc/pgm/filters/Filterable.java @@ -6,6 +6,7 @@ import tc.oc.pgm.api.filter.Filter; import tc.oc.pgm.api.filter.query.MatchQuery; import tc.oc.pgm.api.filter.query.Query; +import tc.oc.pgm.api.match.Match; import tc.oc.pgm.util.Audience; /** @@ -37,6 +38,13 @@ public interface Filterable extends MatchQuery, Audience { default > F getFilterableAncestor(Class type) { if (type.isInstance(this)) { return (F) this; + } else if (Match.class.isAssignableFrom(type)) { + // When disconnecting the player's party is set to null. A monostable filter calling + // #filterable(match) will end with null due to player -> (null) party -> match, + // however going straight to match works fine. + // A use-case for this is a time filter used for blitz. Doing /obs won't eliminate you, + // leaving the server does. This is due to the filter not finding the match and disallowing. + return (F) this.getMatch(); } else { @Nullable Filterable parent = getFilterableParent(); return parent == null ? null : parent.getFilterableAncestor(type); diff --git a/core/src/main/java/tc/oc/pgm/match/MatchImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchImpl.java index 4a6301f5de..d3abb09a11 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchImpl.java @@ -20,6 +20,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import net.kyori.adventure.text.Component; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.entity.Player; @@ -516,11 +517,12 @@ private boolean setOrClearPlayerParty( if (oldParty instanceof Competitor) { PlayerParticipationEvent request = - new PlayerParticipationStopEvent(player, (Competitor) oldParty); + new PlayerParticipationStopEvent(player, (Competitor) oldParty, joinRequest, newParty); callEvent(request); - if (request.isCancelled() - && newParty != null) { // Can't cancel this if the player is leaving the match - player.sendWarning(request.getCancelReason()); + // Can't cancel this if the player is leaving the match + if (request.isCancelled() && newParty != null) { + if (!Objects.equals(Component.empty(), request.getCancelReason())) + player.sendWarning(request.getCancelReason()); return false; } } @@ -529,9 +531,10 @@ private boolean setOrClearPlayerParty( PlayerParticipationEvent request = new PlayerParticipationStartEvent(player, (Competitor) newParty, joinRequest); callEvent(request); - if (request.isCancelled() - && oldParty != null) { // Can't cancel this if the player is joining the match - player.sendWarning(request.getCancelReason()); + // Can't cancel this if the player is joining the match + if (request.isCancelled() && oldParty != null) { + if (!Objects.equals(Component.empty(), request.getCancelReason())) + player.sendWarning(request.getCancelReason()); return false; } } diff --git a/core/src/main/java/tc/oc/pgm/tracker/trackers/CombatLogTracker.java b/core/src/main/java/tc/oc/pgm/tracker/trackers/CombatLogTracker.java index 1055bcb150..d53617fd3c 100644 --- a/core/src/main/java/tc/oc/pgm/tracker/trackers/CombatLogTracker.java +++ b/core/src/main/java/tc/oc/pgm/tracker/trackers/CombatLogTracker.java @@ -28,6 +28,7 @@ import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.events.PlayerParticipationStopEvent; +import tc.oc.pgm.join.JoinRequest; import tc.oc.pgm.tracker.TrackerMatchModule; import tc.oc.pgm.util.material.Materials; @@ -210,6 +211,7 @@ public static boolean isCombatLog(PlayerDeathEvent event) { @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onParticipationStop(PlayerParticipationStopEvent event) { if (event.getMatch().isRunning() + && !event.getRequest().has(JoinRequest.Flag.FORCE) && this.getImminentDeath(event.getPlayer().getBukkit()) != null) { event.cancel(translatable("leave.err.combatLog")); event.setCancelled(true); From 24b044bdb604ba7b20f41be35b9be6bce0e1e320 Mon Sep 17 00:00:00 2001 From: "BT (calcastor/mame)" <43831917+calcastor@users.noreply.github.com> Date: Sun, 23 Apr 2023 11:01:16 -0700 Subject: [PATCH 02/23] Gamemode: resolve mode title typos/inconsistencies (#1174) Signed-off-by: BT (calcastor/mame) <43831917+calcastor@users.noreply.github.com> --- core/src/main/java/tc/oc/pgm/api/map/Gamemode.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/api/map/Gamemode.java b/core/src/main/java/tc/oc/pgm/api/map/Gamemode.java index 0b46f5bf76..bc0f6fb0f8 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/Gamemode.java +++ b/core/src/main/java/tc/oc/pgm/api/map/Gamemode.java @@ -3,14 +3,14 @@ public enum Gamemode { ATTACK_DEFEND("ad", "Attack/Defend", "A/D"), ARCADE("arcade", "Arcade", "Arcade"), - BEDWARS("bedwars", "Bedwars", "Bedwars"), + BEDWARS("bedwars", "Bed Wars", "Bed Wars"), BLITZ("blitz", "Blitz", "Blitz"), BLITZ_RAGE("br", "Blitz: Rage", "Blitz: Rage"), BRIDGE("bridge", "Bridge", "Bridge"), - CAPTURE_THE_FLAG("ctf", "Capture The Flag", "CTF"), + CAPTURE_THE_FLAG("ctf", "Capture the Flag", "CTF"), CONTROL_THE_POINT("cp", "Control the Point", "CP"), CAPTURE_THE_WOOL("ctw", "Capture the Wool", "CTW"), - DESTROY_THE_CORE("dtc", "Destoy the Core", "DTC"), + DESTROY_THE_CORE("dtc", "Destroy the Core", "DTC"), DESTROY_THE_MONUMENT("dtm", "Destroy the Monument", "DTM"), FREE_FOR_ALL("ffa", "Free for All", "FFA"), FLAG_FOOTBALL("ffb", "Flag Football", "FFB"), From 2e84fd4378163ff5dd8edf31926cbcd17e7f19c9 Mon Sep 17 00:00:00 2001 From: "BT (calcastor/mame)" <43831917+calcastor@users.noreply.github.com> Date: Tue, 25 Apr 2023 08:58:36 -0700 Subject: [PATCH 03/23] Fix Docker image (#1173) Signed-off-by: BT (calcastor/mame) <43831917+calcastor@users.noreply.github.com> --- server/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pom.xml b/server/pom.xml index fea5bfc5e2..5ac7028a34 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -116,7 +116,7 @@ 3.3.1 - shipilev/openjdk-shenandoah:8 + shipilev/openjdk:8 pgm From 4daf0709b9d04fe88ef57e62da1ba65755557617 Mon Sep 17 00:00:00 2001 From: KingOfSquares <19822231+KingOfSquares@users.noreply.github.com> Date: Thu, 27 Apr 2023 17:14:09 +0200 Subject: [PATCH 04/23] Remove unused strings in moderation.properties (#1148) Signed-off-by: KingSimon <19822231+KingOfSquares@users.noreply.github.com> --- .../main/i18n/templates/moderation.properties | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/util/src/main/i18n/templates/moderation.properties b/util/src/main/i18n/templates/moderation.properties index 0f1ca88062..856c033a67 100644 --- a/util/src/main/i18n/templates/moderation.properties +++ b/util/src/main/i18n/templates/moderation.properties @@ -5,18 +5,18 @@ moderation.type = Type: {0} moderation.type.kick = Kicked -moderation.type.mute = Muted - moderation.type.warn = Warned moderation.type.ban = Permanent Ban moderation.type.name_ban = Username Ban -# {0} = duration of the ban (e.g. "7 days") +# {0} = duration of the ban (e.g. "7 day") +# time strings can be found in the misc package(e.g. misc.day) moderation.type.temp_ban = {0} ban # {0} = duration of the mute (e.g. "1 hour") +# time strings can be found in the misc package(e.g. misc.hour) moderation.type.mute = {0} mute # {0} = number of online staff @@ -27,25 +27,26 @@ moderation.staff.empty = No staff online :( # {0} = punishment reason (e.g. "Hacking is not allowed") moderation.screen.kick = You were kicked for {0} -# {0} = punishment reason +# {0} = punishment reason (e.g. "Hacking is not allowed") moderation.screen.ban = You were permanently banned for {0} -# {0} = punishment reason +# {0} = punishment reason (e.g. "Talking about the Electroid-Starbucks papers are not allowed") moderation.screen.temp_ban = You were temporarily banned for {0} # {0} = banned username (e.g "OFFENSIVE_NaMe") moderation.screen.name_ban = You have been banned due to your username {0} -# {0} = a http link (e.g. "https://example.com/rules") +# {0} = a http(s) link (e.g. "https://example.com/rules") moderation.screen.rulesLink = Please review our rules at {0} # {0} = staff member name (e.g. "Notch") moderation.screen.signoff = Issued by {0} # {0} = duration (e.g. "2 weeks") +# time strings can be found in the misc package(e.g. misc.weeks) moderation.screen.expires = Expires in {0} -# {0} = mute reason +# {0} = mute reason (e.g. "Please keep a peaceful atmosphere in chat") moderation.mute.message = You are unable to chat while muted: {0} moderation.mute.list = Muted Players @@ -54,46 +55,46 @@ moderation.mute.none = There are no muted players online! moderation.mute.noReason = No reason provided -# {0} = player name +# {0} = player name (e.g. "Notch") moderation.mute.hover = Click to unmute {0} -# {0} = player name +# {0} = player name (e.g. "_jeb") moderation.mute.target = {0} is muted and unable to receive messages -# {0} = player name +# {0} = player name (e.g. "epic_username") moderation.mute.existing = {0} is already muted! -# {0} = player name +# {0} = player name (e.g. "Pablete1234") moderation.unmute.sender = You have unmuted {0} moderation.unmute.target = You have been unmuted and may now send messages -# {0} = player name +# {0} = player name (e.g. "ElectroidFilms") moderation.unmute.none = {0} is not muted. moderation.reports.none = There have been no recent reports! moderation.reports.header = Recent Reports -# {0} = player name +# {0} = player name (e.g. "Xx_Warrior_xX") moderation.reports.hover = Reported by {0} moderation.alts.header = Alt-Accounts -# {0} = player name +# {0} = player name (e.g. "KingOfSquares") moderation.alts.noAlts = {0} has no known alternate accounts. # {0} = ip address (e.g. "1.2.3.4") moderation.ipBan.invalidIP = {0} is not a valid IP address -# {0} = player name +# {0} = player name (e.g. "applenick") moderation.ipBan.banned = {0} has been IP banned # {0} = player name or ip address (e.g. "Notch" or "1.2.3.4") # {1} = number of alternate accounts moderation.ipBan.bannedWithAlts = {0} has been IP banned along with {1} online alts -# {0} = player name +# {0} = player name (e.g. "SethBling") moderation.records.lookupNone = {0} has no punishment record moderation.records.header = Punishment Info @@ -103,24 +104,23 @@ moderation.records.history = Punishment History # {0} = punishment reason (e.g. "Hacking is not allowed") moderation.records.reason = Reason: {0} -# {0} = player name -# {1} = another player name +# {0} = player name (e.g. "CaptainSparklez") +# {1} = another player name (e.g. "HatFilms") moderation.similarIP.loginEvent = {0} has a similar IP to banned player {1} -# {0} = player name +# {0} = player name (e.g. "Seananners") # {1} = past duration (e.g. "5 minutes ago") +# time strings can be found in the misc package(e.g. misc.hour) moderation.similarIP.hover = Banned by {0}, {1} -# {0} = player name +# {0} = player name (e.g. "Vareide") moderation.freeze.freeze = You have frozen {0} -# {0} = player name +# {0} = player name (e.g. "Notch") moderation.freeze.unfreeze = You have unfrozen {0} -# {0} = player name moderation.freeze.frozen = You have been frozen -# {0} = player name moderation.freeze.unfrozen = You have been unfrozen moderation.freeze.itemName = Player Freezer @@ -129,13 +129,13 @@ moderation.freeze.itemDescription = Right-click a player to freeze/thaw them moderation.freeze.notEnabled = Freeze is not enabled -# {0} = player name +# {0} = player name (e.g. "Brottweiler") moderation.freeze.exempt = {0} may not be frozen -# {0} = player name +# {0} = player name (e.g. "TheMolkaPL") moderation.freeze.alreadyFrozen = {0} is already frozen -# {0} = player name +# {0} = player name (e.g. "direwolf20") moderation.freeze.alreadyThawed = {0} is not frozen # {0} = number of players @@ -143,17 +143,17 @@ moderation.freeze.alreadyThawed = {0} is not frozen moderation.freeze.frozenList.online = Frozen Players ({0}): {1} # {0} = number of players -# {1} = list of player names +# {1} = list of player names (e.g. "Diamyx, Pugzy and Eclipsen") moderation.freeze.frozenList.offline = Offline Frozen Players ({0}): {1} moderation.freeze.frozenList.none = There are no frozen players! -# {0} = staff player name -# {1} = target player name +# {0} = staff player name (e.g. "Samwellys") +# {1} = target player name (e.g. "Furioso") moderation.freeze.broadcast.frozen = {0} has frozen {1} -# {0} = staff player name -# {1} = target player name +# {0} = staff player name (e.g. "Cloudy") +# {1} = target player name (e.g. "_jeb") moderation.freeze.broadcast.thaw = {0} has unfrozen {1} moderation.freeze.broadcast.hover = Click to reverse action @@ -168,25 +168,25 @@ moderation.defuse.tooltip = Right-click to defuse TNT in a 5-block radius moderation.defuse.world = You defused world TNT. -# {0} = player name +# {0} = player name (e.g. "Hypixel") moderation.defuse.player = You defused {0}'s TNT. -# {0} = defuser player name -# {1} = griefer player name -# {2} = entity name +# {0} = defuser player name (e.g. "CaptainSparklez") +# {1} = griefer player name (e.g. "Technoblade" +# {2} = entity name (e.g. "Exploding Ball") moderation.defuse.alert.player = {0} defused {1}'s {2}. -# {0} = defuser player name -# {1} = entity name +# {0} = defuser player name (e.g. "kashike") +# {1} = entity name (e.g "Bomb") moderation.defuse.alert.world = {0} defused world {1}. -# {0} = reporter player name -# {1} = accused player name +# {0} = reporter player name (e.g. "Monti") +# {1} = accused player name (e.g. "Notch") # {2} = reason for report (e.g. "Is griefing my home") moderation.report.notify = {0} reported {1}: {2} moderation.report.acknowledge = The issue will be dealt with shortly. -# {0} = number of seconds +# {0} = number of seconds (e.g. "1", "30" etc.) moderation.afk.warn = You will be disconnected for inactivity in {0} seconds moderation.afk.kick = You were disconnected for inactivity From 394ce9ba5f9f6e4046cfb26d8ea01f4243634917 Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Thu, 11 May 2023 20:24:19 +0200 Subject: [PATCH 05/23] Support map variants (#1179) Signed-off-by: Pablo Herrera --- core/src/main/java/tc/oc/pgm/PGMConfig.java | 71 ++++---- core/src/main/java/tc/oc/pgm/PGMPlugin.java | 2 +- core/src/main/java/tc/oc/pgm/api/Config.java | 5 +- .../java/tc/oc/pgm/api/map/MapContext.java | 8 +- .../main/java/tc/oc/pgm/api/map/MapInfo.java | 22 ++- .../java/tc/oc/pgm/api/map/MapSource.java | 21 ++- .../tc/oc/pgm/api/map/factory/MapFactory.java | 9 + .../pgm/api/map/factory/MapSourceFactory.java | 14 +- .../oc/pgm/api/map/includes/MapInclude.java | 4 +- .../api/map/includes/MapIncludeProcessor.java | 18 +- .../java/tc/oc/pgm/map/MapContextImpl.java | 33 ++-- .../java/tc/oc/pgm/map/MapFactoryImpl.java | 58 +++--- .../tc/oc/pgm/map/MapFilePreprocessor.java | 171 ++++++++++++++++++ .../main/java/tc/oc/pgm/map/MapInfoImpl.java | 162 ++++++++--------- .../java/tc/oc/pgm/map/MapLibraryImpl.java | 117 ++++++------ .../oc/pgm/map/includes/MapIncludeImpl.java | 4 +- .../map/includes/MapIncludeProcessorImpl.java | 52 ++---- .../pgm/map/source/GitMapSourceFactory.java | 108 ++++++----- .../pgm/map/source/PathMapSourceFactory.java | 78 ++++---- .../map/source/StreamMapSourceFactory.java | 24 +++ .../tc/oc/pgm/map/source/SystemMapSource.java | 150 +++++++++++++++ .../map/source/SystemMapSourceFactory.java | 157 ---------------- .../tc/oc/pgm/match/MatchFactoryImpl.java | 6 +- .../main/java/tc/oc/pgm/match/MatchImpl.java | 15 +- .../main/java/tc/oc/pgm/util/StringUtils.java | 2 +- .../java/tc/oc/pgm/util/xml/XMLUtils.java | 9 + 26 files changed, 768 insertions(+), 552 deletions(-) create mode 100644 core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java create mode 100644 core/src/main/java/tc/oc/pgm/map/source/StreamMapSourceFactory.java create mode 100644 core/src/main/java/tc/oc/pgm/map/source/SystemMapSource.java delete mode 100644 core/src/main/java/tc/oc/pgm/map/source/SystemMapSourceFactory.java diff --git a/core/src/main/java/tc/oc/pgm/PGMConfig.java b/core/src/main/java/tc/oc/pgm/PGMConfig.java index 28378e9fb9..80691de40d 100644 --- a/core/src/main/java/tc/oc/pgm/PGMConfig.java +++ b/core/src/main/java/tc/oc/pgm/PGMConfig.java @@ -15,6 +15,9 @@ import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.Normalizer; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; @@ -23,9 +26,11 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.TreeSet; import java.util.logging.Level; +import java.util.stream.Collectors; import net.kyori.adventure.text.Component; import org.bukkit.ChatColor; import org.bukkit.configuration.ConfigurationSection; @@ -38,7 +43,7 @@ import tc.oc.pgm.api.Permissions; import tc.oc.pgm.api.map.factory.MapSourceFactory; import tc.oc.pgm.map.source.GitMapSourceFactory; -import tc.oc.pgm.map.source.SystemMapSourceFactory; +import tc.oc.pgm.map.source.PathMapSourceFactory; import tc.oc.pgm.util.bukkit.BukkitUtils; import tc.oc.pgm.util.text.TextException; @@ -56,8 +61,8 @@ public final class PGMConfig implements Config { // map.* private final List mapSourceFactories; - private final String mapPoolFile; - private final String includesDirectory; + private final Path mapPoolFile; + private final Path includesDirectory; // countdown.* private final Duration startTime; @@ -150,22 +155,11 @@ public final class PGMConfig implements Config { } for (String folder : folders) { - File folderFile = new File(folder); - this.mapSourceFactories.add( - new SystemMapSourceFactory( - folderFile.isAbsolute() ? folderFile : folderFile.getAbsoluteFile())); - } - - final String mapPoolFile = config.getString("map.pools"); - this.mapPoolFile = - mapPoolFile == null || mapPoolFile.isEmpty() - ? null - : new File(dataFolder, mapPoolFile).getAbsolutePath(); - final String includesDirectory = config.getString("map.includes"); - this.includesDirectory = - includesDirectory == null || includesDirectory.isEmpty() - ? null - : new File(dataFolder, includesDirectory).getAbsolutePath(); + this.mapSourceFactories.add(new PathMapSourceFactory(Paths.get(folder))); + } + + this.mapPoolFile = getPath(dataFolder.toPath(), config.getString("map.pools")); + this.includesDirectory = getPath(dataFolder.toPath(), config.getString("map.includes")); this.startTime = parseDuration(config.getString("countdown.start", "30s")); this.huddleTime = parseDuration(config.getString("countdown.huddle", "0s")); @@ -225,6 +219,12 @@ public final class PGMConfig implements Config { this.experiments = experiments == null ? ImmutableMap.of() : experiments.getValues(false); } + private Path getPath(Path base, String dir) { + if (dir == null || dir.isEmpty()) return null; + Path path = Paths.get(dir); + return path.isAbsolute() ? path : base.resolve(path); + } + public static final Map DEFAULT_REMOTE_REPO = ImmutableMap.of("uri", "https://github.com/PGMDev/Maps", "path", "default-maps"); @@ -238,31 +238,26 @@ public static void registerRemoteMapSource( } String path = String.valueOf(repository.get("path")); - final File folder; if (path.isEmpty() || path.equals("null")) { - folder = - new File("maps", (uri.getHost() + uri.getPath()).replaceAll("[/._]", "-").toLowerCase()); - } else { - folder = new File(path); + String normalizedPath = + Normalizer.normalize(uri.getHost() + uri.getPath(), Normalizer.Form.NFD) + .replaceAll("[^A-Za-z0-9_]", "-") + .toLowerCase(Locale.ROOT); + path = "maps" + File.pathSeparator + normalizedPath; } - mapSources.add(new GitMapSourceFactory(folder, uri, branch)); + Path base = Paths.get(path).toAbsolutePath(); - TreeSet folders = new TreeSet<>(); + // Set up a path filter, if needed + List children = null; final Object subFolders = repository.get("folders"); if (subFolders instanceof List) { - for (Object subFolder : (List) subFolders) { - folders.add(new File(folder, subFolder.toString())); - } - } else { - folders.add(folder); + children = + ((List) subFolders) + .stream().map(Object::toString).map(Paths::get).collect(Collectors.toList()); } - for (File folderFile : folders) { - mapSources.add( - new SystemMapSourceFactory( - folderFile.isAbsolute() ? folderFile : folderFile.getAbsoluteFile())); - } + mapSources.add(new GitMapSourceFactory(base, children, uri, branch)); } // TODO: Can be removed after 1.0 release @@ -469,12 +464,12 @@ public List getMapSourceFactories() { } @Override - public String getMapPoolFile() { + public Path getMapPoolFile() { return mapPoolFile; } @Override - public @Nullable String getIncludesDirectory() { + public @Nullable Path getIncludesDirectory() { return includesDirectory; } diff --git a/core/src/main/java/tc/oc/pgm/PGMPlugin.java b/core/src/main/java/tc/oc/pgm/PGMPlugin.java index 8da5a41b88..c36b9fd373 100644 --- a/core/src/main/java/tc/oc/pgm/PGMPlugin.java +++ b/core/src/main/java/tc/oc/pgm/PGMPlugin.java @@ -174,7 +174,7 @@ public void onEnable() { if (config.getMapPoolFile() != null) { MapPoolManager manager = - new MapPoolManager(logger, new File(config.getMapPoolFile()), datastore); + new MapPoolManager(logger, config.getMapPoolFile().toFile(), datastore); if (manager.getActiveMapPool() != null) mapOrder = manager; } if (mapOrder == null) mapOrder = new RandomMapOrder(Lists.newArrayList(mapLibrary.getMaps())); diff --git a/core/src/main/java/tc/oc/pgm/api/Config.java b/core/src/main/java/tc/oc/pgm/api/Config.java index aa5c4fc8c1..7773225963 100644 --- a/core/src/main/java/tc/oc/pgm/api/Config.java +++ b/core/src/main/java/tc/oc/pgm/api/Config.java @@ -6,6 +6,7 @@ import static net.kyori.adventure.text.event.ClickEvent.openUrl; import static net.kyori.adventure.text.event.HoverEvent.showText; +import java.nio.file.Path; import java.time.Duration; import java.util.List; import java.util.Map; @@ -58,7 +59,7 @@ public interface Config { * @return A path to a map pool, or null for no map pools. */ @Nullable - String getMapPoolFile(); + Path getMapPoolFile(); /** * Gets a path to the includes directory. @@ -66,7 +67,7 @@ public interface Config { * @return A path to the includes directory, or null for none. */ @Nullable - String getIncludesDirectory(); + Path getIncludesDirectory(); /** * Gets a duration to wait before starting a match. diff --git a/core/src/main/java/tc/oc/pgm/api/map/MapContext.java b/core/src/main/java/tc/oc/pgm/api/map/MapContext.java index da989a7069..b99436f6e6 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/MapContext.java +++ b/core/src/main/java/tc/oc/pgm/api/map/MapContext.java @@ -3,12 +3,12 @@ import tc.oc.pgm.api.module.ModuleContext; /** A {@link MapInfo} that is "loaded" with its {@link MapModule}s and a {@link MapSource}. */ -public interface MapContext extends MapInfo, ModuleContext { +public interface MapContext extends ModuleContext { /** - * Get a {@link MapSource} to access the maps's files. + * Get an immutable {@link MapInfo} which doesn't hold strong references to the context. * - * @return A {@link MapSource}. + * @return A {@link MapInfo} for this context. */ - MapSource getSource(); + MapInfo getInfo(); } diff --git a/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java b/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java index 711738a591..77a94e56ef 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java +++ b/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java @@ -19,6 +19,14 @@ public interface MapInfo extends Comparable, Cloneable { */ String getId(); + /** + * The map variant this info represents + * + * @return A variant for the map, if any. + */ + @Nullable + String getVariant(); + /** * Get the proto of the map's {@link org.jdom2.Document}. * @@ -157,11 +165,19 @@ default String getStyledNameLegacy(MapNameStyle style, @Nullable CommandSender s boolean getFriendlyFire(); /** - * Create an immutable copy of this info. + * Get a {@link MapSource} to access the maps's files. + * + * @return A {@link MapSource}. + */ + MapSource getSource(); + + /** + * Get the {@link MapContext} for this map, it may be null if the map unloaded * - * @return A cloned {@link MapInfo}. + * @return A {@link MapContext} for this map, or null if unloaded. */ - MapInfo clone(); + @Nullable + MapContext getContext(); @Override default int compareTo(MapInfo o) { diff --git a/core/src/main/java/tc/oc/pgm/api/map/MapSource.java b/core/src/main/java/tc/oc/pgm/api/map/MapSource.java index 04c4932e15..1b745f5fed 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/MapSource.java +++ b/core/src/main/java/tc/oc/pgm/api/map/MapSource.java @@ -2,13 +2,16 @@ import java.io.File; import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; +import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.map.exception.MapMissingException; import tc.oc.pgm.api.map.includes.MapInclude; /** A source where {@link MapInfo} documents and files are downloaded. */ public interface MapSource { - String FILE = "map.xml"; + Path FILE = Paths.get("map.xml"); /** * Get a unique identifier for the source, should be human-readable. @@ -17,6 +20,22 @@ public interface MapSource { */ String getId(); + /** + * The variant of the map this is for + * + * @return the variant the source, null for the parent source + */ + @Nullable + String getVariant(); + + /** + * A copy of the map source, tailored to a specific variant + * + * @param variant variant to use + * @return a new instance of map source, using the variant + */ + MapSource asVariant(String variant); + /** * Download the {@link org.bukkit.World} files to a local directory. * diff --git a/core/src/main/java/tc/oc/pgm/api/map/factory/MapFactory.java b/core/src/main/java/tc/oc/pgm/api/map/factory/MapFactory.java index 486b3dee4e..3b5e8a6333 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/factory/MapFactory.java +++ b/core/src/main/java/tc/oc/pgm/api/map/factory/MapFactory.java @@ -1,5 +1,6 @@ package tc.oc.pgm.api.map.factory; +import java.util.Collection; import tc.oc.pgm.api.map.MapContext; import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.map.MapModule; @@ -10,6 +11,7 @@ import tc.oc.pgm.kits.KitParser; import tc.oc.pgm.regions.RegionParser; import tc.oc.pgm.util.Version; +import tc.oc.pgm.util.xml.InvalidXMLException; /** A factory for creating {@link MapInfo}s and {@link MapContext}s. */ public interface MapFactory extends ModuleContext>, AutoCloseable { @@ -57,4 +59,11 @@ public interface MapFactory extends ModuleContext>, AutoCloseable { * @throws MapException If there was an error loading the context. */ MapContext load() throws MapException; + + /** + * Get the map variants found in the map + * + * @return a collection of map variants, empty if no variants exist + */ + Collection getVariants() throws InvalidXMLException; } diff --git a/core/src/main/java/tc/oc/pgm/api/map/factory/MapSourceFactory.java b/core/src/main/java/tc/oc/pgm/api/map/factory/MapSourceFactory.java index 3597ccb6fa..f447ee051e 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/factory/MapSourceFactory.java +++ b/core/src/main/java/tc/oc/pgm/api/map/factory/MapSourceFactory.java @@ -1,8 +1,9 @@ package tc.oc.pgm.api.map.factory; -import java.util.Iterator; +import java.util.function.Consumer; +import java.util.stream.Stream; import tc.oc.pgm.api.map.MapSource; -import tc.oc.pgm.api.map.exception.MapMissingException; +import tc.oc.pgm.api.map.exception.MapException; /** A thread-safe factory for creating {@link MapSource}s. */ public interface MapSourceFactory { @@ -13,11 +14,14 @@ public interface MapSourceFactory { *

It is the responsibility of the callee to keep a strong reference to each {@link MapSource} * it wants to keep, otherwise it will be garbage collected. * + * @param exceptionHandler place to send any exception when loading maps * @return An iterator of new {@link MapSource}s. - * @throws MapMissingException If there is an issue discovering. */ - Iterator loadNewSources() throws MapMissingException; + Stream loadNewSources(Consumer exceptionHandler); - /** Reset any caches so when {@link #loadNewSources()} is next called, it discovers everything. */ + /** + * Reset any caches so when {@link #loadNewSources(Consumer)} is next called, it discovers + * everything. + */ void reset(); } diff --git a/core/src/main/java/tc/oc/pgm/api/map/includes/MapInclude.java b/core/src/main/java/tc/oc/pgm/api/map/includes/MapInclude.java index d8c2762b4a..42ea0f4c22 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/includes/MapInclude.java +++ b/core/src/main/java/tc/oc/pgm/api/map/includes/MapInclude.java @@ -1,6 +1,6 @@ package tc.oc.pgm.api.map.includes; -import java.util.Collection; +import java.util.List; import org.jdom2.Content; /** Represents a snippet of XML that can be referenced for reuse * */ @@ -19,5 +19,5 @@ public interface MapInclude { * * @return a collection of {@link Content} */ - Collection getContent(); + List getContent(); } diff --git a/core/src/main/java/tc/oc/pgm/api/map/includes/MapIncludeProcessor.java b/core/src/main/java/tc/oc/pgm/api/map/includes/MapIncludeProcessor.java index dd24f7dd27..fcfbf8be22 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/includes/MapIncludeProcessor.java +++ b/core/src/main/java/tc/oc/pgm/api/map/includes/MapIncludeProcessor.java @@ -1,7 +1,8 @@ package tc.oc.pgm.api.map.includes; -import java.util.Collection; import org.jdom2.Document; +import org.jdom2.Element; +import org.jetbrains.annotations.Nullable; import tc.oc.pgm.util.xml.InvalidXMLException; /** A processor to determine which {@link MapInclude}s should be included when loading a map * */ @@ -10,11 +11,11 @@ public interface MapIncludeProcessor { /** * Process the given {@link Document} and return a collection of {@link MapInclude}s. * - * @param document A map document - * @return A collection of map includes, collection will be empty if none are found. + * @param element An include element within a map document + * @return element collection of map includes, collection will be empty if none are found. * @throws InvalidXMLException If the given document is not found or able to be parsed. */ - Collection getMapIncludes(Document document) throws InvalidXMLException; + MapInclude getMapInclude(Element element) throws InvalidXMLException; /** * Get a {@link MapInclude} by its id @@ -22,8 +23,17 @@ public interface MapIncludeProcessor { * @param includeId ID of the map include * @return A {@link MapInclude} */ + @Nullable MapInclude getMapIncludeById(String includeId); + /** + * Get a {@link MapInclude} that is global and applies to all maps + * + * @return A global {@link MapInclude} if any is present + */ + @Nullable + MapInclude getGlobalInclude(); + /** Reload the processor to fetch new map includes. */ void loadNewIncludes(); } diff --git a/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java b/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java index de9e1f620d..3a843490d0 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java @@ -4,51 +4,50 @@ import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; +import java.lang.ref.SoftReference; import java.util.Collection; import java.util.List; import tc.oc.pgm.api.map.MapContext; import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.map.MapModule; -import tc.oc.pgm.api.map.MapSource; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.ffa.FreeForAllModule; import tc.oc.pgm.teams.TeamFactory; import tc.oc.pgm.teams.TeamModule; -public class MapContextImpl extends MapInfoImpl implements MapContext { +public class MapContextImpl implements MapContext { private static final MapTag TERRAIN = new MapTag("terrain", "Terrain", false, true); - private final MapSource source; + private final MapInfo info; private final List modules; - public MapContextImpl(MapInfo info, MapSource source, Collection> modules) { - super(info); - this.source = assertNotNull(source); + public MapContextImpl(MapInfoImpl info, Collection> modules) { + info.context = new SoftReference<>(this); + this.info = info; this.modules = ImmutableList.copyOf(assertNotNull(modules)); - for (MapModule module : this.modules) { - this.tags.addAll(module.getTags()); + for (MapModule module : this.modules) { + info.tags.addAll(module.getTags()); if (module instanceof TeamModule) { - this.players.clear(); - this.players.addAll( + info.players.clear(); + info.players.addAll( Collections2.transform(((TeamModule) module).getTeams(), TeamFactory::getMaxPlayers)); } if (module instanceof FreeForAllModule) { - this.players.clear(); - this.players.add(((FreeForAllModule) module).getOptions().maxPlayers); + info.players.clear(); + info.players.add(((FreeForAllModule) module).getOptions().maxPlayers); } } - if (getWorld().hasTerrain()) { - this.tags.add(TERRAIN); + if (info.getWorld().hasTerrain()) { + info.tags.add(TERRAIN); } } - @Override - public MapSource getSource() { - return source; + public MapInfo getInfo() { + return info; } @Override diff --git a/core/src/main/java/tc/oc/pgm/map/MapFactoryImpl.java b/core/src/main/java/tc/oc/pgm/map/MapFactoryImpl.java index 9779b2c2c8..88fd6b0717 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapFactoryImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapFactoryImpl.java @@ -3,24 +3,21 @@ import static tc.oc.pgm.util.Assert.assertNotNull; import java.io.IOException; -import java.io.InputStream; import java.util.Collection; +import java.util.HashSet; +import java.util.Set; import java.util.logging.Logger; import org.jdom2.Document; -import org.jdom2.JDOMException; +import org.jdom2.Element; import org.jdom2.input.JDOMParseException; -import org.jdom2.input.SAXBuilder; import tc.oc.pgm.api.Modules; import tc.oc.pgm.api.map.MapContext; -import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.map.MapSource; import tc.oc.pgm.api.map.exception.MapException; -import tc.oc.pgm.api.map.exception.MapMissingException; import tc.oc.pgm.api.map.factory.MapFactory; import tc.oc.pgm.api.map.factory.MapModuleFactory; -import tc.oc.pgm.api.map.includes.MapInclude; import tc.oc.pgm.api.map.includes.MapIncludeProcessor; import tc.oc.pgm.api.module.ModuleGraph; import tc.oc.pgm.api.module.exception.ModuleLoadException; @@ -37,24 +34,16 @@ import tc.oc.pgm.util.ClassLogger; import tc.oc.pgm.util.Version; import tc.oc.pgm.util.xml.InvalidXMLException; -import tc.oc.pgm.util.xml.SAXHandler; +import tc.oc.pgm.util.xml.XMLUtils; public class MapFactoryImpl extends ModuleGraph, MapModuleFactory> implements MapFactory { - private static final ThreadLocal DOCUMENT_FACTORY = - ThreadLocal.withInitial( - () -> { - final SAXBuilder builder = new SAXBuilder(); - builder.setSAXHandlerFactory(SAXHandler.FACTORY); - return builder; - }); - private final Logger logger; private final MapSource source; private final MapIncludeProcessor includes; private Document document; - private MapInfo info; + private MapInfoImpl info; private RegionParser regions; private FilterParser filters; private KitParser kits; @@ -76,27 +65,12 @@ protected MapModule createModule(MapModuleFactory factory) throws ModuleLo } } - private void preLoad() - throws IOException, JDOMException, InvalidXMLException, MapMissingException { - try (final InputStream stream = source.getDocument()) { - document = DOCUMENT_FACTORY.get().build(stream); - document.setBaseURI(source.getId()); - } - - // Check for any included map sources, appending them to the document if present - Collection mapIncludes = includes.getMapIncludes(document); - for (MapInclude include : mapIncludes) { - document.getRootElement().addContent(0, include.getContent()); - } - source.setIncludes(mapIncludes); - - info = new MapInfoImpl(document.getRootElement()); - } - @Override public MapContext load() throws MapException { try { - preLoad(); + document = MapFilePreprocessor.getDocument(source, includes); + + info = new MapInfoImpl(source, document.getRootElement()); try { loadAll(); } catch (ModuleLoadException e) { @@ -121,7 +95,7 @@ public MapContext load() throws MapException { throw new MapException(source, info, "Unhandled " + t.getClass().getName(), t); } - return new MapContextImpl(info, source, getModules()); + return new MapContextImpl(info, getModules()); } private void postLoad() throws InvalidXMLException { @@ -178,6 +152,20 @@ public FeatureDefinitionContext getFeatures() { return features; } + @Override + public Collection getVariants() throws InvalidXMLException { + Set collect = new HashSet<>(); + for (Element variant : document.getRootElement().getChildren("variant")) { + String id = XMLUtils.parseRequiredId(variant); + if ("default".equals(id)) + throw new InvalidXMLException("Variant id must not be 'default'", variant); + + if (!collect.add(id)) + throw new InvalidXMLException("Duplicate variant ids are not allowed", variant); + } + return collect; + } + @Override public void close() { document = null; diff --git a/core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java b/core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java new file mode 100644 index 0000000000..c7312b96e4 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java @@ -0,0 +1,171 @@ +package tc.oc.pgm.map; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.jdom2.Attribute; +import org.jdom2.Content; +import org.jdom2.Document; +import org.jdom2.Element; +import org.jdom2.JDOMException; +import org.jdom2.Text; +import org.jdom2.input.SAXBuilder; +import tc.oc.pgm.api.map.MapSource; +import tc.oc.pgm.api.map.exception.MapMissingException; +import tc.oc.pgm.api.map.includes.MapInclude; +import tc.oc.pgm.api.map.includes.MapIncludeProcessor; +import tc.oc.pgm.util.xml.InvalidXMLException; +import tc.oc.pgm.util.xml.Node; +import tc.oc.pgm.util.xml.SAXHandler; +import tc.oc.pgm.util.xml.XMLUtils; + +public class MapFilePreprocessor { + + private static final ThreadLocal DOCUMENT_FACTORY = + ThreadLocal.withInitial( + () -> { + final SAXBuilder builder = new SAXBuilder(); + builder.setSAXHandlerFactory(SAXHandler.FACTORY); + return builder; + }); + + private static final Pattern CONSTANT_PATTERN = Pattern.compile("\\$\\{(.+)}"); + + private final MapIncludeProcessor includeProcessor; + private final MapSource source; + private final String variant; + private final List includes; + + private final Map constants; + + public static Document getDocument(MapSource source, MapIncludeProcessor includes) + throws MapMissingException, IOException, JDOMException, InvalidXMLException { + return new MapFilePreprocessor(source, includes).getDocument(); + } + + private MapFilePreprocessor(MapSource source, MapIncludeProcessor includeProcessor) { + this.source = source; + this.includeProcessor = includeProcessor; + this.variant = source.getVariant() == null ? "default" : source.getVariant(); + this.includes = new ArrayList<>(); + this.constants = new HashMap<>(); + } + + public Document getDocument() + throws MapMissingException, IOException, JDOMException, InvalidXMLException { + Document document; + try (final InputStream stream = source.getDocument()) { + document = DOCUMENT_FACTORY.get().build(stream); + document.setBaseURI(source.getId()); + } + + MapInclude global = includeProcessor.getGlobalInclude(); + if (global != null) { + document.getRootElement().addContent(0, global.getContent()); + includes.add(global); + } + + preprocessChildren(document.getRootElement()); + source.setIncludes(includes); + + for (Element constant : + XMLUtils.flattenElements(document.getRootElement(), "constants", "constant", 0)) { + constants.put(XMLUtils.parseRequiredId(constant), constant.getText()); + } + + // If no constants are set, assume we can skip the step + if (constants.size() > 0) { + postprocessChildren(document.getRootElement()); + } + + return document; + } + + private void preprocessChildren(Element parent) throws InvalidXMLException { + for (int i = 0; i < parent.getContentSize(); i++) { + Content content = parent.getContent(i); + if (!(content instanceof Element)) continue; + + Element child = (Element) content; + List replacement = null; + + switch (child.getName()) { + case "include": + replacement = processIncludeElement(child); + break; + case "if": + replacement = processConditional(child, true); + break; + case "unless": + replacement = processConditional(child, false); + break; + } + + if (replacement != null) { + parent.removeContent(i); + parent.addContent(i, replacement); + i--; // Process replacement content + } else { + preprocessChildren(child); + } + } + } + + private List processIncludeElement(Element element) throws InvalidXMLException { + MapInclude include = includeProcessor.getMapInclude(element); + if (include != null) { + includes.add(include); + return include.getContent(); + } + return Collections.emptyList(); + } + + private List processConditional(Element el, boolean shouldContain) + throws InvalidXMLException { + boolean contains = + Arrays.asList(Node.fromRequiredAttr(el, "variant").getValue().split("[\\\\s,]")) + .contains(variant); + + return contains == shouldContain ? el.cloneContent() : Collections.emptyList(); + } + + private void postprocessChildren(Element parent) throws InvalidXMLException { + for (Attribute attribute : parent.getAttributes()) { + attribute.setValue(postprocessString(parent, attribute.getValue())); + } + + for (int i = 0; i < parent.getContentSize(); i++) { + Content content = parent.getContent(i); + if (content instanceof Element) { + postprocessChildren((Element) content); + } else if (content instanceof Text) { + Text text = (Text) content; + text.setText(postprocessString(parent, text.getText())); + } + } + } + + private String postprocessString(Element el, String text) throws InvalidXMLException { + Matcher matcher = CONSTANT_PATTERN.matcher(text); + + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + String constant = matcher.group(1); + String replacement = constants.get(matcher.group(1)); + if (replacement == null) + throw new InvalidXMLException( + "No constant '" + constant + "' is used but has not been defined", el); + + matcher.appendReplacement(result, replacement); + } + matcher.appendTail(result); + return result.toString(); + } +} diff --git a/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java b/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java index 771d00fa0f..8ffb4da8a3 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java @@ -4,6 +4,7 @@ import static net.kyori.adventure.text.Component.translatable; import static tc.oc.pgm.util.Assert.assertNotNull; +import java.lang.ref.SoftReference; import java.time.LocalDate; import java.util.*; import java.util.stream.Collectors; @@ -13,10 +14,13 @@ import net.kyori.adventure.text.format.TextDecoration; import org.bukkit.Difficulty; import org.jdom2.Element; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.map.Contributor; import tc.oc.pgm.api.map.Gamemode; +import tc.oc.pgm.api.map.MapContext; import tc.oc.pgm.api.map.MapInfo; +import tc.oc.pgm.api.map.MapSource; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.Phase; import tc.oc.pgm.api.map.WorldInfo; @@ -32,7 +36,10 @@ import tc.oc.pgm.util.xml.XMLUtils; public class MapInfoImpl implements MapInfo { + private final MapSource source; + private final String id; + private final String variant; private final Version proto; private final Version version; private final Phase phase; @@ -52,91 +59,61 @@ public class MapInfoImpl implements MapInfo { protected final Collection players; protected final Collection gamemodes; - public MapInfoImpl( - @Nullable String id, - Version proto, - Version version, - String name, - String description, - @Nullable LocalDate created, - @Nullable Collection authors, - @Nullable Collection contributors, - @Nullable Collection rules, - @Nullable Integer difficulty, - @Nullable Collection tags, - @Nullable Collection players, - @Nullable WorldInfo world, - @Nullable Component gamemode, - @Nullable Collection gamemodes, - Phase phase, - @Nullable boolean friendlyFire) { - this.name = assertNotNull(name); - this.normalizedName = StringUtils.normalize(name); - this.id = assertNotNull(StringUtils.slugify(id == null ? name : id)); - this.proto = assertNotNull(proto); - this.version = assertNotNull(version); - this.description = assertNotNull(description); - this.created = created; - this.authors = authors == null ? new LinkedList<>() : authors; - this.contributors = contributors == null ? new LinkedList<>() : contributors; - this.rules = rules == null ? new LinkedList<>() : rules; - this.difficulty = difficulty == null ? Difficulty.NORMAL.ordinal() : difficulty; - this.tags = tags == null ? new TreeSet<>() : tags; - this.players = players == null ? new LinkedList<>() : players; - this.world = world == null ? new WorldInfoImpl() : world; - this.gamemode = gamemode; - this.gamemodes = gamemodes; - this.phase = phase; - this.friendlyFire = friendlyFire; - } + protected SoftReference context; - public MapInfoImpl(MapInfo info) { - this( - assertNotNull(info).getId(), - info.getProto(), - info.getVersion(), - info.getName(), - info.getDescription(), - info.getCreated(), - info.getAuthors(), - info.getContributors(), - info.getRules(), - info.getDifficulty(), - info.getTags(), - info.getMaxPlayers(), - info.getWorld(), - info.getGamemode(), - info.getGamemodes(), - info.getPhase(), - info.getFriendlyFire()); - } + public MapInfoImpl(MapSource source, Element root) throws InvalidXMLException { + this.source = source; + this.variant = source.getVariant(); + + String tmpName = assertNotNull(Node.fromRequiredChildOrAttr(root, "name").getValueNormalize()); + if (variant != null) { + Element variantEl = + root.getChildren("variant").stream() + .filter(el -> Objects.equals(variant, el.getAttributeValue("id"))) + .findFirst() + .orElseThrow( + () -> new InvalidXMLException("Could not find variant definition", root)); + + boolean override = XMLUtils.parseBoolean(Node.fromAttr(variantEl, "override"), false); + tmpName = (override ? "" : tmpName + ": ") + variantEl.getTextNormalize(); + } + + this.name = tmpName; + this.normalizedName = StringUtils.normalize(name); - public MapInfoImpl(Element root) throws InvalidXMLException { - this( - assertNotNull(root).getChildTextNormalize("slug"), - XMLUtils.parseSemanticVersion(Node.fromRequiredAttr(root, "proto")), - XMLUtils.parseSemanticVersion(Node.fromRequiredChildOrAttr(root, "version")), - Node.fromRequiredChildOrAttr(root, "name").getValueNormalize(), - Node.fromRequiredChildOrAttr(root, "objective", "description").getValueNormalize(), - XMLUtils.parseDate(Node.fromChildOrAttr(root, "created")), - parseContributors(root, "author"), - parseContributors(root, "contributor"), - parseRules(root), + String slug = assertNotNull(root).getChildTextNormalize("slug"); + if (slug != null && variant != null) slug += "_" + variant; + + this.id = assertNotNull(StringUtils.slugify(slug != null ? slug : name)); + + this.proto = assertNotNull(XMLUtils.parseSemanticVersion(Node.fromRequiredAttr(root, "proto"))); + this.version = + assertNotNull(XMLUtils.parseSemanticVersion(Node.fromRequiredChildOrAttr(root, "version"))); + this.description = + assertNotNull( + Node.fromRequiredChildOrAttr(root, "objective", "description").getValueNormalize()); + this.created = XMLUtils.parseDate(Node.fromChildOrAttr(root, "created")); + this.authors = parseContributors(root, "author"); + this.contributors = parseContributors(root, "contributor"); + this.rules = parseRules(root); + this.difficulty = XMLUtils.parseEnum( Node.fromLastChildOrAttr(root, "difficulty"), Difficulty.class, "difficulty", Difficulty.NORMAL) - .ordinal(), - null, - null, - parseWorld(root), - XMLUtils.parseFormattedText(root, "game"), - parseGamemodes(root), + .ordinal(); + this.tags = new TreeSet<>(); + this.players = new ArrayList<>(); + this.world = parseWorld(root); + this.gamemode = XMLUtils.parseFormattedText(root, "game"); + this.gamemodes = parseGamemodes(root); + this.phase = XMLUtils.parseEnum( - Node.fromLastChildOrAttr(root, "phase"), Phase.class, "phase", Phase.PRODUCTION), + Node.fromLastChildOrAttr(root, "phase"), Phase.class, "phase", Phase.PRODUCTION); + this.friendlyFire = XMLUtils.parseBoolean( - Node.fromLastChildOrAttr(root, "friendlyfire", "friendly-fire"), false)); + Node.fromLastChildOrAttr(root, "friendlyfire", "friendly-fire"), false); } @Override @@ -144,6 +121,11 @@ public String getId() { return id; } + @Override + public String getVariant() { + return variant; + } + @Override public Version getProto() { return proto; @@ -246,8 +228,13 @@ public String toString() { } @Override - public MapInfo clone() { - return new MapInfoImpl(this); + public MapSource getSource() { + return source; + } + + @Override + public @Nullable MapContext getContext() { + return context != null ? context.get() : null; } @Override @@ -271,20 +258,17 @@ public Component getStyledName(MapNameStyle style) { return name.build(); } - private static List parseRules(Element root) { - List rules = null; + private static @NotNull List parseRules(Element root) { + List rules = new ArrayList<>(); for (Element parent : root.getChildren("rules")) { for (Element rule : parent.getChildren("rule")) { - if (rules == null) { - rules = new LinkedList<>(); - } rules.add(rule.getTextNormalize()); } } return rules; } - private static List parseGamemodes(Element root) throws InvalidXMLException { + private static @NotNull List parseGamemodes(Element root) throws InvalidXMLException { List gamemodes = new ArrayList<>(); for (Element gamemodeEl : root.getChildren("gamemode")) { Gamemode gm = Gamemode.byId(gamemodeEl.getText()); @@ -294,9 +278,9 @@ private static List parseGamemodes(Element root) throws InvalidXMLExce return gamemodes; } - private static List parseContributors(Element root, String tag) + private static @NotNull List parseContributors(Element root, String tag) throws InvalidXMLException { - List contributors = null; + List contributors = new ArrayList<>(); for (Element parent : root.getChildren(tag + "s")) { for (Element child : parent.getChildren(tag)) { String name = XMLUtils.getNormalizedNullableText(child); @@ -307,10 +291,6 @@ private static List parseContributors(Element root, String tag) throw new InvalidXMLException("Contributor must have either a name or UUID", child); } - if (contributors == null) { - contributors = new LinkedList<>(); - } - if (uuid == null) { contributors.add(new PseudonymContributor(name, contribution)); } else { @@ -321,7 +301,7 @@ private static List parseContributors(Element root, String tag) return contributors; } - private static WorldInfo parseWorld(Element root) throws InvalidXMLException { + private static @NotNull WorldInfo parseWorld(Element root) throws InvalidXMLException { final Element world = root.getChild("terrain"); return world == null ? new WorldInfoImpl() : new WorldInfoImpl(world); } diff --git a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java index 00714596f3..79f78a7ab8 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java @@ -2,14 +2,11 @@ import static tc.oc.pgm.util.Assert.assertNotNull; -import com.google.common.collect.Iterators; -import java.lang.ref.SoftReference; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.concurrent.CompletableFuture; @@ -28,8 +25,8 @@ import tc.oc.pgm.api.map.factory.MapFactory; import tc.oc.pgm.api.map.factory.MapSourceFactory; import tc.oc.pgm.api.map.includes.MapIncludeProcessor; +import tc.oc.pgm.map.source.StreamMapSourceFactory; import tc.oc.pgm.util.LiquidMetal; -import tc.oc.pgm.util.StreamUtils; import tc.oc.pgm.util.StringUtils; import tc.oc.pgm.util.UsernameResolver; @@ -37,22 +34,10 @@ public class MapLibraryImpl implements MapLibrary { private final Logger logger; private final List factories; - private final SortedMap maps; + private final SortedMap maps; private final Set failed; private final MapIncludeProcessor includes; - private static class MapEntry { - private final MapSource source; - private final MapInfo info; - private final SoftReference context; - - private MapEntry(MapSource source, MapInfo info, MapContext context) { - this.source = assertNotNull(source); - this.info = assertNotNull(info); - this.context = new SoftReference<>(assertNotNull(context)); - } - } - public MapLibraryImpl( Logger logger, List factories, MapIncludeProcessor includes) { this.logger = assertNotNull(logger); // Logger should be visible in-game @@ -66,20 +51,20 @@ public MapLibraryImpl( public MapInfo getMap(String idOrName) { // Exact match - MapEntry map = maps.get(StringUtils.slugify(idOrName)); + MapInfo map = maps.get(StringUtils.slugify(idOrName)); if (map == null) { // Fuzzy match map = StringUtils.bestFuzzyMatch( - StringUtils.normalize(idOrName), maps.values(), m -> m.info.getNormalizedName()); + StringUtils.normalize(idOrName), maps.values(), MapInfo::getNormalizedName); } - return map == null ? null : map.info; + return map; } @Override public Stream getMaps(@Nullable String query) { - Stream maps = this.maps.values().stream().map(e -> e.info); + Stream maps = this.maps.values().stream(); if (query != null) { String normalized = StringUtils.normalize(query); maps = maps.filter(mi -> LiquidMetal.match(mi.getNormalizedName(), normalized)); @@ -135,52 +120,51 @@ public CompletableFuture loadNewMaps(boolean reset) { // Try to search new includes before searching for new maps includes.loadNewIncludes(); - final List> sources = new LinkedList<>(); + final List sources = new LinkedList<>(); // Reload failed maps if (reset) { failed.clear(); } else { - sources.add(failed.iterator()); + sources.add(new StreamMapSourceFactory(failed.stream())); } - final int fail = failed.size(); - final int ok = reset ? 0 : maps.size(); + final int oldFail = failed.size(); + final int oldOk = reset ? 0 : maps.size(); // Discover new maps - final Iterator factories = this.factories.listIterator(); - while (factories.hasNext()) { - final MapSourceFactory factory = factories.next(); - try { - if (reset) factory.reset(); - sources.add(factory.loadNewSources()); - } catch (MapMissingException e) { - factories.remove(); - logMapError(e); - } + for (MapSourceFactory factory : this.factories) { + if (reset) factory.reset(); + sources.add(factory); } - // Reload existing maps that have updates - final Iterator> maps = this.maps.entrySet().iterator(); - while (maps.hasNext()) { - final MapEntry entry = maps.next().getValue(); - try { - if (reset || entry.source.checkForUpdates()) { - sources.add(Iterators.singletonIterator(entry.source)); - } - } catch (MapMissingException e) { - maps.remove(); - logMapError(e); - } - } + // Reload or delete existing maps that have updates + sources.add( + new StreamMapSourceFactory( + this.maps.entrySet().stream() + .filter( + entry -> { + try { + return reset || entry.getValue().getSource().checkForUpdates(); + } catch (MapMissingException e) { + logMapError(e); + this.maps.remove(entry.getKey()); + return false; + } + }) + .map(entry -> entry.getValue().getSource()))); return CompletableFuture.runAsync( - () -> - StreamUtils.of(Iterators.concat(sources.iterator())) - .parallel() - .unordered() - .forEach(source -> loadMapSafe(source, null))) - .thenRunAsync(() -> logMapSuccess(fail, ok)) + () -> { + try (Stream stream = + sources.stream() + .flatMap(s -> s.loadNewSources(this::logMapError)) + .parallel() + .unordered()) { + stream.forEach(s -> this.loadMapSafe(s, null)); + } + }) + .thenRunAsync(() -> logMapSuccess(oldFail, oldOk)) .thenRunAsync(UsernameResolver::resolveAll); } @@ -188,25 +172,25 @@ public CompletableFuture loadNewMaps(boolean reset) { public CompletableFuture loadExistingMap(String id) { return CompletableFuture.supplyAsync( () -> { - final MapEntry entry = maps.get(id); - if (entry == null) { + final MapInfo info = maps.get(id); + if (info == null) { throw new RuntimeException( new MapMissingException(id, "Unable to find map from id (was it deleted?)")); } - final MapContext context = entry.context.get(); + final MapContext context = info.getContext(); try { - if (context != null && !entry.source.checkForUpdates()) { + if (context != null && !info.getSource().checkForUpdates()) { return context; } } catch (MapMissingException e) { - failed.remove(entry.source); + failed.remove(info.getSource()); maps.remove(id); throw new RuntimeException(e); } logger.info(ChatColor.GREEN + "XML changes detected, reloading"); - return loadMapSafe(entry.source, entry.info.getId()); + return loadMapSafe(info.getSource(), info.getId()); }); } @@ -214,6 +198,14 @@ private MapContext loadMap(MapSource source, @Nullable String mapId) throws MapE final MapContext context; try (final MapFactory factory = new MapFactoryImpl(logger, source, includes)) { context = factory.load(); + + // We're not loading a specific map id, and we're not on a variant, load variants + if (mapId == null && source.getVariant() == null) { + for (String variant : factory.getVariants()) { + loadMapSafe(source.asVariant(variant), null); + } + } + } catch (MapMissingException e) { failed.remove(source); if (mapId != null) maps.remove(mapId); @@ -229,10 +221,9 @@ private MapContext loadMap(MapSource source, @Nullable String mapId) throws MapE t.getCause()); } + MapInfo info = context.getInfo(); maps.merge( - context.getId(), - new MapEntry(source, context.clone(), context), - (m1, m2) -> m2.info.getVersion().isOlderThan(m1.info.getVersion()) ? m1 : m2); + info.getId(), info, (m1, m2) -> m2.getVersion().isOlderThan(m1.getVersion()) ? m1 : m2); failed.remove(source); return context; diff --git a/core/src/main/java/tc/oc/pgm/map/includes/MapIncludeImpl.java b/core/src/main/java/tc/oc/pgm/map/includes/MapIncludeImpl.java index feaa0df625..a3d003f9bd 100644 --- a/core/src/main/java/tc/oc/pgm/map/includes/MapIncludeImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/includes/MapIncludeImpl.java @@ -5,7 +5,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.util.Collection; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import org.jdom2.Content; @@ -38,7 +38,7 @@ private void reload() throws MapMissingException, JDOMException, IOException { } @Override - public Collection getContent() { + public List getContent() { if (getLastModified() > lastRead.get()) { try { reload(); diff --git a/core/src/main/java/tc/oc/pgm/map/includes/MapIncludeProcessorImpl.java b/core/src/main/java/tc/oc/pgm/map/includes/MapIncludeProcessorImpl.java index fcf102bc4f..515b36d4b0 100644 --- a/core/src/main/java/tc/oc/pgm/map/includes/MapIncludeProcessorImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/includes/MapIncludeProcessorImpl.java @@ -1,20 +1,17 @@ package tc.oc.pgm.map.includes; import com.google.common.collect.Maps; -import com.google.common.collect.Sets; import java.io.File; import java.io.IOException; -import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import org.jdom2.Document; import org.jdom2.Element; import org.jdom2.JDOMException; import org.jdom2.input.SAXBuilder; +import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.Config; import tc.oc.pgm.api.PGM; import tc.oc.pgm.api.map.exception.MapMissingException; @@ -48,41 +45,28 @@ public MapInclude getGlobalInclude() { } @Override - public MapInclude getMapIncludeById(String includeId) { + public @Nullable MapInclude getMapIncludeById(String includeId) { return includes.get(includeId); } @Override - public Collection getMapIncludes(Document document) throws InvalidXMLException { - Set mapIncludes = Sets.newHashSet(); - - // Always add global include if present - if (getGlobalInclude() != null) { - mapIncludes.add(getGlobalInclude()); + public MapInclude getMapInclude(Element element) throws InvalidXMLException { + if (Node.fromAttr(element, "src") != null) { + // Send a warning to legacy include statements without preventing them from loading + logger.warning( + "[" + + element.getDocument().getBaseURI() + + "] " + + "Legacy include statements are no longer supported, please upgrade to the format."); + return null; } - List elements = document.getRootElement().getChildren("include"); - for (Element element : elements) { - - if (Node.fromAttr(element, "src") != null) { - // Send a warning to legacy include statements without preventing them from loading - logger.warning( - "[" - + document.getBaseURI() - + "] " - + "Legacy include statements are no longer supported, please upgrade to the format."); - continue; - } - - String id = XMLUtils.getRequiredAttribute(element, "id").getValue(); - MapInclude include = getMapIncludeById(id); - if (include == null) - throw new InvalidXMLException( - "The provided include id '" + id + "' could not be found!", element); - - mapIncludes.add(include); - } - return mapIncludes; + String id = XMLUtils.getRequiredAttribute(element, "id").getValue(); + MapInclude include = getMapIncludeById(id); + if (include == null) + throw new InvalidXMLException( + "The provided include id '" + id + "' could not be found!", element); + return include; } @Override @@ -90,7 +74,7 @@ public void loadNewIncludes() { Config config = PGM.get().getConfiguration(); if (config.getIncludesDirectory() == null) return; - File includeFiles = new File(config.getIncludesDirectory()); + File includeFiles = config.getIncludesDirectory().toFile(); if (!includeFiles.isDirectory()) { logger.warning(config.getIncludesDirectory() + " is not a directory!"); return; diff --git a/core/src/main/java/tc/oc/pgm/map/source/GitMapSourceFactory.java b/core/src/main/java/tc/oc/pgm/map/source/GitMapSourceFactory.java index 16d976279e..d703e1929d 100644 --- a/core/src/main/java/tc/oc/pgm/map/source/GitMapSourceFactory.java +++ b/core/src/main/java/tc/oc/pgm/map/source/GitMapSourceFactory.java @@ -3,13 +3,14 @@ import static tc.oc.pgm.util.text.TextException.invalidFormat; import static tc.oc.pgm.util.text.TextException.unknown; -import com.google.common.collect.Iterators; -import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.file.Path; import java.util.Collections; -import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; import java.util.logging.Level; +import java.util.stream.Stream; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.MergeCommand; @@ -22,15 +23,18 @@ import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.PGM; import tc.oc.pgm.api.map.MapSource; +import tc.oc.pgm.api.map.exception.MapException; import tc.oc.pgm.api.map.exception.MapMissingException; -import tc.oc.pgm.api.map.factory.MapSourceFactory; -public class GitMapSourceFactory implements MapSourceFactory { +public class GitMapSourceFactory extends PathMapSourceFactory { - private final Git git; private final CredentialsProvider credentials; + private final Git git; + + public GitMapSourceFactory( + Path base, @Nullable List children, URI uri, @Nullable String branch) { + super(base, children); - public GitMapSourceFactory(File file, URI uri, @Nullable String branch) { final String user = uri.getUserInfo(); if (user != null) { final String[] parts = user.split(":", 2); @@ -39,61 +43,65 @@ public GitMapSourceFactory(File file, URI uri, @Nullable String branch) { credentials = new ChainingCredentialsProvider(); } - Git git; - try (Git jgit = Git.open(file)) { - git = jgit; - } catch (RepositoryNotFoundException e) { - final CloneCommand clone = - Git.cloneRepository() - .setDirectory(file) - .setURI(uri.toString()) - .setCredentialsProvider(credentials) - .setCloneAllBranches(false) - .setCloneSubmodules(true); - - if (branch != null) { - clone.setBranch("refs/heads/" + branch); - clone.setBranchesToClone(Collections.singleton("refs/heads/" + branch)); - } - - PGM.get() - .getGameLogger() - .log( - Level.INFO, - String.format( - "Cloning %s%s on %s branch... (this may take a while)", - uri.getHost(), uri.getPath(), (branch == null ? "the default" : branch))); - - try { - git = clone.call(); - } catch (InvalidRemoteException e1) { - throw invalidFormat(uri.toString(), URI.class, e1); - } catch (GitAPIException e2) { - throw unknown(e2); - } - } catch (IOException e) { - throw unknown(e); - } - this.git = git; + this.git = openRepo(uri, branch); } @Override - public Iterator loadNewSources() throws MapMissingException { + public Stream loadNewSources(Consumer exceptionHandler) { try { git.pull() .setCredentialsProvider(credentials) .setFastForward(MergeCommand.FastForwardMode.FF) .call(); } catch (GitAPIException e) { - throw new MapMissingException( - git.getRepository().getDirectory().getPath(), e.getMessage(), e.getCause()); + exceptionHandler.accept( + new MapMissingException( + git.getRepository().getDirectory().getPath(), e.getMessage(), e.getCause())); } - return Iterators.emptyIterator(); + return super.loadNewSources(exceptionHandler); } - @Override - public void reset() { - // No-op + private Git openRepo(URI uri, @Nullable String branch) { + try (Git jgit = Git.open(base.toFile())) { + return jgit; + } catch (RepositoryNotFoundException e) { + return cloneRepo(uri, branch); + } catch (IOException e) { + throw unknown(e); + } + } + + private Git cloneRepo(URI uri, @Nullable String branch) { + Git git; + final CloneCommand clone = + Git.cloneRepository() + .setDirectory(base.toFile()) + .setURI(uri.toString()) + .setCredentialsProvider(credentials) + .setCloneAllBranches(false) + .setCloneSubmodules(true); + + if (branch != null) { + clone.setBranch("refs/heads/" + branch); + clone.setBranchesToClone(Collections.singleton("refs/heads/" + branch)); + } + + PGM.get() + .getGameLogger() + .log( + Level.INFO, + String.format( + "Cloning %s%s on %s branch... (this may take a while)", + uri.getHost(), uri.getPath(), (branch == null ? "the default" : branch))); + + try { + git = clone.call(); + } catch (InvalidRemoteException e1) { + throw invalidFormat(uri.toString(), URI.class, e1); + } catch (GitAPIException e2) { + throw unknown(e2); + } + return git; } } diff --git a/core/src/main/java/tc/oc/pgm/map/source/PathMapSourceFactory.java b/core/src/main/java/tc/oc/pgm/map/source/PathMapSourceFactory.java index f661bbca1e..1b15ba50f9 100644 --- a/core/src/main/java/tc/oc/pgm/map/source/PathMapSourceFactory.java +++ b/core/src/main/java/tc/oc/pgm/map/source/PathMapSourceFactory.java @@ -3,57 +3,71 @@ import static tc.oc.pgm.util.Assert.assertNotNull; import java.io.IOException; -import java.lang.ref.WeakReference; -import java.util.Iterator; -import java.util.NavigableMap; -import java.util.concurrent.ConcurrentSkipListMap; +import java.nio.file.FileVisitOption; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.function.Consumer; import java.util.stream.Stream; import org.jetbrains.annotations.Nullable; import tc.oc.pgm.api.map.MapSource; +import tc.oc.pgm.api.map.exception.MapException; import tc.oc.pgm.api.map.exception.MapMissingException; import tc.oc.pgm.api.map.factory.MapSourceFactory; -public abstract class PathMapSourceFactory implements MapSourceFactory { +public class PathMapSourceFactory implements MapSourceFactory { - // Sources are held with weak reference because the callee is - // responsible for holding a strong reference. - private final NavigableMap> sources; - private final String path; + private final Set sources; - protected PathMapSourceFactory(String path) { - this.sources = new ConcurrentSkipListMap<>(); - this.path = assertNotNull(path); + protected final Path base; + protected final @Nullable List children; + + public PathMapSourceFactory(Path base) { + this(base, null); } - protected abstract MapSource loadSource(String dir); + public PathMapSourceFactory(Path base, @Nullable List children) { + this.sources = Collections.synchronizedSet(new ConcurrentSkipListSet<>()); + this.base = assertNotNull(base).toAbsolutePath(); + this.children = children; + } - private Stream loadNewSource(@Nullable String path) { - if (path == null || !path.startsWith(this.path) || sources.containsKey(path)) { - return Stream.empty(); - } + protected MapSource loadSource(Path path) { + return new SystemMapSource(path, null); + } - final int index = path.indexOf(MapSource.FILE); - if (index < 1) { + private Stream loadNewSource(Path path) { + if (path == null || !path.startsWith(this.base) || !sources.add(path)) { return Stream.empty(); } - final MapSource source = loadSource(path.substring(0, index - 1)); - sources.put(path, new WeakReference<>(source)); - return Stream.of(source); + if (path.getFileName().equals(MapSource.FILE)) { + return Stream.of(loadSource(path.getParent())); + } + return Stream.empty(); } - protected abstract Stream loadAllPaths() throws IOException; - @Override - public Iterator loadNewSources() throws MapMissingException { - final Stream paths; - try { - paths = loadAllPaths(); - } catch (IOException e) { - throw new MapMissingException(path, "Unable to list files", e); - } + public Stream loadNewSources(Consumer exceptionHandler) { + if (!Files.exists(base) || !Files.isDirectory(base)) return Stream.empty(); - return paths.parallel().flatMap(this::loadNewSource).iterator(); + return (children == null ? Stream.of(base) : children.stream().map(base::resolve)) + .flatMap( + b -> { + try { + return Files.walk(b, FileVisitOption.FOLLOW_LINKS); + } catch (IOException e) { + exceptionHandler.accept( + new MapMissingException(b.toString(), "Unable to list files", e)); + return Stream.empty(); + } + }) + .map(Path::toAbsolutePath) + .parallel() + .flatMap(this::loadNewSource); } @Override diff --git a/core/src/main/java/tc/oc/pgm/map/source/StreamMapSourceFactory.java b/core/src/main/java/tc/oc/pgm/map/source/StreamMapSourceFactory.java new file mode 100644 index 0000000000..068774fb46 --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/map/source/StreamMapSourceFactory.java @@ -0,0 +1,24 @@ +package tc.oc.pgm.map.source; + +import java.util.function.Consumer; +import java.util.stream.Stream; +import tc.oc.pgm.api.map.MapSource; +import tc.oc.pgm.api.map.exception.MapException; +import tc.oc.pgm.api.map.factory.MapSourceFactory; + +public class StreamMapSourceFactory implements MapSourceFactory { + + private final Stream sources; + + public StreamMapSourceFactory(Stream sources) { + this.sources = sources; + } + + @Override + public Stream loadNewSources(Consumer exceptionHandler) { + return sources; + } + + @Override + public void reset() {} +} diff --git a/core/src/main/java/tc/oc/pgm/map/source/SystemMapSource.java b/core/src/main/java/tc/oc/pgm/map/source/SystemMapSource.java new file mode 100644 index 0000000000..56253fda8b --- /dev/null +++ b/core/src/main/java/tc/oc/pgm/map/source/SystemMapSource.java @@ -0,0 +1,150 @@ +package tc.oc.pgm.map.source; + +import static tc.oc.pgm.util.Assert.assertNotNull; + +import com.google.common.collect.Sets; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.api.map.MapSource; +import tc.oc.pgm.api.map.exception.MapMissingException; +import tc.oc.pgm.api.map.includes.MapInclude; +import tc.oc.pgm.util.FileUtils; + +class SystemMapSource implements MapSource { + + private final Path dir; + private final String variant; + private final AtomicLong lastRead; + private final Set storedIncludes; + + public SystemMapSource(Path dir, @Nullable String variant) { + this.dir = assertNotNull(dir); + this.variant = variant; + this.lastRead = new AtomicLong(-1); + this.storedIncludes = Sets.newHashSet(); + } + + private File getDirectory() throws MapMissingException { + final File dir = this.dir.toFile(); + + if (!dir.exists()) { + throw new MapMissingException(dir.getPath(), "Unable to find map folder (was it moved?)"); + } + + if (!dir.isDirectory()) { + throw new MapMissingException( + dir.getPath(), "Unable to read map folder (is it a directory?)"); + } + + return dir; + } + + private File getFile() throws MapMissingException { + final File file = dir.resolve(MapSource.FILE).toFile(); + + if (!file.exists()) { + throw new MapMissingException(file.getPath(), "Unable to find map document (was it moved?)"); + } + + if (!file.isFile()) { + throw new MapMissingException(file.getPath(), "Unable to read map document (is it a file?)"); + } + + if (!file.canRead()) { + throw new MapMissingException( + file.getPath(), "Unable to read map document (file permissions issue?)"); + } + + return file; + } + + @Override + public String getId() { + return dir.toString(); + } + + @Override + public void downloadTo(File dst) throws MapMissingException { + final File src = getDirectory(); + try { + FileUtils.copy(src, dst, true); + } catch (IOException e) { + throw new MapMissingException(dir.toString(), "Unable to copy map folder", e); + } + } + + @Override + public InputStream getDocument() throws MapMissingException { + final File file = getFile(); + try { + return new FileInputStream(file); + } catch (FileNotFoundException e) { + throw new MapMissingException(file.getPath(), "Unable to read map document", e); + } finally { + lastRead.set(System.currentTimeMillis()); + } + } + + @Override + public boolean checkForUpdates() throws MapMissingException { + final File file = getFile(); + + final long read = lastRead.get(); + if (read <= 0) return false; + if (file.lastModified() > read) return true; + + for (MapInclude include : storedIncludes) { + if (include.getLastModified() > read) return true; + } + return false; + } + + @Override + public String getVariant() { + return variant; + } + + @Override + public MapSource asVariant(String variant) { + if (Objects.equals(variant, this.variant)) return this; + return new SystemMapSource(dir, variant); + } + + @Override + public int hashCode() { + return Objects.hash(dir, variant); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SystemMapSource)) return false; + SystemMapSource other = (SystemMapSource) obj; + return dir.equals(other.dir) && Objects.equals(variant, other.variant); + } + + @Override + public String toString() { + return "SystemMapSource{dir=" + + this.dir + + ", variant=" + + this.variant + + ", lastRead=" + + this.lastRead.get() + + "}"; + } + + @Override + public void setIncludes(Collection includes) { + this.storedIncludes.clear(); + this.storedIncludes.addAll(includes); + } +} diff --git a/core/src/main/java/tc/oc/pgm/map/source/SystemMapSourceFactory.java b/core/src/main/java/tc/oc/pgm/map/source/SystemMapSourceFactory.java deleted file mode 100644 index 0d8ee1abbd..0000000000 --- a/core/src/main/java/tc/oc/pgm/map/source/SystemMapSourceFactory.java +++ /dev/null @@ -1,157 +0,0 @@ -package tc.oc.pgm.map.source; - -import static tc.oc.pgm.util.Assert.assertNotNull; - -import com.google.common.collect.Sets; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.FileVisitOption; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collection; -import java.util.Set; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Stream; -import tc.oc.pgm.api.map.MapSource; -import tc.oc.pgm.api.map.exception.MapMissingException; -import tc.oc.pgm.api.map.includes.MapInclude; -import tc.oc.pgm.util.FileUtils; - -public class SystemMapSourceFactory extends PathMapSourceFactory { - - protected final File dir; - - public SystemMapSourceFactory(File file) { - super(file.getPath()); - this.dir = file; - } - - @Override - protected MapSource loadSource(String dir) { - return new SystemMapSource(dir); - } - - @Override - protected Stream loadAllPaths() throws IOException { - if (!dir.exists() || !dir.isDirectory()) return Stream.empty(); - - return Files.walk(dir.toPath(), FileVisitOption.FOLLOW_LINKS) - .map(Path::toAbsolutePath) - .map(Path::toString); - } - - protected static class SystemMapSource implements MapSource { - - private final String dir; - private final AtomicLong lastRead; - private final Set storedIncludes; - - private SystemMapSource(String dir) { - this.dir = assertNotNull(dir); - this.lastRead = new AtomicLong(-1); - this.storedIncludes = Sets.newHashSet(); - } - - private File getDirectory() throws MapMissingException { - final File dir = new File(this.dir); - - if (!dir.exists()) { - throw new MapMissingException(dir.getPath(), "Unable to find map folder (was it moved?)"); - } - - if (!dir.isDirectory()) { - throw new MapMissingException( - dir.getPath(), "Unable to read map folder (is it a directory?)"); - } - - return dir; - } - - private File getFile() throws MapMissingException { - final File file = new File(dir, MapSource.FILE); - - if (!file.exists()) { - throw new MapMissingException( - file.getPath(), "Unable to find map document (was it moved?)"); - } - - if (!file.isFile()) { - throw new MapMissingException( - file.getPath(), "Unable to read map document (is it a file?)"); - } - - if (!file.canRead()) { - throw new MapMissingException( - file.getPath(), "Unable to read map document (file permissions issue?)"); - } - - return file; - } - - @Override - public String getId() { - return dir; - } - - @Override - public void downloadTo(File dst) throws MapMissingException { - final File src = getDirectory(); - try { - FileUtils.copy(src, dst, true); - } catch (IOException e) { - throw new MapMissingException(dir, "Unable to copy map folder", e); - } - } - - @Override - public InputStream getDocument() throws MapMissingException { - final File file = getFile(); - try { - return new FileInputStream(file); - } catch (FileNotFoundException e) { - throw new MapMissingException(file.getPath(), "Unable to read map document", e); - } finally { - lastRead.set(System.currentTimeMillis()); - } - } - - @Override - public boolean checkForUpdates() throws MapMissingException { - final File file = getFile(); - - final long read = lastRead.get(); - if (read <= 0) return false; - if (file.lastModified() > read) return true; - - for (MapInclude include : storedIncludes) { - if (include.getLastModified() > read) return true; - } - return false; - } - - @Override - public int hashCode() { - return dir.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof SystemMapSource)) return false; - return dir.equals(((SystemMapSource) obj).dir); - } - - @Override - public String toString() { - return "SystemMapSourceFactory{dir=" + this.dir + ", lastRead=" + this.lastRead.get() + "}"; - } - - @Override - public void setIncludes(Collection includes) { - this.storedIncludes.clear(); - this.storedIncludes.addAll(includes); - } - } -} diff --git a/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java index 0c11f05ffa..ef1e0e6ab1 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java @@ -230,7 +230,7 @@ private InitWorldStage advanceSync() throws MapMissingException { final File dir = getDirectory(); if (dir.mkdirs()) { - map.getSource().downloadTo(dir); + map.getInfo().getSource().downloadTo(dir); } else { throw new MapMissingException(dir.getPath(), "Unable to mkdirs world directory"); } @@ -261,7 +261,7 @@ private InitWorldStage(MapContext map, String worldName) { } private Stage advanceSync() throws IllegalStateException { - final WorldInfo info = map.getWorld(); + final WorldInfo info = map.getInfo().getWorld(); WorldCreator creator = NMSHacks.detectWorld(worldName); if (creator == null) { creator = new WorldCreator(worldName); @@ -279,7 +279,7 @@ private Stage advanceSync() throws IllegalStateException { world.setPVP(true); world.setSpawnFlags(false, false); world.setAutoSave(false); - world.setDifficulty(difficulties[map.getDifficulty()]); + world.setDifficulty(difficulties[map.getInfo().getDifficulty()]); return new InitMatchStage(world, map); } diff --git a/core/src/main/java/tc/oc/pgm/match/MatchImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchImpl.java index d3abb09a11..22e2c0c9a3 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchImpl.java @@ -36,6 +36,7 @@ import tc.oc.pgm.api.feature.Feature; import tc.oc.pgm.api.filter.query.MatchQuery; import tc.oc.pgm.api.map.MapContext; +import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.match.MatchModule; import tc.oc.pgm.api.match.MatchPhase; @@ -132,7 +133,8 @@ protected MatchImpl(String id, MapContext map, World world) { this.state = new AtomicReference<>(MatchPhase.IDLE); this.start = new AtomicLong(0); this.end = new AtomicLong(0); - this.capacity = new AtomicInteger(map.getMaxPlayers().stream().mapToInt(i -> i).sum()); + this.capacity = + new AtomicInteger(map.getInfo().getMaxPlayers().stream().mapToInt(i -> i).sum()); this.executors = new EnumMap<>(MatchScope.class); this.listeners = new EnumMap<>(MatchScope.class); this.tickables = new EnumMap<>(MatchScope.class); @@ -232,8 +234,8 @@ public String getId() { } @Override - public MapContext getMap() { - return map; + public MapInfo getMap() { + return map.getInfo(); } @Override @@ -723,7 +725,7 @@ public Duration getDuration() { @Override public boolean getFriendlyFire() { - return friendlyFireOverride != null ? friendlyFireOverride : map.getFriendlyFire(); + return friendlyFireOverride != null ? friendlyFireOverride : map.getInfo().getFriendlyFire(); } @Override @@ -766,8 +768,7 @@ private ImmutableMap, MatchModuleFactory> buildM ImmutableMap.Builder, MatchModuleFactory> builder = ImmutableMap.builder(); builder.putAll(Modules.MATCH); - getMap() - .getModules() + map.getModules() .forEach(module -> builder.put(Modules.MAP_TO_MATCH.get(module.getClass()), module)); return builder.build(); } @@ -964,7 +965,7 @@ public String toString() { return "Match{id=" + this.id + ", map=" - + this.map.getId() + + this.map.getInfo().getId() + ", world=" + (world == null ? "" : world.getName()) + ", phase=" diff --git a/util/src/main/java/tc/oc/pgm/util/StringUtils.java b/util/src/main/java/tc/oc/pgm/util/StringUtils.java index 55e9a529d5..76ec69b1e3 100644 --- a/util/src/main/java/tc/oc/pgm/util/StringUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/StringUtils.java @@ -141,7 +141,7 @@ public static String normalize(String text) { } public static String slugify(String text) { - return normalize(text).replace(" ", ""); + return normalize(text).replace(" ", "_"); } public static String substring(String text, int begin, int end) { diff --git a/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java b/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java index 5f75bd7b65..edc9cf4458 100644 --- a/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/xml/XMLUtils.java @@ -215,6 +215,15 @@ public static Boolean parseBoolean(@Nullable Attribute attr, Boolean def) return attr == null ? def : parseBoolean(new Node(attr)); } + public static String parseRequiredId(Element element) throws InvalidXMLException { + String id = Node.fromRequiredAttr(element, "id").getValue(); + + if (id == null || id.isEmpty()) + throw new InvalidXMLException("Id attribute cannot be empty", element); + + return id; + } + /** * Get the value of the given numeric type that best represents positive infinity. * From e51760b0c8ec2b4832238b84c41543c7cebc0f6c Mon Sep 17 00:00:00 2001 From: Half <41559602+OhPointFive@users.noreply.github.com> Date: Thu, 11 May 2023 14:25:11 -0400 Subject: [PATCH 06/23] Clear crafting grid on clear kit (#1169) Signed-off-by: OhPointFive --- core/src/main/java/tc/oc/pgm/kits/ClearItemsKit.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/src/main/java/tc/oc/pgm/kits/ClearItemsKit.java b/core/src/main/java/tc/oc/pgm/kits/ClearItemsKit.java index 998c83141e..f922a49903 100644 --- a/core/src/main/java/tc/oc/pgm/kits/ClearItemsKit.java +++ b/core/src/main/java/tc/oc/pgm/kits/ClearItemsKit.java @@ -1,6 +1,9 @@ package tc.oc.pgm.kits; import java.util.List; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; import tc.oc.pgm.api.player.MatchPlayer; @@ -45,6 +48,15 @@ public void applyPostEvent(MatchPlayer player, boolean force, List di if (this.items) { player.getBukkit().getInventory().clear(); player.getBukkit().getOpenInventory().setCursor(null); + InventoryView openInventory = player.getBukkit().getOpenInventory(); + if (openInventory != null + && (openInventory.getType().equals(InventoryType.CRAFTING) + || openInventory.getType().equals(InventoryType.WORKBENCH))) { + Inventory topInventory = openInventory.getTopInventory(); + if (topInventory != null) { + topInventory.clear(); + } + } } } } From 151080e23bd8ec4cfa0123793a2d98f2b74d83d9 Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Sat, 13 May 2023 18:15:32 +0200 Subject: [PATCH 07/23] Fix broken regex in conditional variant parsing (#1180) Signed-off-by: Pablo Herrera --- core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java b/core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java index c7312b96e4..ddd073dafd 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java +++ b/core/src/main/java/tc/oc/pgm/map/MapFilePreprocessor.java @@ -130,7 +130,7 @@ private List processIncludeElement(Element element) throws InvalidXMLEx private List processConditional(Element el, boolean shouldContain) throws InvalidXMLException { boolean contains = - Arrays.asList(Node.fromRequiredAttr(el, "variant").getValue().split("[\\\\s,]")) + Arrays.asList(Node.fromRequiredAttr(el, "variant").getValue().split("[\\s,]+")) .contains(variant); return contains == shouldContain ? el.cloneContent() : Collections.emptyList(); From 642feae7f52b0fc5f589e662a9fd8cc4baa30d5d Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Mon, 15 May 2023 03:02:08 +0200 Subject: [PATCH 08/23] Fix wrong file separator (#1182) Signed-off-by: Pablo Herrera --- core/src/main/java/tc/oc/pgm/PGMConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/tc/oc/pgm/PGMConfig.java b/core/src/main/java/tc/oc/pgm/PGMConfig.java index 80691de40d..0f7ecf365b 100644 --- a/core/src/main/java/tc/oc/pgm/PGMConfig.java +++ b/core/src/main/java/tc/oc/pgm/PGMConfig.java @@ -243,7 +243,7 @@ public static void registerRemoteMapSource( Normalizer.normalize(uri.getHost() + uri.getPath(), Normalizer.Form.NFD) .replaceAll("[^A-Za-z0-9_]", "-") .toLowerCase(Locale.ROOT); - path = "maps" + File.pathSeparator + normalizedPath; + path = "maps" + File.separator + normalizedPath; } Path base = Paths.get(path).toAbsolutePath(); From fd06354631876ed30ac9db00815337c361504e26 Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Tue, 23 May 2023 20:34:34 +0200 Subject: [PATCH 09/23] Fix obs always being forced (#1184) Signed-off-by: Pablo Herrera --- core/src/main/java/tc/oc/pgm/command/JoinCommand.java | 3 ++- core/src/main/java/tc/oc/pgm/command/TeamCommand.java | 3 ++- .../src/main/java/tc/oc/pgm/join/JoinMatchModule.java | 4 ++-- core/src/main/java/tc/oc/pgm/join/JoinRequest.java | 11 +++++++++++ core/src/main/java/tc/oc/pgm/match/MatchImpl.java | 5 ++++- .../main/java/tc/oc/pgm/picker/PickerMatchModule.java | 4 ++-- 6 files changed, 23 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/command/JoinCommand.java b/core/src/main/java/tc/oc/pgm/command/JoinCommand.java index 6dba46d314..3fe2a7bf1d 100644 --- a/core/src/main/java/tc/oc/pgm/command/JoinCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/JoinCommand.java @@ -11,6 +11,7 @@ import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.join.JoinMatchModule; +import tc.oc.pgm.join.JoinRequest; public final class JoinCommand { @@ -38,6 +39,6 @@ public void join( @CommandDescription("Leave the match") @CommandPermission(Permissions.LEAVE) public void leave(JoinMatchModule joiner, MatchPlayer player) { - joiner.leave(player); + joiner.leave(player, JoinRequest.empty()); } } diff --git a/core/src/main/java/tc/oc/pgm/command/TeamCommand.java b/core/src/main/java/tc/oc/pgm/command/TeamCommand.java index 3945d53b14..7499b96bbb 100644 --- a/core/src/main/java/tc/oc/pgm/command/TeamCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/TeamCommand.java @@ -23,6 +23,7 @@ import tc.oc.pgm.api.party.Party; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.join.JoinMatchModule; +import tc.oc.pgm.join.JoinRequest; import tc.oc.pgm.teams.Team; import tc.oc.pgm.teams.TeamMatchModule; import tc.oc.pgm.util.Audience; @@ -44,7 +45,7 @@ public void force( final Party oldParty = joiner.getParty(); if (team != null && !(team instanceof Competitor)) { - join.leave(joiner); + join.leave(joiner, JoinRequest.force()); } else { join.forceJoin(joiner, (Competitor) team); } diff --git a/core/src/main/java/tc/oc/pgm/join/JoinMatchModule.java b/core/src/main/java/tc/oc/pgm/join/JoinMatchModule.java index 1b917b50a3..37d9574e83 100644 --- a/core/src/main/java/tc/oc/pgm/join/JoinMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/join/JoinMatchModule.java @@ -155,7 +155,7 @@ public boolean join(MatchPlayer joining, JoinRequest request, @NotNull JoinResul return handler.join(joining, request, result); } - public boolean leave(MatchPlayer leaving) { + public boolean leave(MatchPlayer leaving, JoinRequest request) { if (cancelQueuedJoin(leaving)) return true; if (leaving.getParty() instanceof ObserverParty) { @@ -169,7 +169,7 @@ public boolean leave(MatchPlayer leaving) { return false; } - return match.setParty(leaving, match.getDefaultParty()); + return match.setParty(leaving, match.getDefaultParty(), request); } public QueuedParty getQueuedParticipants() { diff --git a/core/src/main/java/tc/oc/pgm/join/JoinRequest.java b/core/src/main/java/tc/oc/pgm/join/JoinRequest.java index a615483b16..c568f90480 100644 --- a/core/src/main/java/tc/oc/pgm/join/JoinRequest.java +++ b/core/src/main/java/tc/oc/pgm/join/JoinRequest.java @@ -10,6 +10,9 @@ import tc.oc.pgm.teams.Team; public class JoinRequest { + private static final JoinRequest FORCE = new JoinRequest(null, 1, EnumSet.of(Flag.FORCE)); + private static final JoinRequest EMPTY = new JoinRequest(null, 1, EnumSet.noneOf(Flag.class)); + private final @Nullable Team team; private final int players; private final ImmutableSet flags; @@ -36,6 +39,14 @@ public static JoinRequest group(@Nullable Team team, int size, Set flags) return new JoinRequest(team, size, flags); } + public static JoinRequest empty() { + return EMPTY; + } + + public static JoinRequest force() { + return FORCE; + } + @Nullable public Team getTeam() { return team; diff --git a/core/src/main/java/tc/oc/pgm/match/MatchImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchImpl.java index 22e2c0c9a3..f29548ceed 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchImpl.java @@ -465,7 +465,10 @@ public void removePlayer(Player bukkit) { @Override public boolean setParty(MatchPlayer player, Party party, @Nullable JoinRequest request) { if (request == null) - request = JoinRequest.of(party instanceof Team ? (Team) party : null, JoinRequest.Flag.FORCE); + request = + party instanceof Team + ? JoinRequest.of((Team) party, JoinRequest.Flag.FORCE) + : JoinRequest.force(); return setOrClearPlayerParty(player, assertNotNull(party), request); } diff --git a/core/src/main/java/tc/oc/pgm/picker/PickerMatchModule.java b/core/src/main/java/tc/oc/pgm/picker/PickerMatchModule.java index c13f0a0792..5169232c98 100644 --- a/core/src/main/java/tc/oc/pgm/picker/PickerMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/picker/PickerMatchModule.java @@ -373,7 +373,7 @@ public void rightClickIcon(final ObserverInteractEvent event) { jmm.join(player, JoinRequest.fromPlayer(player, null)); } } else if (hand.getType() == Button.LEAVE.material && left) { - jmm.leave(player); + jmm.leave(player, JoinRequest.empty()); } if (handled) { @@ -763,7 +763,7 @@ private void scheduleLeave(final MatchPlayer player) { .execute( () -> { if (bukkit.isOnline()) { - match.needModule(JoinMatchModule.class).leave(player); + match.needModule(JoinMatchModule.class).leave(player, JoinRequest.empty()); } }); } From 7427ffc3ad6477c2efeb6ec4e1fae423ee7f868b Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 28 May 2023 16:37:18 +0100 Subject: [PATCH 10/23] Clear a players cached decoration on quit (#1186) Signed-off-by: Pugzy --- .../oc/pgm/namedecorations/NameDecorationRegistryImpl.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java b/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java index 730919b94c..ba8c229e7d 100644 --- a/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java +++ b/core/src/main/java/tc/oc/pgm/namedecorations/NameDecorationRegistryImpl.java @@ -17,6 +17,7 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.metadata.FixedMetadataValue; import org.bukkit.metadata.MetadataValue; import org.jetbrains.annotations.NotNull; @@ -71,6 +72,12 @@ public void onPartyChange(PlayerPartyChangeEvent event) { player.setDisplayName(getDecoratedName(player, party == null ? null : party.getColor())); } + @EventHandler(priority = EventPriority.MONITOR) + public void onPlayerQuit(PlayerQuitEvent event) { + decorationCache.invalidate(event.getPlayer().getUniqueId()); + PlayerComponent.RENDERER.decorationChanged(event.getPlayer().getUniqueId()); + } + @EventHandler public void onNameDecorationChange(NameDecorationChangeEvent event) { if (event.getUUID() == null) return; From cdc81493134dbc8fb7b90f8af3f0895f97315c53 Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Mon, 29 May 2023 06:05:14 +0200 Subject: [PATCH 11/23] Fix issues with carrying & monostable filters (#1188) Signed-off-by: Pablo Herrera --- .../matcher/match/MonostableFilter.java | 4 +++- .../matcher/player/CarryingItemFilter.java | 19 +++++++++++++++++-- .../matcher/player/HoldingItemFilter.java | 5 +++-- .../matcher/player/ParticipantItemFilter.java | 10 ++++------ .../matcher/player/WearingItemFilter.java | 8 ++++++-- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/filters/matcher/match/MonostableFilter.java b/core/src/main/java/tc/oc/pgm/filters/matcher/match/MonostableFilter.java index e9c16d9e78..6adfc77485 100644 --- a/core/src/main/java/tc/oc/pgm/filters/matcher/match/MonostableFilter.java +++ b/core/src/main/java/tc/oc/pgm/filters/matcher/match/MonostableFilter.java @@ -79,6 +79,7 @@ protected class Reactor> extends ReactorFactory.Reactor implements Tickable { protected final Class scope; + protected Instant lastTick = Instant.MIN; // Filterables that currently pass the inner filter, mapped to the instants that they expire. // They are not actually removed until the inner filter goes false. @@ -123,10 +124,11 @@ public void tick(Match match, Tick tick) { endTimes.forEach( (filterable, end) -> { - if (now.isAfter(end)) { + if (!now.isBefore(end) && lastTick.isBefore(end)) { this.invalidate(filterable); } }); + lastTick = now; } } diff --git a/core/src/main/java/tc/oc/pgm/filters/matcher/player/CarryingItemFilter.java b/core/src/main/java/tc/oc/pgm/filters/matcher/player/CarryingItemFilter.java index 1c4ee5fdcf..40da4d2562 100644 --- a/core/src/main/java/tc/oc/pgm/filters/matcher/player/CarryingItemFilter.java +++ b/core/src/main/java/tc/oc/pgm/filters/matcher/player/CarryingItemFilter.java @@ -1,10 +1,14 @@ package tc.oc.pgm.filters.matcher.player; import com.google.common.collect.ImmutableList; +import java.util.Arrays; import java.util.Collection; +import java.util.stream.Stream; import org.bukkit.event.Event; import org.bukkit.event.entity.EntityShootBowEvent; +import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerItemBreakEvent; +import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemStack; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.kits.ApplyKitEvent; @@ -26,7 +30,18 @@ public Collection> getRelevantEvents() { } @Override - protected ItemStack[] getItems(MatchPlayer player) { - return player.getBukkit().getInventory().getContents(); + protected Stream getItems(MatchPlayer player) { + Stream inventory = + Stream.concat( + Arrays.stream(player.getBukkit().getInventory().getContents()), + Stream.of(player.getBukkit().getItemOnCursor())); + + // Potentially add the crafting grid if that's the currently open inventory + InventoryView invView = player.getBukkit().getOpenInventory(); + InventoryType type = invView.getType(); + if (type == InventoryType.CRAFTING || type == InventoryType.WORKBENCH) { + return Stream.concat(inventory, Arrays.stream(invView.getTopInventory().getContents())); + } + return inventory; } } diff --git a/core/src/main/java/tc/oc/pgm/filters/matcher/player/HoldingItemFilter.java b/core/src/main/java/tc/oc/pgm/filters/matcher/player/HoldingItemFilter.java index a7615e453b..556c831afa 100644 --- a/core/src/main/java/tc/oc/pgm/filters/matcher/player/HoldingItemFilter.java +++ b/core/src/main/java/tc/oc/pgm/filters/matcher/player/HoldingItemFilter.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableList; import java.util.Collection; +import java.util.stream.Stream; import org.bukkit.event.Event; import org.bukkit.event.player.PlayerItemBreakEvent; import org.bukkit.event.player.PlayerItemHeldEvent; @@ -26,7 +27,7 @@ public Collection> getRelevantEvents() { } @Override - protected ItemStack[] getItems(MatchPlayer player) { - return new ItemStack[] {player.getBukkit().getItemInHand()}; + protected Stream getItems(MatchPlayer player) { + return Stream.of(player.getBukkit().getItemInHand()); } } diff --git a/core/src/main/java/tc/oc/pgm/filters/matcher/player/ParticipantItemFilter.java b/core/src/main/java/tc/oc/pgm/filters/matcher/player/ParticipantItemFilter.java index 3cbe456898..bfd0d29922 100644 --- a/core/src/main/java/tc/oc/pgm/filters/matcher/player/ParticipantItemFilter.java +++ b/core/src/main/java/tc/oc/pgm/filters/matcher/player/ParticipantItemFilter.java @@ -2,6 +2,8 @@ import static tc.oc.pgm.util.Assert.assertNotNull; +import java.util.Objects; +import java.util.stream.Stream; import org.bukkit.inventory.ItemStack; import tc.oc.pgm.api.filter.query.PlayerQuery; import tc.oc.pgm.api.player.MatchPlayer; @@ -14,14 +16,10 @@ public ParticipantItemFilter(ItemMatcher matcher) { this.matcher = assertNotNull(matcher, "item"); } - protected abstract ItemStack[] getItems(MatchPlayer player); + protected abstract Stream getItems(MatchPlayer player); @Override public boolean matches(PlayerQuery query, MatchPlayer player) { - for (ItemStack item : getItems(player)) { - if (item == null) continue; - if (matcher.matches(item)) return true; - } - return false; + return getItems(player).filter(Objects::nonNull).anyMatch(matcher::matches); } } diff --git a/core/src/main/java/tc/oc/pgm/filters/matcher/player/WearingItemFilter.java b/core/src/main/java/tc/oc/pgm/filters/matcher/player/WearingItemFilter.java index b0e81eeda7..010fcee492 100644 --- a/core/src/main/java/tc/oc/pgm/filters/matcher/player/WearingItemFilter.java +++ b/core/src/main/java/tc/oc/pgm/filters/matcher/player/WearingItemFilter.java @@ -1,7 +1,9 @@ package tc.oc.pgm.filters.matcher.player; import com.google.common.collect.ImmutableList; +import java.util.Arrays; import java.util.Collection; +import java.util.stream.Stream; import org.bukkit.event.Event; import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.PlayerInteractEvent; @@ -9,6 +11,7 @@ import org.bukkit.inventory.ItemStack; import tc.oc.pgm.api.player.MatchPlayer; import tc.oc.pgm.kits.ApplyKitEvent; +import tc.oc.pgm.util.event.PlayerItemTransferEvent; import tc.oc.pgm.util.inventory.ItemMatcher; public class WearingItemFilter extends ParticipantItemFilter { @@ -20,13 +23,14 @@ public WearingItemFilter(ItemMatcher matcher) { public Collection> getRelevantEvents() { return ImmutableList.of( InventoryClickEvent.class, + PlayerItemTransferEvent.class, PlayerInteractEvent.class, ApplyKitEvent.class, PlayerItemBreakEvent.class); } @Override - protected ItemStack[] getItems(MatchPlayer player) { - return player.getBukkit().getInventory().getArmorContents(); + protected Stream getItems(MatchPlayer player) { + return Arrays.stream(player.getBukkit().getInventory().getArmorContents()); } } From 7aa52c8d888228ddc364544ffb3d3cf205bf8ca1 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 29 May 2023 05:05:45 +0100 Subject: [PATCH 12/23] Add custom inventory check for item mods (#1183) Signed-off-by: Pugzy --- .../tc/oc/pgm/itemmeta/ItemModifyMatchModule.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/itemmeta/ItemModifyMatchModule.java b/core/src/main/java/tc/oc/pgm/itemmeta/ItemModifyMatchModule.java index 9c17ec2741..3c8574074a 100644 --- a/core/src/main/java/tc/oc/pgm/itemmeta/ItemModifyMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/itemmeta/ItemModifyMatchModule.java @@ -1,14 +1,17 @@ package tc.oc.pgm.itemmeta; import org.bukkit.entity.Item; +import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.block.BlockDispenseEvent; import org.bukkit.event.entity.ItemSpawnEvent; import org.bukkit.event.inventory.CraftItemEvent; import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.inventory.PrepareItemCraftEvent; import org.bukkit.event.player.PlayerPickupItemEvent; +import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.match.MatchModule; @@ -61,10 +64,16 @@ public void onPrepareItemCraft(PrepareItemCraftEvent event) { @EventHandler public void onInventoryOpen(InventoryOpenEvent event) { - ItemStack[] contents = event.getInventory().getContents(); + Inventory inventory = event.getInventory(); + // Custom GUIs use chest inventory type with player as the holder + if (inventory.getType() == InventoryType.CHEST && inventory.getHolder() instanceof Player) { + return; + } + + ItemStack[] contents = inventory.getContents(); for (int i = 0; i < contents.length; i++) { if (applyRules(contents[i])) { - event.getInventory().setItem(i, contents[i]); + inventory.setItem(i, contents[i]); } } } From 0af77b28db340281bb217388bc47f282fb54278b Mon Sep 17 00:00:00 2001 From: Christopher White Date: Sun, 11 Jun 2023 09:32:12 -0700 Subject: [PATCH 13/23] Fix BlockDrop item velocity not matching vanilla block drop velocity (#1192) Signed-off-by: Christopher White <18whitechristop@gmail.com> --- .../java/tc/oc/pgm/blockdrops/BlockDropsMatchModule.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsMatchModule.java b/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsMatchModule.java index 05f592307d..72ec0a8da6 100644 --- a/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsMatchModule.java @@ -110,7 +110,11 @@ private void dropItems(BlockDrops drops, MatchPlayer player, Location location, Random random = match.getRandom(); for (Map.Entry entry : drops.items.entrySet()) { if (random.nextFloat() < yield * entry.getValue()) { - location.getWorld().dropItemNaturally(location, entry.getKey()); + Location dropLocation = location.clone(); + dropLocation.setX(dropLocation.getBlockX() + random.nextDouble() * 0.5 + 0.25); + dropLocation.setY(dropLocation.getBlockY() + random.nextDouble() * 0.5 + 0.25); + dropLocation.setZ(dropLocation.getBlockZ() + random.nextDouble() * 0.5 + 0.25); + dropLocation.getWorld().dropItem(dropLocation, entry.getKey()); } } } From e1ddd1bda55e5884d265b5fd57d96f4e84b6759f Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Sat, 24 Jun 2023 19:24:39 +0200 Subject: [PATCH 14/23] Fix maps from git repos not being updated (#1193) Signed-off-by: Pablo Herrera --- .../java/tc/oc/pgm/map/MapLibraryImpl.java | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java index 79f78a7ab8..baa20150e7 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java @@ -5,14 +5,15 @@ import java.util.Collections; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.SortedMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentSkipListMap; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.bukkit.ChatColor; import org.jetbrains.annotations.Nullable; @@ -25,7 +26,6 @@ import tc.oc.pgm.api.map.factory.MapFactory; import tc.oc.pgm.api.map.factory.MapSourceFactory; import tc.oc.pgm.api.map.includes.MapIncludeProcessor; -import tc.oc.pgm.map.source.StreamMapSourceFactory; import tc.oc.pgm.util.LiquidMetal; import tc.oc.pgm.util.StringUtils; import tc.oc.pgm.util.UsernameResolver; @@ -120,47 +120,48 @@ public CompletableFuture loadNewMaps(boolean reset) { // Try to search new includes before searching for new maps includes.loadNewIncludes(); - final List sources = new LinkedList<>(); - - // Reload failed maps if (reset) { failed.clear(); - } else { - sources.add(new StreamMapSourceFactory(failed.stream())); + this.factories.forEach(MapSourceFactory::reset); } final int oldFail = failed.size(); final int oldOk = reset ? 0 : maps.size(); - // Discover new maps - for (MapSourceFactory factory : this.factories) { - if (reset) factory.reset(); - sources.add(factory); - } - - // Reload or delete existing maps that have updates - sources.add( - new StreamMapSourceFactory( - this.maps.entrySet().stream() - .filter( - entry -> { - try { - return reset || entry.getValue().getSource().checkForUpdates(); - } catch (MapMissingException e) { - logMapError(e); - this.maps.remove(entry.getKey()); - return false; - } - }) - .map(entry -> entry.getValue().getSource()))); - return CompletableFuture.runAsync( () -> { + // First ensure loadNewSources is called for all factories, this may take some time + // (eg: Git pull) + List> mapSources = + factories + .parallelStream() + .map(s -> s.loadNewSources(this::logMapError)) + .collect(Collectors.toList()); + + if (reset) { + // Doing full reset; add all known maps to be re-loaded + mapSources.add(this.maps.values().stream().map(MapInfo::getSource)); + } else { + // Not a full reset; reload failed & modified maps + mapSources.add(failed.stream()); + + mapSources.add( + this.maps.entrySet().stream() + .filter( + entry -> { + try { + return entry.getValue().getSource().checkForUpdates(); + } catch (MapMissingException e) { + logMapError(e); + this.maps.remove(entry.getKey()); + return false; + } + }) + .map(entry -> entry.getValue().getSource())); + } + try (Stream stream = - sources.stream() - .flatMap(s -> s.loadNewSources(this::logMapError)) - .parallel() - .unordered()) { + mapSources.stream().flatMap(Function.identity()).parallel().unordered()) { stream.forEach(s -> this.loadMapSafe(s, null)); } }) From 61a6dc48064d4ce5c57dfea4b2e825ea678b9bb4 Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Sat, 24 Jun 2023 19:25:00 +0200 Subject: [PATCH 15/23] Implement order & display-all capability for pool command (#1194) Signed-off-by: Pablo Herrera --- .../tc/oc/pgm/command/MapPoolCommand.java | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/command/MapPoolCommand.java b/core/src/main/java/tc/oc/pgm/command/MapPoolCommand.java index 23c6e93df4..5f3d4e163e 100644 --- a/core/src/main/java/tc/oc/pgm/command/MapPoolCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/MapPoolCommand.java @@ -16,6 +16,7 @@ import cloud.commandframework.annotations.specifier.Range; import java.text.DecimalFormat; import java.time.Duration; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -58,7 +59,9 @@ public void pool( @Flag(value = "type", aliases = "t") MapPoolType type, @Flag(value = "pool", aliases = "p") MapPool mapPool, @Flag(value = "score", aliases = "s") boolean scores, - @Flag(value = "chance", aliases = "c") boolean chance) { + @Flag(value = "chance", aliases = "c") boolean chance, + @Flag(value = "order", aliases = "o") boolean order, + @Flag(value = "all", aliases = "a") boolean all) { // Default to current pool if (mapPool == null) mapPool = poolManager.getActiveMapPool(); @@ -66,8 +69,8 @@ public void pool( throw exception("pool.noPoolMatch"); List maps = mapPool.getMaps(); - int resultsPerPage = 8; - int pages = (maps.size() + resultsPerPage - 1) / resultsPerPage; + int resultsPerPage = all ? maps.size() : 8; + int pages = all ? 1 : (maps.size() + resultsPerPage - 1) / resultsPerPage; Component mapPoolComponent = TextFormatter.paginate( @@ -101,6 +104,14 @@ public void pool( int nextPos = mapPool instanceof Rotation ? ((Rotation) mapPool).getNextPosition() : -1; + if (order && votes != null) { + maps = + maps.stream() + .sorted( + Comparator.comparingDouble(chance ? chances::get : votes::getMapScore).reversed()) + .collect(Collectors.toList()); + } + new PrettyPaginatedComponentResults(title, resultsPerPage) { @Override public Component format(MapInfo map, int index) { @@ -304,8 +315,7 @@ public void rot( CommandSender source, MapPoolManager poolManager, @Argument(value = "page", defaultValue = "1") @Range(min = "1") int page, - @Flag(value = "score", aliases = "s") boolean scores, - @Flag(value = "chance", aliases = "c") boolean chance, + @Flag(value = "all", aliases = "a") boolean all, String[] rawArgs) { wrapLegacy( "pool", @@ -315,7 +325,17 @@ public void rot( if (poolManager.getActiveMapPool().getType() != MapPoolType.ORDERED) throw exception("pool.noRotation"); - pool(sender, source, poolManager, page, MapPoolType.ORDERED, null, scores, chance); + pool( + sender, + source, + poolManager, + page, + MapPoolType.ORDERED, + null, + false, + false, + false, + all); }); } From cc37680e8b3599df017b582e90f714fe727159a1 Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Mon, 26 Jun 2023 18:33:31 +0200 Subject: [PATCH 16/23] Fix compile error on newer jdks & cleanup (#1197) Signed-off-by: Pablo Herrera --- .../pgm/api/map/factory/MapSourceFactory.java | 2 +- .../java/tc/oc/pgm/map/MapLibraryImpl.java | 5 ++-- .../pgm/map/source/GitMapSourceFactory.java | 2 +- .../pgm/map/source/PathMapSourceFactory.java | 2 +- .../map/source/StreamMapSourceFactory.java | 24 ------------------- 5 files changed, 6 insertions(+), 29 deletions(-) delete mode 100644 core/src/main/java/tc/oc/pgm/map/source/StreamMapSourceFactory.java diff --git a/core/src/main/java/tc/oc/pgm/api/map/factory/MapSourceFactory.java b/core/src/main/java/tc/oc/pgm/api/map/factory/MapSourceFactory.java index f447ee051e..9c4fddb4ab 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/factory/MapSourceFactory.java +++ b/core/src/main/java/tc/oc/pgm/api/map/factory/MapSourceFactory.java @@ -17,7 +17,7 @@ public interface MapSourceFactory { * @param exceptionHandler place to send any exception when loading maps * @return An iterator of new {@link MapSource}s. */ - Stream loadNewSources(Consumer exceptionHandler); + Stream loadNewSources(Consumer exceptionHandler); /** * Reset any caches so when {@link #loadNewSources(Consumer)} is next called, it discovers diff --git a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java index baa20150e7..7faf4d6864 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapLibraryImpl.java @@ -132,7 +132,7 @@ public CompletableFuture loadNewMaps(boolean reset) { () -> { // First ensure loadNewSources is called for all factories, this may take some time // (eg: Git pull) - List> mapSources = + List> mapSources = factories .parallelStream() .map(s -> s.loadNewSources(this::logMapError)) @@ -160,7 +160,8 @@ public CompletableFuture loadNewMaps(boolean reset) { .map(entry -> entry.getValue().getSource())); } - try (Stream stream = + // Finally load all the maps + try (Stream stream = mapSources.stream().flatMap(Function.identity()).parallel().unordered()) { stream.forEach(s -> this.loadMapSafe(s, null)); } diff --git a/core/src/main/java/tc/oc/pgm/map/source/GitMapSourceFactory.java b/core/src/main/java/tc/oc/pgm/map/source/GitMapSourceFactory.java index d703e1929d..a10c14d3c1 100644 --- a/core/src/main/java/tc/oc/pgm/map/source/GitMapSourceFactory.java +++ b/core/src/main/java/tc/oc/pgm/map/source/GitMapSourceFactory.java @@ -47,7 +47,7 @@ public GitMapSourceFactory( } @Override - public Stream loadNewSources(Consumer exceptionHandler) { + public Stream loadNewSources(Consumer exceptionHandler) { try { git.pull() .setCredentialsProvider(credentials) diff --git a/core/src/main/java/tc/oc/pgm/map/source/PathMapSourceFactory.java b/core/src/main/java/tc/oc/pgm/map/source/PathMapSourceFactory.java index 1b15ba50f9..413a193110 100644 --- a/core/src/main/java/tc/oc/pgm/map/source/PathMapSourceFactory.java +++ b/core/src/main/java/tc/oc/pgm/map/source/PathMapSourceFactory.java @@ -51,7 +51,7 @@ private Stream loadNewSource(Path path) { } @Override - public Stream loadNewSources(Consumer exceptionHandler) { + public Stream loadNewSources(Consumer exceptionHandler) { if (!Files.exists(base) || !Files.isDirectory(base)) return Stream.empty(); return (children == null ? Stream.of(base) : children.stream().map(base::resolve)) diff --git a/core/src/main/java/tc/oc/pgm/map/source/StreamMapSourceFactory.java b/core/src/main/java/tc/oc/pgm/map/source/StreamMapSourceFactory.java deleted file mode 100644 index 068774fb46..0000000000 --- a/core/src/main/java/tc/oc/pgm/map/source/StreamMapSourceFactory.java +++ /dev/null @@ -1,24 +0,0 @@ -package tc.oc.pgm.map.source; - -import java.util.function.Consumer; -import java.util.stream.Stream; -import tc.oc.pgm.api.map.MapSource; -import tc.oc.pgm.api.map.exception.MapException; -import tc.oc.pgm.api.map.factory.MapSourceFactory; - -public class StreamMapSourceFactory implements MapSourceFactory { - - private final Stream sources; - - public StreamMapSourceFactory(Stream sources) { - this.sources = sources; - } - - @Override - public Stream loadNewSources(Consumer exceptionHandler) { - return sources; - } - - @Override - public void reset() {} -} From e2209a49638e953194263bdf7dfc9aa55a0fffa3 Mon Sep 17 00:00:00 2001 From: Christopher White Date: Fri, 7 Jul 2023 12:20:42 -0700 Subject: [PATCH 17/23] Prepare for multi-version PGM (#1187) Signed-off-by: Christopher White <18whitechristop@gmail.com> --- .../tc/oc/pgm/action/actions/FillAction.java | 3 +- .../oc/pgm/api/event/BlockTransformEvent.java | 4 +- .../tc/oc/pgm/blitz/BlitzMatchModule.java | 8 +- .../pgm/blockdrops/BlockDropsMatchModule.java | 8 +- .../oc/pgm/blockdrops/BlockDropsRuleSet.java | 1 - .../tc/oc/pgm/filters/FilterMatchModule.java | 29 +- .../pgm/flag/LegacyFlagBeamMatchModule.java | 15 +- .../inventory/ViewInventoryMatchModule.java | 40 +- .../pgm/listeners/BlockTransformListener.java | 4 +- .../java/tc/oc/pgm/listeners/PGMListener.java | 6 +- .../tc/oc/pgm/match/MatchFactoryImpl.java | 7 +- .../java/tc/oc/pgm/match/MatchPlayerImpl.java | 5 +- .../tc/oc/pgm/portals/PortalTransform.java | 8 +- .../java/tc/oc/pgm/renewable/Renewable.java | 4 +- .../tc/oc/pgm/snapshot/BudgetWorldEdit.java | 3 +- .../tc/oc/pgm/snapshot/WorldSnapshot.java | 3 +- .../pgm/spawner/objects/SpawnablePotion.java | 3 +- .../java/tc/oc/pgm/tnt/TNTMatchModule.java | 13 +- .../pgm/tntrender/TNTRenderMatchModule.java | 16 +- .../tc/oc/pgm/util/block/BlockStates.java | 4 +- .../oc/pgm/util/concurrent/RateLimiter.java | 4 +- .../oc/pgm/util/inventory/InventoryUtils.java | 5 +- .../oc/pgm/util/nms/EnumPlayerInfoAction.java | 9 + .../java/tc/oc/pgm/util/nms/NMSHacks.java | 1451 +++-------------- .../java/tc/oc/pgm/util/nms/NMSHacks1_8.java | 1118 +++++++++++++ .../java/tc/oc/pgm/util/nms/NMSHacksNoOp.java | 200 +++ .../tc/oc/pgm/util/nms/NMSHacksPlatform.java | 205 +++ .../oc/pgm/util/nms/NMSHacksSportPaper.java | 268 +++ .../attribute/AttributeInstance1_8.java} | 9 +- .../attribute/AttributeMap1_8.java} | 15 +- .../util/nms/attribute/AttributeMapNoOp.java | 12 + .../pgm/util/nms/entity/fake/FakeEntity.java | 44 + .../nms/entity/fake/FakeEntityImpl1_8.java | 39 + .../util/nms/entity/fake/FakeEntityNoOp.java | 22 + .../nms/entity/fake/FakeLivingEntity1_8.java | 15 + .../fake/armorstand/FakeArmorStand1_8.java | 39 + .../fake/wither/FakeWitherSkull1_8.java | 22 + .../util/nms/entity/potion/EntityPotion.java | 10 + .../nms/entity/potion/EntityPotion1_8.java | 32 + .../nms/entity/potion/EntityPotionBukkit.java | 31 + .../reflect/MinecraftReflectionUtils.java | 19 + .../oc/pgm/util/reflect/ReflectionUtils.java | 32 +- .../oc/pgm/util/tablist/PlayerTabEntry.java | 6 +- .../tc/oc/pgm/util/tablist/TabDisplay.java | 34 +- .../tc/oc/pgm/util/tablist/TabRender.java | 80 +- 45 files changed, 2546 insertions(+), 1359 deletions(-) create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/EnumPlayerInfoAction.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/NMSHacks1_8.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/NMSHacksSportPaper.java rename util/src/main/java/tc/oc/pgm/util/{attribute/AttributeInstanceImpl.java => nms/attribute/AttributeInstance1_8.java} (87%) rename util/src/main/java/tc/oc/pgm/util/{attribute/AttributeMapImpl.java => nms/attribute/AttributeMap1_8.java} (72%) create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMapNoOp.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntity.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityImpl1_8.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityNoOp.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeLivingEntity1_8.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/fake/armorstand/FakeArmorStand1_8.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/fake/wither/FakeWitherSkull1_8.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion1_8.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotionBukkit.java create mode 100644 util/src/main/java/tc/oc/pgm/util/reflect/MinecraftReflectionUtils.java diff --git a/core/src/main/java/tc/oc/pgm/action/actions/FillAction.java b/core/src/main/java/tc/oc/pgm/action/actions/FillAction.java index 11a1995715..7d80202262 100644 --- a/core/src/main/java/tc/oc/pgm/action/actions/FillAction.java +++ b/core/src/main/java/tc/oc/pgm/action/actions/FillAction.java @@ -9,6 +9,7 @@ import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.region.Region; import tc.oc.pgm.filters.query.BlockQuery; +import tc.oc.pgm.util.nms.NMSHacks; public class FillAction extends AbstractAction { @@ -32,7 +33,7 @@ public void trigger(Match match) { if (filter != null && filter.query(new BlockQuery(block)).isDenied()) continue; BlockState newState = block.getState(); - newState.setMaterialData(materialData); + NMSHacks.setBlockStateData(newState, materialData); if (events) { BlockFormEvent event = new BlockFormEvent(block, newState); diff --git a/core/src/main/java/tc/oc/pgm/api/event/BlockTransformEvent.java b/core/src/main/java/tc/oc/pgm/api/event/BlockTransformEvent.java index 22c7070e1b..fe75e109be 100644 --- a/core/src/main/java/tc/oc/pgm/api/event/BlockTransformEvent.java +++ b/core/src/main/java/tc/oc/pgm/api/event/BlockTransformEvent.java @@ -20,6 +20,7 @@ import tc.oc.pgm.util.block.BlockStates; import tc.oc.pgm.util.event.GeneralizedEvent; import tc.oc.pgm.util.event.entity.ExplosionPrimeByEntityEvent; +import tc.oc.pgm.util.nms.NMSHacks; /** Called when a {@link Block} transforms from one {@link BlockState} to another. */ public class BlockTransformEvent extends GeneralizedEvent { @@ -97,8 +98,7 @@ public final BlockState getNewState() { return newState; } else { final BlockState state = newState.getBlock().getState(); - state.setType(drops.replacement.getItemType()); - state.setData(drops.replacement); + NMSHacks.setBlockStateData(state, drops.replacement); return state; } } diff --git a/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java b/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java index a05b34e589..d4916f4543 100644 --- a/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/blitz/BlitzMatchModule.java @@ -142,7 +142,7 @@ public void handleSpawn(final ParticipantSpawnEvent event) { @EventHandler(priority = EventPriority.MONITOR) public void onBlitzPlayerEliminated(final BlitzPlayerEliminatedEvent event) { - this.eliminatedPlayers.add(event.getPlayer().getBukkit().getUniqueId()); + this.eliminatedPlayers.add(event.getPlayer().getId()); World world = event.getMatch().getWorld(); Location death = event.getDeathLocation(); @@ -162,8 +162,7 @@ public void onBlitzPlayerEliminated(final BlitzPlayerEliminatedEvent event) { private void handleElimination(final MatchPlayer player, Competitor competitor) { if (!eliminatedPlayers.add(player.getBukkit().getUniqueId())) return; - match.callEvent( - new BlitzPlayerEliminatedEvent(player, competitor, player.getBukkit().getLocation())); + match.callEvent(new BlitzPlayerEliminatedEvent(player, competitor, player.getLocation())); checkEnd(); } @@ -179,8 +178,7 @@ private void checkEnd() { .execute( () -> { ImmutableSet.copyOf(match.getParticipants()).stream() - .filter( - participating -> isPlayerEliminated(participating.getBukkit().getUniqueId())) + .filter(participating -> isPlayerEliminated(participating.getId())) .forEach(participating -> match.setParty(participating, match.getDefaultParty())); match.calculateVictory(); diff --git a/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsMatchModule.java b/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsMatchModule.java index 72ec0a8da6..9a846fcb74 100644 --- a/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsMatchModule.java @@ -37,6 +37,7 @@ import tc.oc.pgm.util.event.PlayerTrampleBlockEvent; import tc.oc.pgm.util.event.entity.EntityDespawnInVoidEvent; import tc.oc.pgm.util.material.Materials; +import tc.oc.pgm.util.nms.NMSHacks; @ListenerScope(MatchScope.RUNNING) public class BlockDropsMatchModule implements MatchModule, Listener { @@ -106,7 +107,7 @@ private void giveKit(BlockDrops drops, MatchPlayer player) { } private void dropItems(BlockDrops drops, MatchPlayer player, Location location, double yield) { - if (player == null || player.getBukkit().getGameMode() != GameMode.CREATIVE) { + if (player == null || player.getGameMode() != GameMode.CREATIVE) { Random random = match.getRandom(); for (Map.Entry entry : drops.items.entrySet()) { if (random.nextFloat() < yield * entry.getValue()) { @@ -142,8 +143,7 @@ private void replaceBlock(BlockDrops drops, Block block, MatchPlayer player) { if (!event.isCancelled()) { BlockState state = block.getState(); - state.setType(drops.replacement.getItemType()); - state.setData(drops.replacement); + NMSHacks.setBlockStateData(state, drops.replacement); state.update(true, true); } } @@ -280,7 +280,7 @@ public void onBlockTrample(PlayerTrampleBlockEvent event) { replaceBlock(drops, event.getBlock(), player); - Location location = player.getBukkit().getLocation(); + Location location = player.getLocation(); dropObjects(drops, player, location, 1d, false); } } diff --git a/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsRuleSet.java b/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsRuleSet.java index 19de6f92d6..4645e50bb1 100644 --- a/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsRuleSet.java +++ b/core/src/main/java/tc/oc/pgm/blockdrops/BlockDropsRuleSet.java @@ -100,7 +100,6 @@ public BlockDrops getDrops( if (event instanceof BlockBreakEvent) { rightToolUsed = NMSHacks.canMineBlock(material, ((BlockBreakEvent) event).getPlayer().getItemInHand()); - ; } else { rightToolUsed = true; } diff --git a/core/src/main/java/tc/oc/pgm/filters/FilterMatchModule.java b/core/src/main/java/tc/oc/pgm/filters/FilterMatchModule.java index 5b9255e025..a8c6f2f71b 100644 --- a/core/src/main/java/tc/oc/pgm/filters/FilterMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/filters/FilterMatchModule.java @@ -48,8 +48,10 @@ import tc.oc.pgm.flag.event.FlagStateChangeEvent; import tc.oc.pgm.util.MapUtils; import tc.oc.pgm.util.MethodHandleUtils; +import tc.oc.pgm.util.bukkit.BukkitUtils; import tc.oc.pgm.util.collection.ContextStore; import tc.oc.pgm.util.event.PlayerCoarseMoveEvent; +import tc.oc.pgm.util.nms.NMSHacks; /** * Handler of dynamic filter magic. @@ -106,6 +108,9 @@ private static class ListenerSet { // Filterables that need a check in the next tick (cleared every tick) private final Set> dirtySet = new HashSet<>(); + // cleanup players from onPartyChange for non SportPaper servers + private Set nonSportCleanUpSet = new HashSet<>(); + public ContextStore getFilterContext() { return filterContext; } @@ -387,6 +392,15 @@ public void tick(Match match, Tick tick) { } public void tick() { + // Always empty when the server is running sportpaper + if (!nonSportCleanUpSet.isEmpty()) { + for (MatchPlayer matchPlayer : nonSportCleanUpSet) { + dirtySet.remove(matchPlayer); + this.lastResponses.columnKeySet().remove(matchPlayer); + } + nonSportCleanUpSet.clear(); + } + final Set> checked = new HashSet<>(); Set> checking; // Collect Filterables that are dirty, and have not already been checked in this tick @@ -483,7 +497,7 @@ public void onPlayerMove(PlayerCoarseMoveEvent event) { if (player != null) { this.invalidate(player); - PGM.get().getServer().postToMainThread(PGM.get(), true, this::tick); + NMSHacks.postToMainThread(PGM.get(), true, this::tick); } } @@ -530,11 +544,16 @@ public void onPartyChange(PlayerPartyChangeEvent event) throws EventException { } }); - event.yield(); + if (BukkitUtils.isSportPaper()) { + event.yield(); - // Wait until after the event to remove them, in case they get invalidated during the event. - dirtySet.remove(event.getPlayer()); - this.lastResponses.columnKeySet().remove(event.getPlayer()); + // Wait until after the event to remove them, in case they get invalidated during the event. + dirtySet.remove(event.getPlayer()); + this.lastResponses.columnKeySet().remove(event.getPlayer()); + } else { + // Clean up at the start of the next tick. Most platforms don't include event.yield() + nonSportCleanUpSet.add(event.getPlayer()); + } } } diff --git a/core/src/main/java/tc/oc/pgm/flag/LegacyFlagBeamMatchModule.java b/core/src/main/java/tc/oc/pgm/flag/LegacyFlagBeamMatchModule.java index 1ce50921cf..c6bfd52a44 100644 --- a/core/src/main/java/tc/oc/pgm/flag/LegacyFlagBeamMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/flag/LegacyFlagBeamMatchModule.java @@ -37,6 +37,7 @@ import tc.oc.pgm.flag.state.Spawned; import tc.oc.pgm.util.inventory.ItemBuilder; import tc.oc.pgm.util.nms.NMSHacks; +import tc.oc.pgm.util.nms.entity.fake.FakeEntity; @ListenerScope(MatchScope.LOADED) public class LegacyFlagBeamMatchModule implements MatchModule, Listener { @@ -133,8 +134,8 @@ public void onFlagStateChange(FlagStateChangeEvent event) { class Beam { private final Flag flag; - private final NMSHacks.FakeEntity base, legacyBase; - private final List segments; + private final FakeEntity base, legacyBase; + private final List segments; private final Set viewers = new HashSet<>(); @@ -142,13 +143,13 @@ class Beam { this.flag = flag; ItemStack wool = new ItemBuilder().material(Material.WOOL).color(flag.getDyeColor()).build(); - this.base = new NMSHacks.FakeArmorStand(match.getWorld(), wool); - this.legacyBase = new NMSHacks.FakeWitherSkull(match.getWorld()); + this.base = NMSHacks.fakeArmorStand(match.getWorld(), wool); + this.legacyBase = NMSHacks.fakeWitherSkull(match.getWorld()); this.segments = range(0, 64) // ~100 blocks is the height which the particles appear to be reasonably // visible (similar amount to amount closest to the flag), we limit this to 64 blocks // to reduce load on the client - .mapToObj(i -> new NMSHacks.FakeArmorStand(match.getWorld(), wool)) + .mapToObj(i -> NMSHacks.fakeArmorStand(match.getWorld(), wool)) .collect(Collectors.toList()); } @@ -169,7 +170,7 @@ Optional location() { return Optional.of(location); } - private NMSHacks.FakeEntity base(MatchPlayer player) { + private FakeEntity base(MatchPlayer player) { return player.isLegacy() ? legacyBase : base; } @@ -194,7 +195,7 @@ public void show(MatchPlayer player) { update(player); } - private void spawn(Player player, NMSHacks.FakeEntity entity) { + private void spawn(Player player, FakeEntity entity) { location().ifPresent(l -> entity.spawn(player, l)); } diff --git a/core/src/main/java/tc/oc/pgm/inventory/ViewInventoryMatchModule.java b/core/src/main/java/tc/oc/pgm/inventory/ViewInventoryMatchModule.java index 33e14f8134..e56ee72147 100644 --- a/core/src/main/java/tc/oc/pgm/inventory/ViewInventoryMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/inventory/ViewInventoryMatchModule.java @@ -53,6 +53,7 @@ import tc.oc.pgm.spawns.events.ParticipantSpawnEvent; import tc.oc.pgm.util.StringUtils; import tc.oc.pgm.util.attribute.Attribute; +import tc.oc.pgm.util.attribute.AttributeInstance; import tc.oc.pgm.util.bukkit.BukkitUtils; import tc.oc.pgm.util.named.NameStyle; import tc.oc.pgm.util.nms.NMSHacks; @@ -341,25 +342,30 @@ protected void previewPlayerInventory(Player viewer, PlayerInventory inventory) ChatColor.LIGHT_PURPLE + TextTranslations.translate("preview.doubleJump", viewer)); } - double knockbackResistance = - matchHolder.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE).getValue(); - if (knockbackResistance > 0) { - specialLore.add( - ChatColor.LIGHT_PURPLE - + TextTranslations.translate( - "preview.knockbackResistance", - viewer, - (int) Math.ceil(knockbackResistance * 100))); + AttributeInstance knockbackAttribute = + matchHolder.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE); + if (knockbackAttribute != null) { + double knockbackResistance = knockbackAttribute.getValue(); + if (knockbackResistance > 0) { + specialLore.add( + ChatColor.LIGHT_PURPLE + + TextTranslations.translate( + "preview.knockbackResistance", + viewer, + (int) Math.ceil(knockbackResistance * 100))); + } } - double knockbackReduction = holder.getKnockbackReduction(); - if (knockbackReduction > 0) { - specialLore.add( - ChatColor.LIGHT_PURPLE - + TextTranslations.translate( - "preview.knockbackReduction", - viewer, - (int) Math.ceil(knockbackReduction * 100))); + if (BukkitUtils.isSportPaper()) { + double knockbackReduction = holder.getKnockbackReduction(); + if (knockbackReduction > 0) { + specialLore.add( + ChatColor.LIGHT_PURPLE + + TextTranslations.translate( + "preview.knockbackReduction", + viewer, + (int) Math.ceil(knockbackReduction * 100))); + } } double walkSpeed = holder.getWalkSpeed(); diff --git a/core/src/main/java/tc/oc/pgm/listeners/BlockTransformListener.java b/core/src/main/java/tc/oc/pgm/listeners/BlockTransformListener.java index dc7c3a55d5..e83c7fa168 100644 --- a/core/src/main/java/tc/oc/pgm/listeners/BlockTransformListener.java +++ b/core/src/main/java/tc/oc/pgm/listeners/BlockTransformListener.java @@ -68,6 +68,7 @@ import tc.oc.pgm.util.event.block.BlockFallEvent; import tc.oc.pgm.util.event.entity.ExplosionPrimeByEntityEvent; import tc.oc.pgm.util.material.Materials; +import tc.oc.pgm.util.nms.NMSHacks; public class BlockTransformListener implements Listener { private static final BlockFace[] NEIGHBORS = { @@ -488,8 +489,7 @@ public void onBlockPistonExtend(final BlockPistonExtendEvent event) { new PistonExtensionMaterial(Material.PISTON_EXTENSION); pistonExtension.setFacingDirection(event.getDirection()); BlockState pistonExtensionState = event.getBlock().getRelative(event.getDirection()).getState(); - pistonExtensionState.setType(pistonExtension.getItemType()); - pistonExtensionState.setData(pistonExtension); + NMSHacks.setBlockStateData(pistonExtensionState, pistonExtension); newStates.put(event.getBlock(), pistonExtensionState); this.onPistonMove(event, event.getBlocks(), newStates); diff --git a/core/src/main/java/tc/oc/pgm/listeners/PGMListener.java b/core/src/main/java/tc/oc/pgm/listeners/PGMListener.java index 9fa87904dc..4ae98a1e37 100644 --- a/core/src/main/java/tc/oc/pgm/listeners/PGMListener.java +++ b/core/src/main/java/tc/oc/pgm/listeners/PGMListener.java @@ -54,6 +54,7 @@ import tc.oc.pgm.util.bukkit.WorldBorders; import tc.oc.pgm.util.event.PlayerCoarseMoveEvent; import tc.oc.pgm.util.nms.NMSHacks; +import tc.oc.pgm.util.skin.Skin; import tc.oc.pgm.util.text.TemporalComponent; import tc.oc.pgm.util.text.TextTranslations; @@ -369,7 +370,10 @@ public void announceDynamicMapPoolChange(MapPoolAdjustEvent event) { @EventHandler // We only need to store skins for the post match stats public void storeSkinOnMatchJoin(PlayerJoinMatchEvent event) { final MatchPlayer player = event.getPlayer(); - PGM.get().getDatastore().setSkin(player.getId(), NMSHacks.getPlayerSkin(player.getBukkit())); + Skin playerSkin = NMSHacks.getPlayerSkin(player.getBukkit()); + if (playerSkin != null) { + PGM.get().getDatastore().setSkin(player.getId(), playerSkin); + } } public void setGameRule(MatchLoadEvent event, String gameRule, boolean gameRuleValue) { diff --git a/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java index ef1e0e6ab1..7eb589a63c 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java @@ -360,10 +360,9 @@ private MoveMatchStage(Match match) { for (Player player : Bukkit.getOnlinePlayers()) { if (viewer.canSee(player) && viewer != player) players.add(player.getName()); } + String prefix = ChatColor.AQUA.toString(); NMSHacks.sendPacket( - viewer, - NMSHacks.teamCreatePacket( - "dummy", "dummy", ChatColor.AQUA.toString(), "", false, false, players)); + viewer, NMSHacks.teamCreatePacket("dummy", "dummy", prefix, "", false, false, players)); } int tpPerSecond = Integer.MAX_VALUE; @@ -400,7 +399,7 @@ private Stage advanceSync() { } // After all players have been teleported, remove the dummy team - NMSHacks.sendPacket(NMSHacks.teamRemovePacket("dummy")); + NMSHacks.sendDestroyTeamDummyPacket(); match.callEvent(new MatchAfterLoadEvent(match)); diff --git a/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java index 3666a353ea..8e2dbd3609 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchPlayerImpl.java @@ -54,7 +54,6 @@ import tc.oc.pgm.util.attribute.Attribute; import tc.oc.pgm.util.attribute.AttributeInstance; import tc.oc.pgm.util.attribute.AttributeMap; -import tc.oc.pgm.util.attribute.AttributeMapImpl; import tc.oc.pgm.util.attribute.AttributeModifier; import tc.oc.pgm.util.bukkit.ViaUtils; import tc.oc.pgm.util.named.NameStyle; @@ -95,7 +94,7 @@ public MatchPlayerImpl(Match match, Player player) { this.visible = new AtomicBoolean(false); this.protocolReady = new AtomicBoolean(ViaUtils.isReady(player)); this.protocolVersion = new AtomicInteger(ViaUtils.getProtocolVersion(player)); - this.attributeMap = new AttributeMapImpl(player); + this.attributeMap = NMSHacks.buildAttributeMap(player); } @Override @@ -329,7 +328,7 @@ public void setFrozen(boolean yes) { NMSHacks.spawnFreezeEntity(bukkit, FROZEN_VEHICLE_ENTITY_ID, isLegacy()); NMSHacks.entityAttach(bukkit, bukkit.getEntityId(), FROZEN_VEHICLE_ENTITY_ID, false); } else { - NMSHacks.destroyEntities(bukkit, FROZEN_VEHICLE_ENTITY_ID); + NMSHacks.sendPacket(bukkit, NMSHacks.destroyEntitiesPacket(FROZEN_VEHICLE_ENTITY_ID)); } resetInteraction(); } diff --git a/core/src/main/java/tc/oc/pgm/portals/PortalTransform.java b/core/src/main/java/tc/oc/pgm/portals/PortalTransform.java index d56100d538..915262ab79 100644 --- a/core/src/main/java/tc/oc/pgm/portals/PortalTransform.java +++ b/core/src/main/java/tc/oc/pgm/portals/PortalTransform.java @@ -56,7 +56,9 @@ private Vector mutate(Vector v) { private Location mutate(Location v) { Vector mutated = mutate(v.toVector()); - v.set(mutated.getX(), mutated.getY(), mutated.getZ()); + v.setX(mutated.getX()); + v.setY(mutated.getY()); + v.setZ(mutated.getZ()); v.setYaw((float) yaw.apply(v.getYaw())); v.setPitch((float) pitch.apply(v.getPitch())); return v; @@ -138,7 +140,9 @@ public Vector apply(Vector v) { public Location apply(Location v) { v = v.clone(); Vector region = to.getRandom(random); - v.set(region.getX(), region.getY(), region.getZ()); + v.setX(region.getX()); + v.setY(region.getY()); + v.setZ(region.getZ()); return v; } diff --git a/core/src/main/java/tc/oc/pgm/renewable/Renewable.java b/core/src/main/java/tc/oc/pgm/renewable/Renewable.java index d4c26da0a9..441babfa09 100644 --- a/core/src/main/java/tc/oc/pgm/renewable/Renewable.java +++ b/core/src/main/java/tc/oc/pgm/renewable/Renewable.java @@ -30,6 +30,7 @@ import tc.oc.pgm.util.block.BlockVectors; import tc.oc.pgm.util.material.MaterialCounter; import tc.oc.pgm.util.material.Materials; +import tc.oc.pgm.util.nms.NMSHacks; public class Renewable implements Listener, Tickable { @@ -266,8 +267,7 @@ boolean renew(BlockVector pos, MaterialData material) { Location location = pos.toLocation(match.getWorld()); Block block = location.getBlock(); BlockState newState = location.getBlock().getState(); - newState.setType(material.getItemType()); - newState.setData(material); + NMSHacks.setBlockStateData(newState, material); BlockRenewEvent event = new BlockRenewEvent(block, newState, this); match.callEvent(event); // Our own handler will get this and remove the block from the pool diff --git a/core/src/main/java/tc/oc/pgm/snapshot/BudgetWorldEdit.java b/core/src/main/java/tc/oc/pgm/snapshot/BudgetWorldEdit.java index 2c0d997472..d6ea1258b5 100644 --- a/core/src/main/java/tc/oc/pgm/snapshot/BudgetWorldEdit.java +++ b/core/src/main/java/tc/oc/pgm/snapshot/BudgetWorldEdit.java @@ -8,6 +8,7 @@ import tc.oc.pgm.api.match.Match; import tc.oc.pgm.api.region.Region; import tc.oc.pgm.util.BlockData; +import tc.oc.pgm.util.nms.NMSHacks; /** * Utils to save, remove and paste blocks in some {@link Region} in some {@link Match} using the @@ -33,7 +34,7 @@ public void placeBlocks(Region region, BlockVector offset) { for (BlockData blockData : snapshot.getMaterials(region)) { BlockState state = blockData.getBlock(world, offset).getState(); - state.setMaterialData(blockData.getMaterialData()); + NMSHacks.setBlockStateData(state, blockData.getMaterialData()); state.update(true, true); } } diff --git a/core/src/main/java/tc/oc/pgm/snapshot/WorldSnapshot.java b/core/src/main/java/tc/oc/pgm/snapshot/WorldSnapshot.java index a5cfa1f621..519a8f1f5b 100644 --- a/core/src/main/java/tc/oc/pgm/snapshot/WorldSnapshot.java +++ b/core/src/main/java/tc/oc/pgm/snapshot/WorldSnapshot.java @@ -52,7 +52,8 @@ public BlockState getOriginalBlock(int x, int y, int z) { ChunkSnapshot chunkSnapshot = chunkSnapshots.get(chunkVector); if (chunkSnapshot != null) { BlockVector chunkPos = chunkVector.worldToChunk(x, y, z); - state.setMaterialData( + NMSHacks.setBlockStateData( + state, new MaterialData( chunkSnapshot.getBlockTypeId( chunkPos.getBlockX(), chunkPos.getBlockY(), chunkPos.getBlockZ()), diff --git a/core/src/main/java/tc/oc/pgm/spawner/objects/SpawnablePotion.java b/core/src/main/java/tc/oc/pgm/spawner/objects/SpawnablePotion.java index 8fa7ad6cdc..4ca72b6e74 100644 --- a/core/src/main/java/tc/oc/pgm/spawner/objects/SpawnablePotion.java +++ b/core/src/main/java/tc/oc/pgm/spawner/objects/SpawnablePotion.java @@ -12,6 +12,7 @@ import tc.oc.pgm.spawner.Spawnable; import tc.oc.pgm.spawner.Spawner; import tc.oc.pgm.util.nms.NMSHacks; +import tc.oc.pgm.util.nms.entity.potion.EntityPotion; public class SpawnablePotion implements Spawnable { private final ItemStack potionItem; @@ -31,7 +32,7 @@ public SpawnablePotion(List potion, int damageValue, String spawne @Override public void spawn(Location location, Match match) { - NMSHacks.EntityPotion entityPotion = new NMSHacks.EntityPotion(location, potionItem); + EntityPotion entityPotion = NMSHacks.entityPotion(location, potionItem); entityPotion.spawn(); entityPotion .getBukkitEntity() diff --git a/core/src/main/java/tc/oc/pgm/tnt/TNTMatchModule.java b/core/src/main/java/tc/oc/pgm/tnt/TNTMatchModule.java index 6be3211143..1afe4e27a5 100644 --- a/core/src/main/java/tc/oc/pgm/tnt/TNTMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/tnt/TNTMatchModule.java @@ -69,6 +69,17 @@ public void yieldSet(EntityExplodeEvent event) { } } + private static Sound FUSE_SOUND = chooseFuseSound(); + + private static Sound chooseFuseSound() { + try { + return Sound.FUSE; + } catch (NoSuchFieldError error) { + // TODO: make or use a 1.8 -> 1.9+ sound conversion api + return Sound.valueOf("ENTITY_TNT_PRIMED"); + } + } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void handleInstantActivation(BlockPlaceEvent event) { if (this.properties.instantIgnite && event.getBlock().getType() == Material.TNT) { @@ -88,7 +99,7 @@ public void handleInstantActivation(BlockPlaceEvent event) { if (callPrimeEvent(tnt, event.getPlayer())) { event.setCancelled(true); // Allow the block to be placed if priming is cancelled - world.playSound(tnt.getLocation(), Sound.FUSE, 1, 1); + world.playSound(tnt.getLocation(), FUSE_SOUND, 1, 1); ItemStack inHand = event.getPlayer().getItemInHand(); if (inHand.getAmount() == 1) { diff --git a/core/src/main/java/tc/oc/pgm/tntrender/TNTRenderMatchModule.java b/core/src/main/java/tc/oc/pgm/tntrender/TNTRenderMatchModule.java index 73e19f0fd1..a405711bdf 100644 --- a/core/src/main/java/tc/oc/pgm/tntrender/TNTRenderMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/tntrender/TNTRenderMatchModule.java @@ -68,18 +68,19 @@ private class PrimedTnt { public PrimedTnt(TNTPrimed entity) { this.entity = entity; - this.lastLocation = currentLocation = entity.getLocation().toBlockLocation(); + this.lastLocation = currentLocation = toBlockLocation(entity.getLocation()); } public boolean update() { if (entity.isDead()) { - for (MatchPlayer viewer : viewers) + for (MatchPlayer viewer : viewers) { NMSHacks.sendBlockChange(currentLocation, viewer.getBukkit(), null); + } return true; } this.lastLocation = currentLocation; - this.currentLocation = entity.getLocation().toBlockLocation(); + this.currentLocation = toBlockLocation(entity.getLocation()); this.moved = !currentLocation.equals(lastLocation); for (MatchPlayer player : match.getPlayers()) { @@ -101,4 +102,13 @@ private void updatePlayer(MatchPlayer player) { } } } + + private static Location toBlockLocation(Location location) { + // Spigot 1.8 doesn't have Location.toBlockLocation() + Location blockLoc = location.clone(); + blockLoc.setX(blockLoc.getBlockX()); + blockLoc.setY(blockLoc.getBlockY()); + blockLoc.setZ(blockLoc.getBlockZ()); + return blockLoc; + } } diff --git a/util/src/main/java/tc/oc/pgm/util/block/BlockStates.java b/util/src/main/java/tc/oc/pgm/util/block/BlockStates.java index eb3d9263c7..181a9ce8b3 100644 --- a/util/src/main/java/tc/oc/pgm/util/block/BlockStates.java +++ b/util/src/main/java/tc/oc/pgm/util/block/BlockStates.java @@ -6,6 +6,7 @@ import org.bukkit.block.BlockState; import org.bukkit.material.MaterialData; import org.bukkit.util.BlockVector; +import tc.oc.pgm.util.nms.NMSHacks; public interface BlockStates { @@ -37,8 +38,7 @@ static BlockState cloneWithMaterial(Block block, MaterialData materialData) { static BlockState create(World world, BlockVector pos, MaterialData materialData) { BlockState state = pos.toLocation(world).getBlock().getState(); - state.setType(materialData.getItemType()); - state.setData(materialData); + NMSHacks.setBlockStateData(state, materialData); return state; } diff --git a/util/src/main/java/tc/oc/pgm/util/concurrent/RateLimiter.java b/util/src/main/java/tc/oc/pgm/util/concurrent/RateLimiter.java index ae356b6a88..9524c36dd8 100644 --- a/util/src/main/java/tc/oc/pgm/util/concurrent/RateLimiter.java +++ b/util/src/main/java/tc/oc/pgm/util/concurrent/RateLimiter.java @@ -1,6 +1,6 @@ package tc.oc.pgm.util.concurrent; -import org.bukkit.Bukkit; +import tc.oc.pgm.util.nms.NMSHacks; public class RateLimiter { private final int minDelay, maxDelay; @@ -36,7 +36,7 @@ public long getDelay() { long nextUpdate = (endedAt - now) + ((endedAt - startedAt) * timeRatio) - + (long) Math.max(0, (20 - Bukkit.getServer().spigot().getTPS()[0]) * tpsRatio); + + (long) Math.max(0, (20 - NMSHacks.getTPS()) * tpsRatio); return Math.max(timedOutUntil - now, 0) + Math.min(Math.max(minDelay, nextUpdate), maxDelay); } diff --git a/util/src/main/java/tc/oc/pgm/util/inventory/InventoryUtils.java b/util/src/main/java/tc/oc/pgm/util/inventory/InventoryUtils.java index 0eb0a99541..eeb7ed1fd5 100644 --- a/util/src/main/java/tc/oc/pgm/util/inventory/InventoryUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/inventory/InventoryUtils.java @@ -81,12 +81,11 @@ public static Collection getEffects(ItemStack potion) { PotionMeta meta = (PotionMeta) potion.getItemMeta(); if (meta.hasCustomEffects()) { return meta.getCustomEffects(); - } else { + } else if (potion.getType() == Material.POTION) { // Sanity check, SpawnablePotionBukkit return Potion.fromItemStack(potion).getEffects(); } - } else { - return Collections.emptyList(); } + return Collections.emptyList(); } public static @Nullable PotionEffectType getPrimaryEffectType(ItemStack potion) { diff --git a/util/src/main/java/tc/oc/pgm/util/nms/EnumPlayerInfoAction.java b/util/src/main/java/tc/oc/pgm/util/nms/EnumPlayerInfoAction.java new file mode 100644 index 0000000000..3eae83b777 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/EnumPlayerInfoAction.java @@ -0,0 +1,9 @@ +package tc.oc.pgm.util.nms; + +public enum EnumPlayerInfoAction { + ADD_PLAYER, + UPDATE_GAME_MODE, + UPDATE_LATENCY, + UPDATE_DISPLAY_NAME, + REMOVE_PLAYER, +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java index 0cdebd8091..a4edf6303a 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java @@ -1,888 +1,311 @@ package tc.oc.pgm.util.nms; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.SetMultimap; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.Property; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.*; +import java.util.Collection; +import java.util.Set; +import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; -import net.md_5.bungee.api.chat.BaseComponent; -import net.minecraft.server.v1_8_R3.*; -import net.minecraft.server.v1_8_R3.Item; -import net.minecraft.server.v1_8_R3.WorldBorder; -import org.bukkit.*; +import java.util.logging.Logger; +import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.ChunkSnapshot; +import org.bukkit.GameMode; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; -import org.bukkit.craftbukkit.v1_8_R3.CraftChunk; -import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; -import org.bukkit.craftbukkit.v1_8_R3.block.CraftBlock; -import org.bukkit.craftbukkit.v1_8_R3.entity.*; -import org.bukkit.craftbukkit.v1_8_R3.inventory.CraftItemStack; -import org.bukkit.craftbukkit.v1_8_R3.scoreboard.CraftTeam; -import org.bukkit.craftbukkit.v1_8_R3.util.CraftMagicNumbers; -import org.bukkit.entity.*; +import org.bukkit.WorldCreator; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; import org.bukkit.entity.Entity; +import org.bukkit.entity.Fireball; +import org.bukkit.entity.Firework; +import org.bukkit.entity.Item; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerTeleportEvent; -import org.bukkit.inventory.DoubleChestInventory; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.material.MaterialData; +import org.bukkit.plugin.Plugin; import org.bukkit.potion.PotionEffectType; import org.bukkit.scoreboard.NameTagVisibility; import org.bukkit.util.Vector; import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.util.ClassLogger; +import tc.oc.pgm.util.attribute.AttributeMap; import tc.oc.pgm.util.attribute.AttributeModifier; import tc.oc.pgm.util.block.RayBlockIntersection; import tc.oc.pgm.util.bukkit.BukkitUtils; -import tc.oc.pgm.util.bukkit.ViaUtils; -import tc.oc.pgm.util.reflect.ReflectionUtils; +import tc.oc.pgm.util.nms.entity.fake.FakeEntity; +import tc.oc.pgm.util.nms.entity.potion.EntityPotion; import tc.oc.pgm.util.skin.Skin; -import tc.oc.pgm.util.skin.Skins; public interface NMSHacks { - AtomicInteger ENTITY_IDS = new AtomicInteger(Integer.MAX_VALUE); - - static EntityTrackerEntry getTrackerEntry(net.minecraft.server.v1_8_R3.Entity nms) { - return ((WorldServer) nms.getWorld()).getTracker().trackedEntities.get(nms.getId()); - } - - static EntityTrackerEntry getTrackerEntry(Entity entity) { - return getTrackerEntry(((CraftEntity) entity).getHandle()); - } - - static void sendPacket(Object packet) { - for (Player pl : Bukkit.getOnlinePlayers()) { - sendPacket(pl, packet); - } - } + NMSHacksPlatform INSTANCE = chooseNMSHacks(); - static void sendPacket(Player bukkitPlayer, Object packet) { - if (bukkitPlayer.isOnline()) { - EntityPlayer nmsPlayer = ((CraftPlayer) bukkitPlayer).getHandle(); - nmsPlayer.playerConnection.sendPacket((Packet) packet); - } - } + static NMSHacksPlatform chooseNMSHacks() { + NMSHacksPlatform choice; + Logger logger = ClassLogger.get(NMSHacks.class); - 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) entry.trackedPlayers)) { - if (excludeSpectators) { - Entity spectatorTarget = viewer.getBukkitEntity().getSpectatorTarget(); - if (spectatorTarget != null && spectatorTarget.getUniqueId().equals(entity.getUniqueId())) - continue; - } - viewer.playerConnection.sendPacket((Packet) packet); - } - } - - /** Immediately send the given entity's metadata to all viewers in range */ - static void sendEntityMetadataToViewers(Entity entity, boolean complete) { - sendPacketToViewers(entity, entityMetadataPacket(entity.getEntityId(), entity, complete)); - } - - Constructor playerInfoDataConstructor = - getPlayerInfoDataConstructor(); - - static Constructor getPlayerInfoDataConstructor() { try { - Constructor constructor = - PacketPlayOutPlayerInfo.PlayerInfoData.class.getConstructor( - PacketPlayOutPlayerInfo.class, - GameProfile.class, - int.class, - WorldSettings.EnumGamemode.class, - IChatBaseComponent.class); - - constructor.setAccessible(true); - return constructor; - } catch (NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - - static PacketPlayOutPlayerInfo.PlayerInfoData playerListPacketData( - PacketPlayOutPlayerInfo packet, - UUID uuid, - String name, - GameMode gamemode, - int ping, - @Nullable Skin skin, - @Nullable String renderedDisplayName) { - GameProfile profile = new GameProfile(uuid, name); - if (skin != null) { - for (Map.Entry> entry : - Skins.toProperties(skin).asMap().entrySet()) { - profile.getProperties().putAll(entry.getKey(), entry.getValue()); + if (BukkitUtils.isSportPaper()) { + choice = new NMSHacksSportPaper(); + logger.info("Using NMSHacksSportPaper"); + } else { + choice = new NMSHacks1_8(); + logger.info("Using NMSHacks1_8"); } + } catch (Throwable throwable) { + logger.severe("You are trying to run PGM on an unsupported version!"); + throw throwable; } - - if (BukkitUtils.isSportPaper()) { - try { - PacketPlayOutPlayerInfo.PlayerInfoData data = - packet.constructData( - profile, - ping, - gamemode == null ? null : WorldSettings.EnumGamemode.getById(gamemode.getValue()), - null); // ELECTROID - data.jsonDisplayName = renderedDisplayName; - return data; - } catch (NoSuchFieldError ignored) { - } // Using an old SportPaper version, fallback to spigot reflection - } - - try { - WorldSettings.EnumGamemode enumGamemode = - gamemode == null ? null : WorldSettings.EnumGamemode.getById(gamemode.getValue()); - IChatBaseComponent iChatBaseComponent = - renderedDisplayName == null - ? null - : IChatBaseComponent.ChatSerializer.a(renderedDisplayName); - - return playerInfoDataConstructor.newInstance( - packet, profile, ping, enumGamemode, iChatBaseComponent); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - Field playerInfoActionField = ReflectionUtils.getField(PacketPlayOutPlayerInfo.class, "a"); - - static PacketPlayOutPlayerInfo createPlayerInfoPacket( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction action) { - PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo(); - if (BukkitUtils.isSportPaper()) { - packet.a = action; - } else { - ReflectionUtils.setField(packet, action, playerInfoActionField); - } - return packet; + return choice; } - static PacketPlayOutPlayerInfo.PlayerInfoData playerListPacketData( - PacketPlayOutPlayerInfo packet, UUID uuid, String renderedDisplayName) { - return playerListPacketData( - packet, uuid, "|" + uuid.toString().substring(0, 15), null, 0, null, renderedDisplayName); - } + AtomicInteger ENTITY_IDS = new AtomicInteger(Integer.MAX_VALUE); - static PacketPlayOutPlayerInfo.PlayerInfoData playerListPacketData( - PacketPlayOutPlayerInfo packet, UUID uuid) { - return playerListPacketData(packet, uuid, null, null, 0, null, null); + static int allocateEntityId() { + return ENTITY_IDS.decrementAndGet(); } - static PacketPlayOutPlayerInfo.PlayerInfoData playerListPacketData( - PacketPlayOutPlayerInfo packet, UUID uuid, int ping) { - return playerListPacketData( - packet, uuid, uuid.toString().substring(0, 16), null, ping, null, null); + static void sendPacket(Player bukkitPlayer, Object packet) { + INSTANCE.sendPacket(bukkitPlayer, packet); } - /** - * Removes all players from the tab for the viewer and re-adds them - * - * @param viewer The viewer to send the packets to - */ - static void removeAndAddAllTabPlayers(Player viewer) { - List players = new ArrayList<>(); - for (Player player : Bukkit.getOnlinePlayers()) { - if (viewer.canSee(player) || player == viewer) - players.add(((CraftPlayer) player).getHandle()); - } - - sendPacket( - viewer, - new PacketPlayOutPlayerInfo( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, players)); - sendPacket( - viewer, - new PacketPlayOutPlayerInfo( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, players)); - } - - Field bField = ReflectionUtils.getField(PacketPlayOutPlayerInfo.class, "b"); - - static List getPlayerInfoDataList( - PacketPlayOutPlayerInfo packet) { - // SportPaper makes this field public - if (BukkitUtils.isSportPaper()) { - return packet.b; - } else { - return (List) - ReflectionUtils.readField(packet, bField); - } + static void playDeathAnimation(Player player) { + INSTANCE.playDeathAnimation(player); } - enum TeamPacketFields { - a, - b, - c, - d, - e, - g, - h, - i; - - Field field; - - TeamPacketFields() { - field = ReflectionUtils.getField(PacketPlayOutScoreboardTeam.class, name()); - } - - public Field getField() { - return field; - } + static Object teleportEntityPacket(int entityId, Location location) { + return INSTANCE.teleportEntityPacket(entityId, location); } - static Packet teamPacket( - int operation, - String name, - String displayName, - String prefix, - String suffix, - boolean friendlyFire, - boolean seeFriendlyInvisibles, - NameTagVisibility nameTagVisibility, - Collection players) { - - PacketPlayOutScoreboardTeam packet = new PacketPlayOutScoreboardTeam(); - - if (BukkitUtils.isSportPaper()) { - packet.a = name; - packet.b = displayName; - packet.c = prefix; - packet.d = suffix; - packet.e = nameTagVisibility == null ? null : CraftTeam.bukkitToNotch(nameTagVisibility).e; - // packet.f = color - packet.g = players; - packet.h = operation; - if (friendlyFire) { - packet.i |= 1; - } - if (seeFriendlyInvisibles) { - packet.i |= 2; - } - } else { - ReflectionUtils.setField(packet, name, TeamPacketFields.a.getField()); - ReflectionUtils.setField(packet, displayName, TeamPacketFields.b.getField()); - ReflectionUtils.setField(packet, prefix, TeamPacketFields.c.getField()); - ReflectionUtils.setField(packet, suffix, TeamPacketFields.d.getField()); - - String e = null; - if (nameTagVisibility != null) { - switch (nameTagVisibility) { - case ALWAYS: - e = "always"; - break; - case NEVER: - e = "never"; - break; - case HIDE_FOR_OTHER_TEAMS: - e = "hideForOtherTeams"; - break; - case HIDE_FOR_OWN_TEAM: - e = "hideForOwnTeam"; - break; - } - } - - ReflectionUtils.setField(packet, e, TeamPacketFields.e.getField()); - ReflectionUtils.setField(packet, players, TeamPacketFields.g.getField()); - ReflectionUtils.setField(packet, operation, TeamPacketFields.h.getField()); - - int i = (int) ReflectionUtils.readField(packet, TeamPacketFields.i.getField()); - if (friendlyFire) { - i |= 1; - } - if (seeFriendlyInvisibles) { - i |= 2; - } - - ReflectionUtils.setField(packet, i, TeamPacketFields.i.getField()); - } - return packet; + static Object entityMetadataPacket(int entityId, Entity entity, boolean complete) { + return INSTANCE.entityMetadataPacket(entityId, entity, complete); } - static Packet teamCreatePacket( - String name, - String displayName, - String prefix, - String suffix, - boolean friendlyFire, - boolean seeFriendlyInvisibles, - Collection players) { - return teamPacket( - 0, - name, - displayName, - prefix, - suffix, - friendlyFire, - seeFriendlyInvisibles, - NameTagVisibility.ALWAYS, - players); + static void skipFireworksLaunch(Firework firework) { + INSTANCE.skipFireworksLaunch(firework); } - static Packet teamRemovePacket(String name) { - return teamPacket(1, name, null, null, null, false, false, null, Lists.newArrayList()); + static void fakePlayerItemPickup(Player player, Item item) { + INSTANCE.fakePlayerItemPickup(player, item); } - static Packet teamUpdatePacket( - String name, - String displayName, - String prefix, - String suffix, - boolean friendlyFire, - boolean seeFriendlyInvisibles) { - return teamPacket( - 2, - name, - displayName, - prefix, - suffix, - friendlyFire, - seeFriendlyInvisibles, - NameTagVisibility.ALWAYS, - Lists.newArrayList()); + static boolean isCraftItemArrowEntity(org.bukkit.entity.Item item) { + return INSTANCE.isCraftItemArrowEntity(item); } - static Packet teamJoinPacket(String name, Collection players) { - return teamPacket(3, name, null, null, null, false, false, null, players); + static void freezeEntity(Entity entity) { + INSTANCE.freezeEntity(entity); } - static Packet teamLeavePacket(String name, Collection players) { - return teamPacket(4, name, null, null, null, false, false, null, players); + static void setFireballDirection(Fireball entity, Vector direction) { + INSTANCE.setFireballDirection(entity, direction); } - static int allocateEntityId() { - return ENTITY_IDS.decrementAndGet(); + static void removeAndAddAllTabPlayers(Player viewer) { + INSTANCE.removeAndAddAllTabPlayers(viewer); } static void sendLegacyWearing(Player player, int slot, ItemStack item) { - Packet packet = - new PacketPlayOutEntityEquipment( - player.getEntityId(), slot, CraftItemStack.asNMSCopy(item)); - EntityTrackerEntry entry = getTrackerEntry(player); - for (EntityPlayer viewer : entry.trackedPlayers) { - if (ViaUtils.getProtocolVersion(viewer.getBukkitEntity()) <= ViaUtils.VERSION_1_7) - viewer.playerConnection.sendPacket(packet); - } + INSTANCE.sendLegacyWearing(player, slot, item); } - class EntityMetadata { - public final DataWatcher dataWatcher; - - public EntityMetadata(DataWatcher watcher) { - dataWatcher = watcher; - } - - static EntityMetadata clone(DataWatcher original) { - List values = original.c(); - DataWatcher copy = new DataWatcher(null); - for (DataWatcher.WatchableObject value : values) { - copy.a(value.a(), value.b()); - } - return new EntityMetadata(copy); - } - - static EntityMetadata clone(Entity entity) { - return clone(((CraftEntity) entity).getHandle().getDataWatcher()); - } - - @Override - public EntityMetadata clone() { - return clone(this.dataWatcher); - } + static EntityPotion entityPotion(Location location, ItemStack potionItem) { + return INSTANCE.entityPotion(location, potionItem); } - static Packet destroyEntitiesPacket(int... entityIds) { - return new PacketPlayOutEntityDestroy(entityIds); - } - - static void destroyEntities(Player player, int... entityIds) { - sendPacket(player, destroyEntitiesPacket(entityIds)); + static void sendBlockChange(Location loc, Player player, @Nullable Material material) { + INSTANCE.sendBlockChange(loc, player, material); } - static Packet spawnPlayerPacket(int entityId, UUID uuid, Location location, Player player) { - return spawnPlayerPacket(entityId, uuid, location, null, EntityMetadata.clone(player)); + static void updateChunkSnapshot(ChunkSnapshot snapshot, org.bukkit.block.BlockState blockState) { + INSTANCE.updateChunkSnapshot(snapshot, blockState); } - enum NamedEntitySpawnFields { - a, - b, - c, - d, - e, - f, - g, - h, - i, - j; - - Field field; - - NamedEntitySpawnFields() { - field = ReflectionUtils.getField(PacketPlayOutNamedEntitySpawn.class, name()); - } - - public Field getField() { - return field; - } + static void setKnockbackReduction(Player player, float amount) { + INSTANCE.setKnockbackReduction(player, amount); } - static Packet spawnPlayerPacket( - int entityId, UUID uuid, Location location, ItemStack heldItem, EntityMetadata metadata) { - if (BukkitUtils.isSportPaper()) { - return new PacketPlayOutNamedEntitySpawn( - entityId, - uuid, - location.getX(), - location.getY(), - location.getZ(), - (byte) location.getYaw(), - (byte) location.getPitch(), - CraftItemStack.asNMSCopy(heldItem), - metadata.dataWatcher); - } else { - PacketPlayOutNamedEntitySpawn packet = new PacketPlayOutNamedEntitySpawn(); - - ReflectionUtils.setField(packet, entityId, NamedEntitySpawnFields.a.getField()); - ReflectionUtils.setField(packet, uuid, NamedEntitySpawnFields.b.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(location.getX() * 32.0D), NamedEntitySpawnFields.c.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(location.getY() * 32.0D), NamedEntitySpawnFields.d.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(location.getZ() * 32.0D), NamedEntitySpawnFields.e.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) location.getYaw()) * 256.0F / 360.0F)), - NamedEntitySpawnFields.f.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) location.getPitch()) * 256.0F / 360.0F)), - NamedEntitySpawnFields.g.getField()); - ReflectionUtils.setField( - packet, - heldItem == null ? 0 : Item.getId(CraftItemStack.asNMSCopy(heldItem).getItem()), - NamedEntitySpawnFields.h.getField()); - ReflectionUtils.setField(packet, metadata.dataWatcher, NamedEntitySpawnFields.i.getField()); - ReflectionUtils.setField( - packet, metadata.dataWatcher.b(), NamedEntitySpawnFields.j.getField()); - - return packet; - } + static void showInvisibles(Player player, boolean showInvisibles) { + INSTANCE.showInvisibles(player, showInvisibles); } - static void spawnLivingEntity( - Player player, EntityType type, int entityId, Location location, EntityMetadata metadata) { - sendPacket(player, spawnLivingEntityPacket(type, entityId, location, metadata)); + static void setAffectsSpawning(Player player, boolean affectsSpawning) { + INSTANCE.setAffectsSpawning(player, affectsSpawning); } - enum LivingEntitySpawnFields { - a, - b, - c, - d, - e, - i, - j, - k, - l; - - Field field; - - LivingEntitySpawnFields() { - field = ReflectionUtils.getField(PacketPlayOutSpawnEntityLiving.class, name()); - } - - public Field getField() { - return field; - } + static void clearArrowsInPlayer(Player player) { + INSTANCE.clearArrowsInPlayer(player); } - @SuppressWarnings("deprecation") - static Packet spawnLivingEntityPacket( - EntityType type, int entityId, Location location, EntityMetadata metadata) { - if (BukkitUtils.isSportPaper()) { - return new PacketPlayOutSpawnEntityLiving( - entityId, - (byte) type.getTypeId(), - location.getX(), - location.getY(), - location.getZ(), - location.getYaw(), - location.getPitch(), - location.getPitch(), - 0, - 0, - 0, - metadata.dataWatcher); - } else { - PacketPlayOutSpawnEntityLiving packet = new PacketPlayOutSpawnEntityLiving(); - - ReflectionUtils.setField(packet, entityId, LivingEntitySpawnFields.a.getField()); - ReflectionUtils.setField( - packet, (byte) type.getTypeId(), LivingEntitySpawnFields.b.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(location.getX() * 32.0D), LivingEntitySpawnFields.c.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(location.getY() * 32.0D), LivingEntitySpawnFields.d.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(location.getZ() * 32.0D), LivingEntitySpawnFields.e.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) location.getYaw()) * 256.0F / 360.0F)), - LivingEntitySpawnFields.i.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) location.getPitch()) * 256.0F / 360.0F)), - LivingEntitySpawnFields.j.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) location.getPitch()) * 256.0F / 360.0F)), - LivingEntitySpawnFields.k.getField()); - ReflectionUtils.setField(packet, metadata.dataWatcher, LivingEntitySpawnFields.l.getField()); - - return packet; - } + static void showBorderWarning(Player player, boolean show) { + INSTANCE.showBorderWarning(player, show); } - static void spawnEntity(Player player, int type, int entityId, Location location) { - sendPacket(player, spawnEntityPacket(type, entityId, location)); + static PotionEffectType getPotionEffectType(String key) { + return INSTANCE.getPotionEffectType(key); } - enum EntitySpawnFields { - a, - b, - c, - d, - h, - i, - j; - - Field field; - - EntitySpawnFields() { - field = ReflectionUtils.getField(PacketPlayOutSpawnEntity.class, name()); - } - - public Field getField() { - return field; - } + static org.bukkit.enchantments.Enchantment getEnchantment(String key) { + return INSTANCE.getEnchantment(key); } - static Packet spawnEntityPacket(int type, int entityId, Location location) { - if (BukkitUtils.isSportPaper()) { - return new PacketPlayOutSpawnEntity( - entityId, - location.getX(), - location.getY(), - location.getZ(), - 0, - 0, - 0, - (int) location.getPitch(), - (int) location.getYaw(), - type, - 0); - } else { - PacketPlayOutSpawnEntity packet = new PacketPlayOutSpawnEntity(); - - ReflectionUtils.setField(packet, entityId, EntitySpawnFields.a.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(location.getX() * 32.0D), EntitySpawnFields.b.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(location.getY() * 32.0D), EntitySpawnFields.c.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(location.getZ() * 32.0D), EntitySpawnFields.d.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) location.getYaw()) * 256.0F / 360.0F)), - EntitySpawnFields.h.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) location.getPitch()) * 256.0F / 360.0F)), - EntitySpawnFields.i.getField()); - ReflectionUtils.setField(packet, type, EntitySpawnFields.j.getField()); - - return packet; - } + static long getMonotonicTime(World world) { + return INSTANCE.getMonotonicTime(world); } - static void spawnFreezeEntity(Player player, int entityId, boolean legacy) { - if (legacy) { - Location location = player.getLocation().add(0, 0.286, 0); - if (location.getY() < -64) { - location.setY(-64); - player.teleport(location); - } - - NMSHacks.spawnEntity(player, 66, entityId, location); - } else { - Location loc = player.getLocation().subtract(0, 1.1, 0); - - NMSHacks.EntityMetadata metadata = NMSHacks.createEntityMetadata(); - NMSHacks.setEntityMetadata(metadata, false, false, false, false, true, (short) 0); - NMSHacks.setArmorStandFlags(metadata, false, false, false, false); - NMSHacks.spawnLivingEntity(player, EntityType.ARMOR_STAND, entityId, loc, metadata); - } + static int getPing(Player player) { + return INSTANCE.getPing(player); } - enum EntityAttachFields { - a, - b, - c; - - Field field; - - EntityAttachFields() { - field = ReflectionUtils.getField(PacketPlayOutAttachEntity.class, name()); - } - - public Field getField() { - return field; - } + static Object entityEquipmentPacket(int entityId, int slot, ItemStack armor) { + return INSTANCE.entityEquipmentPacket(entityId, slot, armor); } static void entityAttach(Player player, int entityID, int vehicleID, boolean leash) { - if (BukkitUtils.isSportPaper()) { - sendPacket(player, new PacketPlayOutAttachEntity(entityID, vehicleID, leash)); - } else { - PacketPlayOutAttachEntity packet = new PacketPlayOutAttachEntity(); - - ReflectionUtils.setField(packet, (byte) (leash ? 1 : 0), EntityAttachFields.a.getField()); - ReflectionUtils.setField(packet, entityID, EntityAttachFields.b.getField()); - ReflectionUtils.setField(packet, vehicleID, EntityAttachFields.c.getField()); + INSTANCE.entityAttach(player, entityID, vehicleID, leash); + } - sendPacket(player, packet); - } + static void resumeServer() { + INSTANCE.resumeServer(); } - static Packet teleportEntityPacket(int entityId, Location location) { - return new PacketPlayOutEntityTeleport( - entityId, // Entity ID - (int) (location.getX() * 32), // World X * 32 - (int) (location.getY() * 32), // World Y * 32 - (int) (location.getZ() * 32), // World Z * 32 - (byte) (location.getYaw() * 256 / 360), // Yaw - (byte) (location.getPitch() * 256 / 360), // Pitch - true); // On Ground + Height Correction + static Inventory createFakeInventory(Player viewer, Inventory realInventory) { + return INSTANCE.createFakeInventory(viewer, realInventory); } - static Packet entityMetadataPacket(int entityId, Entity entity, boolean complete) { - return new PacketPlayOutEntityMetadata( - entityId, - ((CraftEntity) entity).getHandle().getDataWatcher(), - complete); // true = all values, false = only dirty values + static FakeEntity fakeWitherSkull(World world) { + return INSTANCE.fakeWitherSkull(world); } - static EntityMetadata createEntityMetadata() { - return new EntityMetadata(new DataWatcher(null)); + static FakeEntity fakeArmorStand(World world, ItemStack head) { + return INSTANCE.fakeArmorStand(world, head); } - static void setEntityMetadata(EntityMetadata metadata, byte flags, short air) { - DataWatcher dataWatcher = metadata.dataWatcher; - dataWatcher.a(0, (byte) flags); - dataWatcher.a(1, (short) air); + static Set getBlocks(Chunk bukkitChunk, Material material) { + return INSTANCE.getBlocks(bukkitChunk, material); } - static void setEntityMetadata( - EntityMetadata metadata, - boolean onFire, - boolean crouched, - boolean sprinting, - boolean eatingOrBlocking, - boolean invisible, - short air) { - int flags = 0; - if (onFire) flags |= 0x01; - if (crouched) flags |= 0x02; - if (sprinting) flags |= 0x08; - if (eatingOrBlocking) flags |= 0x10; - if (invisible) flags |= 0x20; - setEntityMetadata(metadata, (byte) flags, air); + static Object spawnPlayerPacket(int entityId, UUID uuid, Location location, Player player) { + return INSTANCE.spawnPlayerPacket(entityId, uuid, location, player); } - static void setArmorStandFlags( - EntityMetadata metadata, boolean small, boolean gravity, boolean arms, boolean baseplate) { - int flags = 0; - if (small) flags |= 0x01; - if (gravity) flags |= 0x02; - if (arms) flags |= 0x04; - if (baseplate) flags |= 0x08; - metadata.dataWatcher.a(10, (byte) flags); + static Object destroyEntitiesPacket(int... entityIds) { + return INSTANCE.destroyEntitiesPacket(entityIds); } - Method enablePotionParticlesMethod = ReflectionUtils.getMethod(EntityLiving.class, "B"); - Method disablePotionParticlesMethod = ReflectionUtils.getMethod(EntityLiving.class, "bj"); + static Object createPlayerInfoPacket(EnumPlayerInfoAction action) { + return INSTANCE.createPlayerInfoPacket(action); + } static void setPotionParticles(Player player, boolean enabled) { - if (BukkitUtils.isSportPaper()) { - player.setPotionParticles(enabled); - } else { - CraftPlayer craftPlayer = (CraftPlayer) player; - EntityPlayer handle = craftPlayer.getHandle(); - - if (enabled) { - ReflectionUtils.callMethod(enablePotionParticlesMethod, handle); - } else { - ReflectionUtils.callMethod(disablePotionParticlesMethod, handle); - } - } + INSTANCE.setPotionParticles(player, enabled); } - static void clearArrowsInPlayer(Player player) { - ((CraftPlayer) player).getHandle().o(0); + static ItemStack craftItemCopy(ItemStack item) { + return INSTANCE.craftItemCopy(item); } - /** - * Test if the given tool is capable of "efficiently" mining the given block. - * - *

Derived from CraftBlock.itemCausesDrops() - */ - static boolean canMineBlock(MaterialData blockMaterial, ItemStack tool) { - if (!blockMaterial.getItemType().isBlock()) { - throw new IllegalArgumentException("Material '" + blockMaterial + "' is not a block"); - } - - net.minecraft.server.v1_8_R3.Block nmsBlock = - CraftMagicNumbers.getBlock(blockMaterial.getItemType()); - net.minecraft.server.v1_8_R3.Item nmsTool = - tool == null ? null : CraftMagicNumbers.getItem(tool.getType()); - - return nmsBlock != null - && (nmsBlock.getMaterial().isAlwaysDestroyable() - || (nmsTool != null && nmsTool.canDestroySpecialBlock(nmsBlock))); + static RayBlockIntersection getTargetedBLock(Player player) { + return INSTANCE.getTargetedBLock(player); } - static long getMonotonicTime(World world) { - return ((CraftWorld) world).getHandle().getTime(); + static boolean playerInfoDataListNotEmpty(Object packet) { + return INSTANCE.playerInfoDataListNotEmpty(packet); } - static void sendMessage(Player player, BaseComponent[] message, int position) { - PacketPlayOutChat packet = new PacketPlayOutChat(null, (byte) position); - packet.components = message; - sendPacket(player, packet); + static Object playerListPacketData( + Object packetPlayOutPlayerInfo, + UUID uuid, + String name, + GameMode gamemode, + int ping, + @Nullable Skin skin, + @Nullable String renderedDisplayName) { + return INSTANCE.playerListPacketData( + packetPlayOutPlayerInfo, uuid, name, gamemode, ping, skin, renderedDisplayName); } - static void showBorderWarning(Player player, boolean show) { - WorldBorder border = new WorldBorder(); - border.setWarningDistance(show ? Integer.MAX_VALUE : 0); - sendPacket( - player, - new PacketPlayOutWorldBorder( - border, PacketPlayOutWorldBorder.EnumWorldBorderAction.SET_WARNING_BLOCKS)); + static void addPlayerInfoToPacket(Object packet, UUID uuid, int ping) { + INSTANCE.addPlayerInfoToPacket( + packet, + playerListPacketData( + packet, uuid, uuid.toString().substring(0, 16), null, ping, null, null)); } - Field entityMetadataWatchableField = - ReflectionUtils.getField(PacketPlayOutEntityMetadata.class, "b"); - - static void playDeathAnimation(Player player) { - EntityPlayer handle = ((CraftPlayer) player).getHandle(); - PacketPlayOutEntityMetadata metadata = - new PacketPlayOutEntityMetadata(handle.getId(), handle.getDataWatcher(), false); - - // Add/replace health to zero - boolean replaced = false; - DataWatcher.WatchableObject zeroHealth = - new DataWatcher.WatchableObject(3, 6, 0f); // type 3 (float), index 6 (health) - - List b = - (List) - ReflectionUtils.readField(metadata, entityMetadataWatchableField); - if (b != null) { - for (int i = 0; i < b.size(); i++) { - DataWatcher.WatchableObject wo = b.get(i); - if (wo.a() == 6) { - b.set(i, zeroHealth); - replaced = true; - } - } - } - - if (!replaced) { - if (b != null) b.add(zeroHealth); - else - ReflectionUtils.setField( - metadata, Collections.singletonList(zeroHealth), entityMetadataWatchableField); - } - - Location location = player.getLocation(); - PacketPlayOutBed useBed = - new PacketPlayOutBed( - ((CraftPlayer) player).getHandle(), - new BlockPosition(location.getX(), location.getY(), location.getZ())); - - Packet teleport = teleportEntityPacket(player.getEntityId(), location); + static void addPlayerInfoToPacket(Object packet, UUID uuid) { + INSTANCE.addPlayerInfoToPacket( + packet, playerListPacketData(packet, uuid, null, null, 0, null, null)); + } - sendPacketToViewers(player, metadata, true); - sendPacketToViewers(player, useBed, true); - sendPacketToViewers(player, teleport, true); + static void addPlayerInfoToPacket(Object packet, UUID uuid, String renderedDisplayName) { + INSTANCE.addPlayerInfoToPacket( + packet, + playerListPacketData( + packet, + uuid, + "|" + uuid.toString().substring(0, 15), + null, + 0, + null, + renderedDisplayName)); } - static org.bukkit.enchantments.Enchantment getEnchantment(String key) { - Enchantment enchantment = Enchantment.getByName(key); - return enchantment == null ? null : org.bukkit.enchantments.Enchantment.getById(enchantment.id); + static void addPlayerInfoToPacket( + Object packetPlayOutPlayerInfo, + UUID uuid, + String name, + GameMode gamemode, + int ping, + @Nullable Skin skin, + @Nullable String renderedDisplayName) { + INSTANCE.addPlayerInfoToPacket( + packetPlayOutPlayerInfo, + playerListPacketData( + packetPlayOutPlayerInfo, uuid, name, gamemode, ping, skin, renderedDisplayName)); } - static PotionEffectType getPotionEffectType(String key) { - MobEffectList nms = MobEffectList.b(key); - return nms == null ? null : PotionEffectType.getById(nms.id); + static void setSkullMetaOwner(SkullMeta meta, String name, UUID uuid, Skin skin) { + INSTANCE.setSkullMetaOwner(meta, name, uuid, skin); } - static Set getBlockStates(Material material) { - ImmutableSet.Builder materials = ImmutableSet.builder(); - Block nmsBlock = CraftMagicNumbers.getBlock(material); - List states = - ReflectionUtils.readField(BlockStateList.class, nmsBlock.P(), List.class, "e"); - if (states != null) { - for (IBlockData state : states) { - int data = nmsBlock.toLegacyData(state); - materials.add(material.getNewData((byte) data)); - } - } - return materials.build(); + static WorldCreator detectWorld(String worldName) { + return INSTANCE.detectWorld(worldName); } static void setAbsorption(LivingEntity entity, double health) { - ((CraftLivingEntity) entity).getHandle().setAbsorptionHearts((float) health); + INSTANCE.setAbsorption(entity, health); } static double getAbsorption(LivingEntity entity) { - return ((CraftLivingEntity) entity).getHandle().getAbsorptionHearts(); + return INSTANCE.getAbsorption(entity); } - static int getPing(Player player) { - return ((CraftPlayer) player).getHandle().ping; + @Deprecated + static Set getBlockStates(Material material) { + return INSTANCE.getBlockStates(material); } - static Packet entityEquipmentPacket(int entityId, int slot, ItemStack armor) { - return new PacketPlayOutEntityEquipment(entityId, slot, CraftItemStack.asNMSCopy(armor)); + static void setBlockStateData(BlockState state, MaterialData materialData) { + INSTANCE.setBlockStateData(state, materialData); } static Skin getPlayerSkin(Player player) { - CraftPlayer craftPlayer = (CraftPlayer) player; - return Skins.fromProperties(craftPlayer.getProfile().getProperties()); + return INSTANCE.getPlayerSkin(player); + } + + static Skin getPlayerSkinForViewer(Player player, Player viewer) { + return INSTANCE.getPlayerSkinForViewer(player, viewer); } static void updateVelocity(Player player) { - EntityPlayer handle = ((CraftPlayer) player).getHandle(); - handle.velocityChanged = false; - handle.playerConnection.sendPacket(new PacketPlayOutEntityVelocity(handle)); + INSTANCE.updateVelocity(player); } static boolean teleportRelative( @@ -891,499 +314,141 @@ static boolean teleportRelative( float deltaYaw, float deltaPitch, PlayerTeleportEvent.TeleportCause cause) { - CraftPlayer craftPlayer = (CraftPlayer) player; - - if (craftPlayer.getHandle().playerConnection == null - || craftPlayer.getHandle().playerConnection.isDisconnected()) { - return false; - } - - // From = Players current Location - Location from = player.getLocation(); - // To = Players new Location if Teleport is Successful - Location to = from.clone().add(deltaPos); - to.setYaw(to.getYaw() + deltaYaw); - to.setPitch(to.getPitch() + deltaPitch); - - // Create & Call the Teleport Event. - PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, cause); - Bukkit.getPluginManager().callEvent(event); - - // Return False to inform the Plugin that the Teleport was unsuccessful/cancelled. - if (event.isCancelled()) { - return false; - } - - craftPlayer.getHandle().playerConnection.teleport(to); - return true; + return INSTANCE.teleportRelative(player, deltaPos, deltaYaw, deltaPitch, cause); } - Field skullProfileField = - ReflectionUtils.getField( - "org.bukkit.craftbukkit.v1_8_R3.inventory.CraftMetaSkull", "profile"); - - static void setSkullMetaOwner(SkullMeta meta, String name, UUID uuid, Skin skin) { - GameProfile gameProfile = new GameProfile(uuid, name); - Skins.setProperties(skin, gameProfile.getProperties()); - ReflectionUtils.setField(meta, gameProfile, skullProfileField); - } - - static Set getBlocks(Chunk bukkitChunk, Material material) { - CraftChunk craftChunk = (CraftChunk) bukkitChunk; - Set blocks = new HashSet<>(); - - net.minecraft.server.v1_8_R3.Block nmsBlock = CraftMagicNumbers.getBlock(material); - net.minecraft.server.v1_8_R3.Chunk chunk = craftChunk.getHandle(); - - for (ChunkSection section : chunk.getSections()) { - if (section == null || section.a()) continue; // ChunkSection.a() -> true if section is empty - - char[] blockIds = section.getIdArray(); - for (int i = 0; i < blockIds.length; i++) { - // This does a lookup in the block registry, but does not create any objects, so should be - // pretty efficient - IBlockData blockData = (IBlockData) net.minecraft.server.v1_8_R3.Block.d.a(blockIds[i]); - if (blockData != null && blockData.getBlock() == nmsBlock) { - blocks.add( - bukkitChunk.getBlock(i & 0xf, section.getYPosition() | (i >> 8), (i >> 4) & 0xf)); - } - } - } - - return blocks; + static void spawnFreezeEntity(Player player, int entityId, boolean legacy) { + INSTANCE.spawnFreezeEntity(player, entityId, legacy); } - static WorldCreator detectWorld(String worldName) { - IDataManager sdm = - new ServerNBTManager(Bukkit.getServer().getWorldContainer(), worldName, true); - WorldData worldData = sdm.getWorldData(); - if (worldData == null) return null; - - return new WorldCreator(worldName) - .generateStructures(worldData.shouldGenerateMapFeatures()) - .generatorSettings(worldData.getGeneratorOptions()) - .seed(worldData.getSeed()) - .type(org.bukkit.WorldType.getByName(worldData.getType().name())); + /** + * Test if the given tool is capable of "efficiently" mining the given block. + * + *

Derived from CraftBlock.itemCausesDrops() + */ + @Deprecated + static boolean canMineBlock(MaterialData blockMaterial, ItemStack tool) { + return INSTANCE.canMineBlock(blockMaterial, tool); } - Field worldServerField = ReflectionUtils.getField(CraftWorld.class, "world"); - Field dimensionField = ReflectionUtils.getField(WorldServer.class, "dimension"); - Field modifiersField = ReflectionUtils.getField(Field.class, "modifiers"); - static void resetDimension(World world) { - try { - modifiersField.setInt(dimensionField, dimensionField.getModifiers() & ~Modifier.FINAL); - - dimensionField.set(worldServerField.get(world), 11); - } catch (IllegalAccessException e) { - // No-op, newer version of Java have disabled modifying final fields - } + INSTANCE.resetDimension(world); } - static RayBlockIntersection getTargetedBLock(Player player) { - Location start = player.getEyeLocation(); - World world = player.getWorld(); - Vector startVector = start.toVector(); - Vector end = - start - .toVector() - .add( - start.getDirection().multiply(player.getGameMode() == GameMode.CREATIVE ? 6 : 4.5)); - MovingObjectPosition hit = - ((CraftWorld) world) - .getHandle() - .rayTrace( - new Vec3D(startVector.getX(), startVector.getY(), startVector.getZ()), - new Vec3D(end.getX(), end.getY(), end.getZ()), - false, - false, - false); - if (hit != null && hit.type == MovingObjectPosition.EnumMovingObjectType.BLOCK) { - return new RayBlockIntersection( - world.getBlockAt(hit.a().getX(), hit.a().getY(), hit.a().getZ()), - CraftBlock.notchToBlockFace(hit.direction), - new Vector(hit.pos.a, hit.pos.b, hit.pos.c)); - } else { - return null; - } - } - - static ItemStack craftItemCopy(ItemStack item) { - return CraftItemStack.asCraftCopy(item); - } - - String CAN_DESTROY = "CanDestroy"; - String CAN_PLACE_ON = "CanPlaceOn"; - static void setCanDestroy(ItemMeta itemMeta, Collection materials) { - if (BukkitUtils.isSportPaper()) { - // Since this is handled by SportPaper this can't be done through unhandled tags - itemMeta.setCanDestroy(materials); - } - setMaterialList(itemMeta, materials, CAN_DESTROY); + INSTANCE.setCanDestroy(itemMeta, materials); } static Set getCanDestroy(ItemMeta itemMeta) { - if (BukkitUtils.isSportPaper()) { - // Since this is handled by SportPaper this can't be done through unhandled tags - return itemMeta.getCanDestroy(); - } - return getMaterialCollection(itemMeta, CAN_DESTROY); + return INSTANCE.getCanDestroy(itemMeta); } static void setCanPlaceOn(ItemMeta itemMeta, Collection materials) { - if (BukkitUtils.isSportPaper()) { - // Since this is handled by SportPaper this can't be done through unhandled tags - itemMeta.setCanPlaceOn(materials); - } - setMaterialList(itemMeta, materials, CAN_PLACE_ON); + INSTANCE.setCanPlaceOn(itemMeta, materials); } static Set getCanPlaceOn(ItemMeta itemMeta) { - if (BukkitUtils.isSportPaper()) { - // Since this is handled by SportPaper this can't be done through unhandled tags - return itemMeta.getCanPlaceOn(); - } - return getMaterialCollection(itemMeta, CAN_PLACE_ON); - } - - Field unhandledTagsField = - ReflectionUtils.getField( - "org.bukkit.craftbukkit.v1_8_R3.inventory.CraftMetaItem", "unhandledTags"); - - static Map getUnhandledTags(ItemMeta meta) { - return (Map) ReflectionUtils.readField(meta, unhandledTagsField); - } - - static void setMaterialList( - ItemMeta itemMeta, Collection materials, String canPlaceOn) { - Map unhandledTags = getUnhandledTags(itemMeta); - NBTTagList canDestroyList = - unhandledTags.containsKey(canPlaceOn) - ? (NBTTagList) unhandledTags.get(canPlaceOn) - : new NBTTagList(); - for (Material material : materials) { - Block block = Block.getById(material.getId()); - if (block != null) { - canDestroyList.add(new NBTTagString(Block.REGISTRY.c(block).toString())); - } - } - if (!canDestroyList.isEmpty()) unhandledTags.put(canPlaceOn, canDestroyList); - } - - Field nbtListField = ReflectionUtils.getField(NBTTagList.class, "list"); - - static Set getMaterialCollection(ItemMeta itemMeta, String key) { - Map unhandledTags = getUnhandledTags(itemMeta); - if (!unhandledTags.containsKey(key)) return EnumSet.noneOf(Material.class); - EnumSet materialSet = EnumSet.noneOf(Material.class); - NBTTagList canDestroyList = (NBTTagList) unhandledTags.get(key); - - for (NBTBase item : (List) ReflectionUtils.readField(canDestroyList, nbtListField)) { - NBTTagString nbtTagString = (NBTTagString) item; - String blockString = nbtTagString.a_(); - materialSet.add(Material.getMaterial(Block.getId(Block.getByName(blockString)))); - } - - return materialSet; + return INSTANCE.getCanPlaceOn(itemMeta); } static void copyAttributeModifiers(ItemMeta destination, ItemMeta source) { - // Since SportPaper handles attributes they don't show up in unhandledTags - if (BukkitUtils.isSportPaper()) { - for (String attribute : source.getModifiedAttributes()) { - for (org.bukkit.attribute.AttributeModifier modifier : - source.getAttributeModifiers(attribute)) { - destination.addAttributeModifier(attribute, modifier); - } - } - } else { - SetMultimap attributeModifiers = - NMSHacks.getAttributeModifiers(source); - attributeModifiers.putAll(NMSHacks.getAttributeModifiers(destination)); - NMSHacks.applyAttributeModifiers(attributeModifiers, destination); - } + INSTANCE.copyAttributeModifiers(destination, source); } static void applyAttributeModifiers( SetMultimap attributeModifiers, ItemMeta meta) { - if (BukkitUtils.isSportPaper()) { - for (Map.Entry entry : attributeModifiers.entries()) { - AttributeModifier attributeModifier = entry.getValue(); - meta.addAttributeModifier( - entry.getKey(), - new org.bukkit.attribute.AttributeModifier( - attributeModifier.getUniqueId(), - attributeModifier.getName(), - attributeModifier.getAmount(), - org.bukkit.attribute.AttributeModifier.Operation.fromOpcode( - attributeModifier.getOperation().ordinal()))); - } - } else { - NBTTagList list = new NBTTagList(); - for (Map.Entry entry : attributeModifiers.entries()) { - AttributeModifier modifier = entry.getValue(); - NBTTagCompound tag = new NBTTagCompound(); - tag.setString("Name", modifier.getName()); - tag.setDouble("Amount", modifier.getAmount()); - tag.setInt("Operation", modifier.getOperation().ordinal()); - tag.setLong("UUIDMost", modifier.getUniqueId().getMostSignificantBits()); - tag.setLong("UUIDLeast", modifier.getUniqueId().getLeastSignificantBits()); - tag.setString("AttributeName", entry.getKey()); - list.add(tag); - } - - Map unhandledTags = getUnhandledTags(meta); - unhandledTags.put("AttributeModifiers", list); - } - } - - static SetMultimap getAttributeModifiers(ItemMeta meta) { - Map unhandledTags = getUnhandledTags(meta); - if (unhandledTags.containsKey("AttributeModifiers")) { - final SetMultimap attributeModifiers = HashMultimap.create(); - final NBTTagList modTags = (NBTTagList) unhandledTags.get("AttributeModifiers"); - for (int i = 0; i < modTags.size(); i++) { - final NBTTagCompound modTag = modTags.get(i); - attributeModifiers.put( - modTag.getString("AttributeName"), - new AttributeModifier( - new UUID(modTag.getLong("UUIDMost"), modTag.getLong("UUIDLeast")), - modTag.getString("Name"), - modTag.getDouble("Amount"), - AttributeModifier.Operation.fromOpcode(modTag.getInt("Operation")))); - } - return attributeModifiers; - } else { - return HashMultimap.create(); - } - } - - static Inventory createFakeInventory(Player viewer, Inventory realInventory) { - if (BukkitUtils.isSportPaper() && realInventory.hasCustomName()) { - return realInventory instanceof DoubleChestInventory - ? Bukkit.createInventory(viewer, realInventory.getSize(), realInventory.getName()) - : Bukkit.createInventory(viewer, realInventory.getType(), realInventory.getName()); - } else { - return realInventory instanceof DoubleChestInventory - ? Bukkit.createInventory(viewer, realInventory.getSize()) - : Bukkit.createInventory(viewer, realInventory.getType()); - } - } - - // Not relevant if not SportPaper - static void resumeServer() { - if (BukkitUtils.isSportPaper() && Bukkit.getServer().isSuspended()) - Bukkit.getServer().setSuspended(false); - } - - static void setAffectsSpawning(Player player, boolean affectsSpawning) { - if (BukkitUtils.isSportPaper()) { - player.spigot().setAffectsSpawning(affectsSpawning); - } - } - - static void showInvisibles(Player player, boolean showInvisibles) { - if (BukkitUtils.isSportPaper()) { - player.showInvisibles(showInvisibles); - } + INSTANCE.applyAttributeModifiers(attributeModifiers, meta); } - static void setKnockbackReduction(Player player, float amount) { - // Not possible outside of SportPaper - if (BukkitUtils.isSportPaper()) { - player.setKnockbackReduction(amount); - } + static double getTPS() { + return INSTANCE.getTPS(); } - static void updateChunkSnapshot(ChunkSnapshot snapshot, org.bukkit.block.BlockState blockState) { - // ChunkSnapshot is immutable outside of SportPaper - if (BukkitUtils.isSportPaper()) { - snapshot.updateBlock(blockState); - } - } - - static void sendBlockChange(Location loc, Player player, @Nullable Material material) { - if (material != null) player.sendBlockChange(loc, material, (byte) 0); - else player.sendBlockChange(loc, loc.getBlock().getType(), loc.getBlock().getData()); - } - - interface FakeEntity { - int entityId(); - - Entity entity(); - - void spawn(Player viewer, Location location, org.bukkit.util.Vector velocity); - - default void spawn(Player viewer, Location location) { - spawn(viewer, location, new org.bukkit.util.Vector(0, 0, 0)); - } - - default void destroy(Player viewer) { - sendPacket(viewer, destroyEntitiesPacket(entityId())); - } - - default void teleport(Player viewer, Location location) { - sendPacket(viewer, teleportEntityPacket(entityId(), location)); - } - - default void ride(Player viewer, Entity rider) { - entityAttach(viewer, rider.getEntityId(), entityId(), false); - } - - default void mount(Player viewer, Entity vehicle) { - entityAttach(viewer, entityId(), vehicle.getEntityId(), false); - } - - default void wear(Player viewer, int slot, ItemStack item) { - sendPacket(viewer, entityEquipmentPacket(entityId(), slot, item)); - } - } - - abstract class FakeEntityImpl - implements FakeEntity { - protected final T entity; - - protected FakeEntityImpl(T entity) { - this.entity = entity; - } - - @Override - public Entity entity() { - return entity.getBukkitEntity(); - } - - @Override - public void spawn(Player viewer, Location location, Vector velocity) { - entity.setPositionRotation( - location.getX(), - location.getY(), - location.getZ(), - location.getYaw(), - location.getPitch()); - entity.motX = velocity.getX(); - entity.motY = velocity.getY(); - entity.motZ = velocity.getZ(); - sendPacket(viewer, spawnPacket()); - } - - abstract Packet spawnPacket(); - - @Override - public int entityId() { - return entity.getId(); - } - } - - class FakeLivingEntity extends FakeEntityImpl { - - protected FakeLivingEntity(T entity) { - super(entity); - } - - protected Packet spawnPacket() { - return new PacketPlayOutSpawnEntityLiving(entity); - } - } - - class FakeArmorStand extends FakeLivingEntity { - - private final ItemStack head; - - public FakeArmorStand(World world, ItemStack head) { - super(new EntityArmorStand(((CraftWorld) world).getHandle())); - this.head = head; - - entity.setInvisible(true); - NBTTagCompound tag = entity.getNBTTag(); - if (tag == null) { - tag = new NBTTagCompound(); - } - entity.c(tag); - tag.setBoolean("Silent", true); - tag.setBoolean("Invulnerable", true); - tag.setBoolean("NoGravity", true); - tag.setBoolean("NoAI", true); - entity.f(tag); - } - - @Override - public void spawn(Player viewer, Location location, Vector velocity) { - super.spawn(viewer, location, velocity); - if (head != null) wear(viewer, 4, head); - } - } - - class FakeWitherSkull extends FakeEntityImpl { - public FakeWitherSkull(World world) { - super(new EntityWitherSkull(((CraftWorld) world).getHandle())); - } - - protected Packet spawnPacket() { - return new PacketPlayOutSpawnEntity(entity, 66); + static void sendDestroyTeamDummyPacket() { + Object packet = teamRemovePacket("dummy"); + for (Player pl : Bukkit.getOnlinePlayers()) { + sendPacket(pl, packet); } - - @Override - public void wear(Player viewer, int slot, ItemStack item) {} } - class EntityPotion extends net.minecraft.server.v1_8_R3.EntityPotion { - public EntityPotion(Location location, ItemStack potionItem) { - super( - ((CraftWorld) location.getWorld()).getHandle(), - location.getX(), - location.getY(), - location.getZ(), - CraftItemStack.asNMSCopy(potionItem)); - } - - public void spawn() { - world.addEntity(this); - } + static Object teamPacket( + int operation, + String name, + String displayName, + String prefix, + String suffix, + boolean friendlyFire, + boolean seeFriendlyInvisibles, + NameTagVisibility nameTagVisibility, + Collection players) { + return INSTANCE.teamPacket( + operation, + name, + displayName, + prefix, + suffix, + friendlyFire, + seeFriendlyInvisibles, + nameTagVisibility, + players); } - static void setFireworksExpectedLifespan(Firework firework, int ticks) { - ((CraftFirework) firework).getHandle().expectedLifespan = ticks; + static Object teamCreatePacket( + String name, + String displayName, + String prefix, + String suffix, + boolean friendlyFire, + boolean seeFriendlyInvisibles, + Collection players) { + return teamPacket( + 0, + name, + displayName, + prefix, + suffix, + friendlyFire, + seeFriendlyInvisibles, + NameTagVisibility.ALWAYS, + players); } - static void setFireworksTicksFlown(Firework firework, int ticks) { - EntityFireworks entityFirework = ((CraftFirework) firework).getHandle(); - entityFirework.ticksFlown = ticks; + static Object teamRemovePacket(String name) { + return teamPacket(1, name, null, null, null, false, false, null, Lists.newArrayList()); } - static void skipFireworksLaunch(Firework firework) { - setFireworksExpectedLifespan(firework, 2); - setFireworksTicksFlown(firework, 2); - sendEntityMetadataToViewers(firework, false); + static Object teamUpdatePacket( + String name, + String displayName, + String prefix, + String suffix, + boolean friendlyFire, + boolean seeFriendlyInvisibles) { + return teamPacket( + 2, + name, + displayName, + prefix, + suffix, + friendlyFire, + seeFriendlyInvisibles, + NameTagVisibility.ALWAYS, + Lists.newArrayList()); } - static boolean isCraftItemArrowEntity(org.bukkit.entity.Item item) { - return ((CraftItem) item).getHandle() instanceof EntityArrow; + static Object teamJoinPacket(String name, Collection players) { + return teamPacket(3, name, null, null, null, false, false, null, players); } - static void fakePlayerItemPickup(Player player, org.bukkit.entity.Item item) { - float pitch = (((float) (Math.random() - Math.random()) * 0.7F + 1.0F) * 2.0F); - item.getWorld().playSound(item.getLocation(), org.bukkit.Sound.ITEM_PICKUP, 0.2F, pitch); - - NMSHacks.sendPacketToViewers( - item, new PacketPlayOutCollect(item.getEntityId(), player.getEntityId())); - - item.remove(); + static Object teamLeavePacket(String name, Collection players) { + return teamPacket(4, name, null, null, null, false, false, null, players); } - static void freezeEntity(Entity entity) { - net.minecraft.server.v1_8_R3.Entity nmsEntity = ((CraftEntity) entity).getHandle(); - NBTTagCompound tag = new NBTTagCompound(); - nmsEntity.c(tag); // save to tag - tag.setBoolean("NoAI", true); - tag.setBoolean("NoGravity", true); - nmsEntity.f(tag); // load from tag + static AttributeMap buildAttributeMap(Player player) { + return INSTANCE.buildAttributeMap(player); } - static void setFireballDirection(Fireball entity, Vector direction) { - EntityFireball fireball = ((CraftFireball) entity).getHandle(); - fireball.dirX = direction.getX() * 0.1D; - fireball.dirY = direction.getY() * 0.1D; - fireball.dirZ = direction.getZ() * 0.1D; + static void postToMainThread(Plugin plugin, boolean priority, Runnable task) { + INSTANCE.postToMainThread(plugin, priority, task); } } diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks1_8.java new file mode 100644 index 0000000000..b9e3886b75 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks1_8.java @@ -0,0 +1,1118 @@ +package tc.oc.pgm.util.nms; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.SetMultimap; +import com.google.common.util.concurrent.ListenableFutureTask; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Executors; +import net.minecraft.server.v1_8_R3.BlockPosition; +import net.minecraft.server.v1_8_R3.BlockStateList; +import net.minecraft.server.v1_8_R3.ChunkSection; +import net.minecraft.server.v1_8_R3.DataWatcher; +import net.minecraft.server.v1_8_R3.EntityArrow; +import net.minecraft.server.v1_8_R3.EntityFireball; +import net.minecraft.server.v1_8_R3.EntityFireworks; +import net.minecraft.server.v1_8_R3.EntityLiving; +import net.minecraft.server.v1_8_R3.EntityPlayer; +import net.minecraft.server.v1_8_R3.EntityTrackerEntry; +import net.minecraft.server.v1_8_R3.IBlockData; +import net.minecraft.server.v1_8_R3.IChatBaseComponent; +import net.minecraft.server.v1_8_R3.IDataManager; +import net.minecraft.server.v1_8_R3.MathHelper; +import net.minecraft.server.v1_8_R3.MinecraftServer; +import net.minecraft.server.v1_8_R3.MobEffectList; +import net.minecraft.server.v1_8_R3.MovingObjectPosition; +import net.minecraft.server.v1_8_R3.NBTBase; +import net.minecraft.server.v1_8_R3.NBTTagCompound; +import net.minecraft.server.v1_8_R3.NBTTagList; +import net.minecraft.server.v1_8_R3.NBTTagString; +import net.minecraft.server.v1_8_R3.Packet; +import net.minecraft.server.v1_8_R3.PacketPlayOutAttachEntity; +import net.minecraft.server.v1_8_R3.PacketPlayOutBed; +import net.minecraft.server.v1_8_R3.PacketPlayOutCollect; +import net.minecraft.server.v1_8_R3.PacketPlayOutEntityDestroy; +import net.minecraft.server.v1_8_R3.PacketPlayOutEntityEquipment; +import net.minecraft.server.v1_8_R3.PacketPlayOutEntityMetadata; +import net.minecraft.server.v1_8_R3.PacketPlayOutEntityTeleport; +import net.minecraft.server.v1_8_R3.PacketPlayOutEntityVelocity; +import net.minecraft.server.v1_8_R3.PacketPlayOutNamedEntitySpawn; +import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; +import net.minecraft.server.v1_8_R3.PacketPlayOutScoreboardTeam; +import net.minecraft.server.v1_8_R3.PacketPlayOutSpawnEntity; +import net.minecraft.server.v1_8_R3.PacketPlayOutSpawnEntityLiving; +import net.minecraft.server.v1_8_R3.PacketPlayOutWorldBorder; +import net.minecraft.server.v1_8_R3.ServerNBTManager; +import net.minecraft.server.v1_8_R3.Vec3D; +import net.minecraft.server.v1_8_R3.WorldBorder; +import net.minecraft.server.v1_8_R3.WorldData; +import net.minecraft.server.v1_8_R3.WorldServer; +import net.minecraft.server.v1_8_R3.WorldSettings; +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.block.Block; +import org.bukkit.craftbukkit.v1_8_R3.CraftChunk; +import org.bukkit.craftbukkit.v1_8_R3.CraftServer; +import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; +import org.bukkit.craftbukkit.v1_8_R3.block.CraftBlock; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftFireball; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftFirework; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftItem; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftLivingEntity; +import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; +import org.bukkit.craftbukkit.v1_8_R3.inventory.CraftItemStack; +import org.bukkit.craftbukkit.v1_8_R3.util.CraftMagicNumbers; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Fireball; +import org.bukkit.entity.Firework; +import org.bukkit.entity.Item; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.DoubleChestInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.material.MaterialData; +import org.bukkit.plugin.Plugin; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scoreboard.NameTagVisibility; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.util.attribute.AttributeMap; +import tc.oc.pgm.util.attribute.AttributeModifier; +import tc.oc.pgm.util.block.RayBlockIntersection; +import tc.oc.pgm.util.bukkit.BukkitUtils; +import tc.oc.pgm.util.bukkit.ViaUtils; +import tc.oc.pgm.util.nms.attribute.AttributeMap1_8; +import tc.oc.pgm.util.nms.entity.fake.FakeEntity; +import tc.oc.pgm.util.nms.entity.fake.armorstand.FakeArmorStand1_8; +import tc.oc.pgm.util.nms.entity.fake.wither.FakeWitherSkull1_8; +import tc.oc.pgm.util.nms.entity.potion.EntityPotion; +import tc.oc.pgm.util.nms.entity.potion.EntityPotion1_8; +import tc.oc.pgm.util.reflect.ReflectionUtils; +import tc.oc.pgm.util.skin.Skin; +import tc.oc.pgm.util.skin.Skins; + +public class NMSHacks1_8 extends NMSHacksNoOp { + @Override + public void sendPacket(Player bukkitPlayer, Object packet) { + if (bukkitPlayer.isOnline()) { + EntityPlayer nmsPlayer = ((CraftPlayer) bukkitPlayer).getHandle(); + nmsPlayer.playerConnection.sendPacket((Packet) packet); + } + } + + @Override + public void sendPacketToViewers(Entity entity, Object packet, boolean excludeSpectators) { + net.minecraft.server.v1_8_R3.Entity nms = ((CraftEntity) entity).getHandle(); + EntityTrackerEntry entry = + ((WorldServer) nms.getWorld()).getTracker().trackedEntities.get(nms.getId()); + for (EntityPlayer viewer : entry.trackedPlayers) { + if (excludeSpectators) { + Entity spectatorTarget = viewer.getBukkitEntity().getSpectatorTarget(); + if (spectatorTarget != null && spectatorTarget.getUniqueId().equals(entity.getUniqueId())) + continue; + } + viewer.playerConnection.sendPacket((Packet) packet); + } + } + + static Field entityMetadataWatchableField = + ReflectionUtils.getField(PacketPlayOutEntityMetadata.class, "b"); + + @Override + public void playDeathAnimation(Player player) { + EntityPlayer handle = ((CraftPlayer) player).getHandle(); + PacketPlayOutEntityMetadata metadata = + new PacketPlayOutEntityMetadata(handle.getId(), handle.getDataWatcher(), false); + + // Add/replace health to zero + boolean replaced = false; + DataWatcher.WatchableObject zeroHealth = + new DataWatcher.WatchableObject(3, 6, 0f); // type 3 (float), index 6 (health) + + List b = + (List) + ReflectionUtils.readField(metadata, entityMetadataWatchableField); + if (b != null) { + for (int i = 0; i < b.size(); i++) { + DataWatcher.WatchableObject wo = b.get(i); + if (wo.a() == 6) { + b.set(i, zeroHealth); + replaced = true; + } + } + } + + if (!replaced) { + if (b != null) b.add(zeroHealth); + else + ReflectionUtils.setField( + metadata, Collections.singletonList(zeroHealth), entityMetadataWatchableField); + } + + Location location = player.getLocation(); + PacketPlayOutBed useBed = + new PacketPlayOutBed( + ((CraftPlayer) player).getHandle(), + new BlockPosition(location.getX(), location.getY(), location.getZ())); + + Object teleport = teleportEntityPacket(player.getEntityId(), location); + + sendPacketToViewers(player, metadata, true); + sendPacketToViewers(player, useBed, true); + sendPacketToViewers(player, teleport, true); + } + + @Override + public Object teleportEntityPacket(int entityId, Location location) { + return new PacketPlayOutEntityTeleport( + entityId, // Entity ID + (int) (location.getX() * 32), // World X * 32 + (int) (location.getY() * 32), // World Y * 32 + (int) (location.getZ() * 32), // World Z * 32 + (byte) (location.getYaw() * 256 / 360), // Yaw + (byte) (location.getPitch() * 256 / 360), // Pitch + true); // On Ground + Height Correction + } + + @Override + public Object entityMetadataPacket(int entityId, Entity entity, boolean complete) { + return new PacketPlayOutEntityMetadata( + entityId, + ((CraftEntity) entity).getHandle().getDataWatcher(), + complete); // true = all values, false = only dirty values + } + + @Override + public void skipFireworksLaunch(Firework firework) { + EntityFireworks entityFirework = ((CraftFirework) firework).getHandle(); + entityFirework.expectedLifespan = 2; + entityFirework.ticksFlown = 2; + sendPacketToViewers( + firework, entityMetadataPacket(firework.getEntityId(), firework, false), false); + } + + @Override + public void fakePlayerItemPickup(Player player, Item item) { + float pitch = (((float) (Math.random() - Math.random()) * 0.7F + 1.0F) * 2.0F); + item.getWorld().playSound(item.getLocation(), org.bukkit.Sound.ITEM_PICKUP, 0.2F, pitch); + + sendPacketToViewers( + item, new PacketPlayOutCollect(item.getEntityId(), player.getEntityId()), false); + + item.remove(); + } + + @Override + public boolean isCraftItemArrowEntity(Item item) { + return ((CraftItem) item).getHandle() instanceof EntityArrow; + } + + @Override + public void freezeEntity(Entity entity) { + net.minecraft.server.v1_8_R3.Entity nmsEntity = ((CraftEntity) entity).getHandle(); + NBTTagCompound tag = new NBTTagCompound(); + nmsEntity.c(tag); // save to tag + tag.setBoolean("NoAI", true); + tag.setBoolean("NoGravity", true); + nmsEntity.f(tag); // load from tag + } + + @Override + public void setFireballDirection(Fireball entity, Vector direction) { + EntityFireball fireball = ((CraftFireball) entity).getHandle(); + fireball.dirX = direction.getX() * 0.1D; + fireball.dirY = direction.getY() * 0.1D; + fireball.dirZ = direction.getZ() * 0.1D; + } + + @Override + public void removeAndAddAllTabPlayers(Player viewer) { + List players = new ArrayList<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + if (viewer.canSee(player) || player == viewer) + players.add(((CraftPlayer) player).getHandle()); + } + + sendPacket( + viewer, + new PacketPlayOutPlayerInfo( + PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER, players)); + sendPacket( + viewer, + new PacketPlayOutPlayerInfo( + PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER, players)); + } + + @Override + public void sendLegacyWearing(Player player, int slot, ItemStack item) { + Packet packet = + new PacketPlayOutEntityEquipment( + player.getEntityId(), slot, CraftItemStack.asNMSCopy(item)); + net.minecraft.server.v1_8_R3.Entity nms = ((CraftEntity) player).getHandle(); + EntityTrackerEntry entry = + ((WorldServer) nms.getWorld()).getTracker().trackedEntities.get(nms.getId()); + for (EntityPlayer viewer : entry.trackedPlayers) { + if (ViaUtils.getProtocolVersion(viewer.getBukkitEntity()) <= ViaUtils.VERSION_1_7) + viewer.playerConnection.sendPacket(packet); + } + } + + @Override + public EntityPotion entityPotion(Location location, ItemStack potionItem) { + return new EntityPotion1_8(location, potionItem); + } + + @Override + public void sendBlockChange(Location loc, Player player, @Nullable Material material) { + if (material != null) player.sendBlockChange(loc, material, (byte) 0); + else player.sendBlockChange(loc, loc.getBlock().getType(), loc.getBlock().getData()); + } + + @Override + public void clearArrowsInPlayer(Player player) { + ((CraftPlayer) player).getHandle().o(0); + } + + @Override + public void showBorderWarning(Player player, boolean show) { + WorldBorder border = new WorldBorder(); + border.setWarningDistance(show ? Integer.MAX_VALUE : 0); + Object packet = + new PacketPlayOutWorldBorder( + border, PacketPlayOutWorldBorder.EnumWorldBorderAction.SET_WARNING_BLOCKS); + sendPacket(player, packet); + } + + @Override + public PotionEffectType getPotionEffectType(String key) { + MobEffectList nms = MobEffectList.b(key); + return nms == null ? null : PotionEffectType.getById(nms.id); + } + + @Override + public Enchantment getEnchantment(String key) { + net.minecraft.server.v1_8_R3.Enchantment enchantment = + net.minecraft.server.v1_8_R3.Enchantment.getByName(key); + return enchantment == null ? null : org.bukkit.enchantments.Enchantment.getById(enchantment.id); + } + + @Override + public long getMonotonicTime(World world) { + return ((CraftWorld) world).getHandle().getTime(); + } + + @Override + public int getPing(Player player) { + return ((CraftPlayer) player).getHandle().ping; + } + + @Override + public Object entityEquipmentPacket(int entityId, int slot, ItemStack armor) { + return new PacketPlayOutEntityEquipment(entityId, slot, CraftItemStack.asNMSCopy(armor)); + } + + enum EntityAttachFields { + a, + b, + c; + + Field field; + + EntityAttachFields() { + field = ReflectionUtils.getField(PacketPlayOutAttachEntity.class, name()); + } + + public Field getField() { + return field; + } + } + + @Override + public void entityAttach(Player player, int entityID, int vehicleID, boolean leash) { + PacketPlayOutAttachEntity packet = new PacketPlayOutAttachEntity(); + + ReflectionUtils.setField(packet, (byte) (leash ? 1 : 0), EntityAttachFields.a.getField()); + ReflectionUtils.setField(packet, entityID, EntityAttachFields.b.getField()); + ReflectionUtils.setField(packet, vehicleID, EntityAttachFields.c.getField()); + + sendPacket(player, packet); + } + + @Override + public Inventory createFakeInventory(Player viewer, Inventory realInventory) { + return realInventory instanceof DoubleChestInventory + ? Bukkit.createInventory(viewer, realInventory.getSize()) + : Bukkit.createInventory(viewer, realInventory.getType()); + } + + @Override + public FakeEntity fakeWitherSkull(World world) { + return new FakeWitherSkull1_8(world); + } + + @Override + public FakeEntity fakeArmorStand(World world, ItemStack head) { + return new FakeArmorStand1_8(world, head); + } + + @Override + public Set getBlocks(Chunk bukkitChunk, Material material) { + CraftChunk craftChunk = (CraftChunk) bukkitChunk; + Set blocks = new HashSet<>(); + + net.minecraft.server.v1_8_R3.Block nmsBlock = CraftMagicNumbers.getBlock(material); + net.minecraft.server.v1_8_R3.Chunk chunk = craftChunk.getHandle(); + + for (ChunkSection section : chunk.getSections()) { + if (section == null || section.a()) continue; // ChunkSection.a() -> true if section is empty + + char[] blockIds = section.getIdArray(); + for (int i = 0; i < blockIds.length; i++) { + // This does a lookup in the block registry, but does not create any objects, so should be + // pretty efficient + IBlockData blockData = (IBlockData) net.minecraft.server.v1_8_R3.Block.d.a(blockIds[i]); + if (blockData != null && blockData.getBlock() == nmsBlock) { + blocks.add( + bukkitChunk.getBlock(i & 0xf, section.getYPosition() | (i >> 8), (i >> 4) & 0xf)); + } + } + } + + return blocks; + } + + @Override + public Object spawnPlayerPacket(int entityId, UUID uuid, Location location, Player player) { + DataWatcher dataWatcher = copyDataWatcher(player); + + PacketPlayOutNamedEntitySpawn packet = new PacketPlayOutNamedEntitySpawn(); + + ReflectionUtils.setField(packet, entityId, NamedEntitySpawnFields.a.getField()); + ReflectionUtils.setField(packet, uuid, NamedEntitySpawnFields.b.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(location.getX() * 32.0D), NamedEntitySpawnFields.c.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(location.getY() * 32.0D), NamedEntitySpawnFields.d.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(location.getZ() * 32.0D), NamedEntitySpawnFields.e.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) location.getYaw()) * 256.0F / 360.0F)), + NamedEntitySpawnFields.f.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) location.getPitch()) * 256.0F / 360.0F)), + NamedEntitySpawnFields.g.getField()); + ReflectionUtils.setField( + packet, + null == null + ? 0 + : net.minecraft.server.v1_8_R3.Item.getId(CraftItemStack.asNMSCopy(null).getItem()), + NamedEntitySpawnFields.h.getField()); + ReflectionUtils.setField(packet, dataWatcher, NamedEntitySpawnFields.i.getField()); + ReflectionUtils.setField(packet, dataWatcher.b(), NamedEntitySpawnFields.j.getField()); + + return packet; + } + + @NotNull + protected static DataWatcher copyDataWatcher(Player player) { + DataWatcher original = ((CraftPlayer) player).getHandle().getDataWatcher(); + List values = original.c(); + DataWatcher copy = new DataWatcher(null); + for (DataWatcher.WatchableObject value : values) { + copy.a(value.a(), value.b()); + } + return copy; + } + + @Override + public Object destroyEntitiesPacket(int... entityIds) { + return new PacketPlayOutEntityDestroy(entityIds); + } + + Field playerInfoActionField = ReflectionUtils.getField(PacketPlayOutPlayerInfo.class, "a"); + + @Override + public Object createPlayerInfoPacket(EnumPlayerInfoAction action) { + PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo(); + ReflectionUtils.setField(packet, convertEnumPlayerInfoAction(action), playerInfoActionField); + return packet; + } + + static Method enablePotionParticlesMethod = ReflectionUtils.getMethod(EntityLiving.class, "B"); + static Method disablePotionParticlesMethod = ReflectionUtils.getMethod(EntityLiving.class, "bj"); + + @Override + public void setPotionParticles(Player player, boolean enabled) { + CraftPlayer craftPlayer = (CraftPlayer) player; + EntityPlayer handle = craftPlayer.getHandle(); + + if (enabled) { + ReflectionUtils.callMethod(enablePotionParticlesMethod, handle); + } else { + ReflectionUtils.callMethod(disablePotionParticlesMethod, handle); + } + } + + @Override + public ItemStack craftItemCopy(ItemStack item) { + return CraftItemStack.asCraftCopy(item); + } + + @Override + public RayBlockIntersection getTargetedBLock(Player player) { + Location start = player.getEyeLocation(); + World world = player.getWorld(); + Vector startVector = start.toVector(); + Vector end = + start + .toVector() + .add( + start.getDirection().multiply(player.getGameMode() == GameMode.CREATIVE ? 6 : 4.5)); + MovingObjectPosition hit = + ((CraftWorld) world) + .getHandle() + .rayTrace( + new Vec3D(startVector.getX(), startVector.getY(), startVector.getZ()), + new Vec3D(end.getX(), end.getY(), end.getZ()), + false, + false, + false); + if (hit != null && hit.type == MovingObjectPosition.EnumMovingObjectType.BLOCK) { + return new RayBlockIntersection( + world.getBlockAt(hit.a().getX(), hit.a().getY(), hit.a().getZ()), + CraftBlock.notchToBlockFace(hit.direction), + new Vector(hit.pos.a, hit.pos.b, hit.pos.c)); + } else { + return null; + } + } + + Field bField = ReflectionUtils.getField(PacketPlayOutPlayerInfo.class, "b"); + + @Override + public boolean playerInfoDataListNotEmpty(Object packet) { + List result; + PacketPlayOutPlayerInfo playOutPlayerInfo = (PacketPlayOutPlayerInfo) packet; + // SportPaper makes this field public + if (BukkitUtils.isSportPaper()) { + result = playOutPlayerInfo.b; + } else { + result = + (List) + ReflectionUtils.readField(playOutPlayerInfo, bField); + } + return !result.isEmpty(); + } + + Constructor playerInfoDataConstructor = + getPlayerInfoDataConstructor(); + + static Constructor getPlayerInfoDataConstructor() { + try { + Constructor constructor = + PacketPlayOutPlayerInfo.PlayerInfoData.class.getConstructor( + PacketPlayOutPlayerInfo.class, + GameProfile.class, + int.class, + WorldSettings.EnumGamemode.class, + IChatBaseComponent.class); + + constructor.setAccessible(true); + return constructor; + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object playerListPacketData( + Object packetPlayOutPlayerInfo, + UUID uuid, + String name, + GameMode gamemode, + int ping, + @Nullable Skin skin, + @Nullable String renderedDisplayName) { + GameProfile profile = new GameProfile(uuid, name); + if (skin != null) { + for (Map.Entry> entry : + Skins.toProperties(skin).asMap().entrySet()) { + profile.getProperties().putAll(entry.getKey(), entry.getValue()); + } + } + + try { + WorldSettings.EnumGamemode enumGamemode = + gamemode == null ? null : WorldSettings.EnumGamemode.getById(gamemode.getValue()); + IChatBaseComponent iChatBaseComponent = + renderedDisplayName == null + ? null + : IChatBaseComponent.ChatSerializer.a(renderedDisplayName); + + return playerInfoDataConstructor.newInstance( + packetPlayOutPlayerInfo, profile, ping, enumGamemode, iChatBaseComponent); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + @Override + public void addPlayerInfoToPacket(Object packet, Object playerInfoData) { + List result; + PacketPlayOutPlayerInfo playOutPlayerInfo = (PacketPlayOutPlayerInfo) packet; + + result = + (List) + ReflectionUtils.readField(playOutPlayerInfo, bField); + + result.add((PacketPlayOutPlayerInfo.PlayerInfoData) playerInfoData); + } + + Field skullProfileField = + ReflectionUtils.getField( + "org.bukkit.craftbukkit.v1_8_R3.inventory.CraftMetaSkull", "profile"); + + @Override + public void setSkullMetaOwner(SkullMeta meta, String name, UUID uuid, Skin skin) { + GameProfile gameProfile = new GameProfile(uuid, name); + Skins.setProperties(skin, gameProfile.getProperties()); + ReflectionUtils.setField(meta, gameProfile, skullProfileField); + } + + @Override + public WorldCreator detectWorld(String worldName) { + IDataManager sdm = + new ServerNBTManager(Bukkit.getServer().getWorldContainer(), worldName, true); + WorldData worldData = sdm.getWorldData(); + if (worldData == null) return null; + + return new WorldCreator(worldName) + .generateStructures(worldData.shouldGenerateMapFeatures()) + .generatorSettings(worldData.getGeneratorOptions()) + .seed(worldData.getSeed()) + .type(org.bukkit.WorldType.getByName(worldData.getType().name())); + } + + @Override + public void setAbsorption(LivingEntity entity, double health) { + ((CraftLivingEntity) entity).getHandle().setAbsorptionHearts((float) health); + } + + @Override + public double getAbsorption(LivingEntity entity) { + return ((CraftLivingEntity) entity).getHandle().getAbsorptionHearts(); + } + + @Override + public Set getBlockStates(Material material) { + ImmutableSet.Builder materials = ImmutableSet.builder(); + net.minecraft.server.v1_8_R3.Block nmsBlock = CraftMagicNumbers.getBlock(material); + List states = + ReflectionUtils.readField(BlockStateList.class, nmsBlock.P(), List.class, "e"); + if (states != null) { + for (IBlockData state : states) { + int data = nmsBlock.toLegacyData(state); + materials.add(material.getNewData((byte) data)); + } + } + return materials.build(); + } + + @Override + public Skin getPlayerSkin(Player player) { + CraftPlayer craftPlayer = (CraftPlayer) player; + return Skins.fromProperties(craftPlayer.getProfile().getProperties()); + } + + @Override + public Skin getPlayerSkinForViewer(Player player, Player viewer) { + return getPlayerSkin(player); + } + + @Override + public void updateVelocity(Player player) { + EntityPlayer handle = ((CraftPlayer) player).getHandle(); + handle.velocityChanged = false; + handle.playerConnection.sendPacket(new PacketPlayOutEntityVelocity(handle)); + } + + @Override + public boolean teleportRelative( + Player player, + Vector deltaPos, + float deltaYaw, + float deltaPitch, + PlayerTeleportEvent.TeleportCause cause) { + CraftPlayer craftPlayer = (CraftPlayer) player; + + if (craftPlayer.getHandle().playerConnection == null + || craftPlayer.getHandle().playerConnection.isDisconnected()) { + return false; + } + + // From = Players current Location + Location from = player.getLocation(); + // To = Players new Location if Teleport is Successful + Location to = from.clone().add(deltaPos); + to.setYaw(to.getYaw() + deltaYaw); + to.setPitch(to.getPitch() + deltaPitch); + + // Create & Call the Teleport Event. + PlayerTeleportEvent event = new PlayerTeleportEvent(player, from, to, cause); + Bukkit.getPluginManager().callEvent(event); + + // Return False to inform the Plugin that the Teleport was unsuccessful/cancelled. + if (event.isCancelled()) { + return false; + } + + craftPlayer.getHandle().playerConnection.teleport(to); + return true; + } + + enum LivingEntitySpawnFields { + a, + b, + c, + d, + e, + i, + j, + k, + l; + + Field field; + + LivingEntitySpawnFields() { + field = ReflectionUtils.getField(PacketPlayOutSpawnEntityLiving.class, name()); + } + + public Field getField() { + return field; + } + } + + enum EntitySpawnFields { + a, + b, + c, + d, + h, + i, + j; + + Field field; + + EntitySpawnFields() { + field = ReflectionUtils.getField(PacketPlayOutSpawnEntity.class, name()); + } + + public Field getField() { + return field; + } + } + + @Override + public void sendSpawnEntityPacket(Player player, int entityId, Location location) { + PacketPlayOutSpawnEntity packet = new PacketPlayOutSpawnEntity(); + + ReflectionUtils.setField(packet, entityId, EntitySpawnFields.a.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(location.getX() * 32.0D), EntitySpawnFields.b.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(location.getY() * 32.0D), EntitySpawnFields.c.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(location.getZ() * 32.0D), EntitySpawnFields.d.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) location.getYaw()) * 256.0F / 360.0F)), + EntitySpawnFields.h.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) location.getPitch()) * 256.0F / 360.0F)), + EntitySpawnFields.i.getField()); + ReflectionUtils.setField(packet, 66, EntitySpawnFields.j.getField()); + + sendPacket(player, packet); + } + + @Override + public void spawnFreezeEntity(Player player, int entityId, boolean legacy) { + if (legacy) { + Location location = player.getLocation().add(0, 0.286, 0); + if (location.getY() < -64) { + location.setY(-64); + player.teleport(location); + } + + sendSpawnEntityPacket(player, entityId, location); + } else { + Location loc = player.getLocation().subtract(0, 1.1, 0); + + DataWatcher dataWatcher = new DataWatcher(null); + int flags = 0; + flags |= 0x20; + dataWatcher.a(0, (byte) flags); + dataWatcher.a(1, (short) 0); + int flags1 = 0; + dataWatcher.a(10, (byte) flags1); + PacketPlayOutSpawnEntityLiving packet = new PacketPlayOutSpawnEntityLiving(); + + ReflectionUtils.setField(packet, entityId, LivingEntitySpawnFields.a.getField()); + ReflectionUtils.setField( + packet, (byte) EntityType.ARMOR_STAND.getTypeId(), LivingEntitySpawnFields.b.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(loc.getX() * 32.0D), LivingEntitySpawnFields.c.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(loc.getY() * 32.0D), LivingEntitySpawnFields.d.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(loc.getZ() * 32.0D), LivingEntitySpawnFields.e.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) loc.getYaw()) * 256.0F / 360.0F)), + LivingEntitySpawnFields.i.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) loc.getPitch()) * 256.0F / 360.0F)), + LivingEntitySpawnFields.j.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) loc.getPitch()) * 256.0F / 360.0F)), + LivingEntitySpawnFields.k.getField()); + ReflectionUtils.setField(packet, dataWatcher, LivingEntitySpawnFields.l.getField()); + + sendPacket(player, packet); + } + } + + @Override + public boolean canMineBlock(MaterialData blockMaterial, ItemStack tool) { + if (!blockMaterial.getItemType().isBlock()) { + throw new IllegalArgumentException("Material '" + blockMaterial + "' is not a block"); + } + + net.minecraft.server.v1_8_R3.Block nmsBlock = + CraftMagicNumbers.getBlock(blockMaterial.getItemType()); + net.minecraft.server.v1_8_R3.Item nmsTool = + tool == null ? null : CraftMagicNumbers.getItem(tool.getType()); + + return nmsBlock != null + && (nmsBlock.getMaterial().isAlwaysDestroyable() + || (nmsTool != null && nmsTool.canDestroySpecialBlock(nmsBlock))); + } + + Field worldServerField = ReflectionUtils.getField(CraftWorld.class, "world"); + Field dimensionField = ReflectionUtils.getField(WorldServer.class, "dimension"); + Field modifiersField = ReflectionUtils.getField(Field.class, "modifiers"); + + @Override + public void resetDimension(World world) { + try { + modifiersField.setInt(dimensionField, dimensionField.getModifiers() & ~Modifier.FINAL); + + dimensionField.set(worldServerField.get(world), 11); + } catch (IllegalAccessException e) { + // No-op, newer version of Java have disabled modifying final fields + } + } + + Field unhandledTagsField = + ReflectionUtils.getField( + "org.bukkit.craftbukkit.v1_8_R3.inventory.CraftMetaItem", "unhandledTags"); + static Field nbtListField = ReflectionUtils.getField(NBTTagList.class, "list"); + + @Override + public Set getMaterialCollection(ItemMeta itemMeta, String key) { + Map unhandledTags = + (Map) ReflectionUtils.readField(itemMeta, unhandledTagsField); + if (!unhandledTags.containsKey(key)) return EnumSet.noneOf(Material.class); + EnumSet materialSet = EnumSet.noneOf(Material.class); + NBTTagList canDestroyList = (NBTTagList) unhandledTags.get(key); + + for (NBTBase item : (List) ReflectionUtils.readField(canDestroyList, nbtListField)) { + NBTTagString nbtTagString = (NBTTagString) item; + String blockString = nbtTagString.a_(); + materialSet.add( + Material.getMaterial( + net.minecraft.server.v1_8_R3.Block.getId( + net.minecraft.server.v1_8_R3.Block.getByName(blockString)))); + } + + return materialSet; + } + + @Override + public void setMaterialCollection( + ItemMeta itemMeta, Collection materials, String canPlaceOn) { + Map unhandledTags = + (Map) ReflectionUtils.readField(itemMeta, unhandledTagsField); + NBTTagList canDestroyList = + unhandledTags.containsKey(canPlaceOn) + ? (NBTTagList) unhandledTags.get(canPlaceOn) + : new NBTTagList(); + for (Material material : materials) { + net.minecraft.server.v1_8_R3.Block block = + net.minecraft.server.v1_8_R3.Block.getById(material.getId()); + if (block != null) { + canDestroyList.add( + new NBTTagString(net.minecraft.server.v1_8_R3.Block.REGISTRY.c(block).toString())); + } + } + if (!canDestroyList.isEmpty()) unhandledTags.put(canPlaceOn, canDestroyList); + } + + @Override + public void setCanDestroy(ItemMeta itemMeta, Collection materials) { + setMaterialCollection(itemMeta, materials, "CanDestroy"); + } + + @Override + public Set getCanDestroy(ItemMeta itemMeta) { + return getMaterialCollection(itemMeta, "CanDestroy"); + } + + @Override + public void setCanPlaceOn(ItemMeta itemMeta, Collection materials) { + setMaterialCollection(itemMeta, materials, "CanPlaceOn"); + } + + @Override + public Set getCanPlaceOn(ItemMeta itemMeta) { + return getMaterialCollection(itemMeta, "CanPlaceOn"); + } + + @Override + public void copyAttributeModifiers(ItemMeta destination, ItemMeta source) { + SetMultimap attributeModifiers = + getAttributeModifiers(source); + attributeModifiers.putAll(getAttributeModifiers(destination)); + applyAttributeModifiers(attributeModifiers, destination); + } + + @Override + public void applyAttributeModifiers( + SetMultimap attributeModifiers, ItemMeta meta) { + NBTTagList list = new NBTTagList(); + for (Map.Entry entry : attributeModifiers.entries()) { + AttributeModifier modifier = entry.getValue(); + NBTTagCompound tag = new NBTTagCompound(); + tag.setString("Name", modifier.getName()); + tag.setDouble("Amount", modifier.getAmount()); + tag.setInt("Operation", modifier.getOperation().ordinal()); + tag.setLong("UUIDMost", modifier.getUniqueId().getMostSignificantBits()); + tag.setLong("UUIDLeast", modifier.getUniqueId().getLeastSignificantBits()); + tag.setString("AttributeName", entry.getKey()); + list.add(tag); + } + + Map unhandledTags = + (Map) ReflectionUtils.readField(meta, unhandledTagsField); + unhandledTags.put("AttributeModifiers", list); + } + + @Override + public SetMultimap getAttributeModifiers(ItemMeta meta) { + Map unhandledTags = + (Map) ReflectionUtils.readField(meta, unhandledTagsField); + if (unhandledTags.containsKey("AttributeModifiers")) { + final SetMultimap attributeModifiers = HashMultimap.create(); + final NBTTagList modTags = (NBTTagList) unhandledTags.get("AttributeModifiers"); + for (int i = 0; i < modTags.size(); i++) { + final NBTTagCompound modTag = modTags.get(i); + attributeModifiers.put( + modTag.getString("AttributeName"), + new AttributeModifier( + new UUID(modTag.getLong("UUIDMost"), modTag.getLong("UUIDLeast")), + modTag.getString("Name"), + modTag.getDouble("Amount"), + AttributeModifier.Operation.fromOpcode(modTag.getInt("Operation")))); + } + return attributeModifiers; + } else { + return HashMultimap.create(); + } + } + + @Override + public double getTPS() { + return 20.0; + } + + enum TeamPacketFields { + a, + b, + c, + d, + e, + g, + h, + i; + + Field field; + + TeamPacketFields() { + field = ReflectionUtils.getField(PacketPlayOutScoreboardTeam.class, name()); + } + + public Field getField() { + return field; + } + } + + @Override + public Object teamPacket( + int operation, + String name, + String displayName, + String prefix, + String suffix, + boolean friendlyFire, + boolean seeFriendlyInvisibles, + NameTagVisibility nameTagVisibility, + Collection players) { + + PacketPlayOutScoreboardTeam packet = new PacketPlayOutScoreboardTeam(); + + ReflectionUtils.setField(packet, name, TeamPacketFields.a.getField()); + ReflectionUtils.setField(packet, displayName, TeamPacketFields.b.getField()); + ReflectionUtils.setField(packet, prefix, TeamPacketFields.c.getField()); + ReflectionUtils.setField(packet, suffix, TeamPacketFields.d.getField()); + + String e = null; + if (nameTagVisibility != null) { + switch (nameTagVisibility) { + case ALWAYS: + e = "always"; + break; + case NEVER: + e = "never"; + break; + case HIDE_FOR_OTHER_TEAMS: + e = "hideForOtherTeams"; + break; + case HIDE_FOR_OWN_TEAM: + e = "hideForOwnTeam"; + break; + } + } + + ReflectionUtils.setField(packet, e, TeamPacketFields.e.getField()); + ReflectionUtils.setField(packet, players, TeamPacketFields.g.getField()); + ReflectionUtils.setField(packet, operation, TeamPacketFields.h.getField()); + + int i = (int) ReflectionUtils.readField(packet, TeamPacketFields.i.getField()); + if (friendlyFire) { + i |= 1; + } + if (seeFriendlyInvisibles) { + i |= 2; + } + + ReflectionUtils.setField(packet, i, TeamPacketFields.i.getField()); + + return packet; + } + + @Override + public AttributeMap buildAttributeMap(Player player) { + return new AttributeMap1_8(player); + } + + public ListenableFutureTask constructFutureTask(Runnable task) { + final ListenableFutureTask future = ListenableFutureTask.create(task, null); + return future; + } + + static Field TASK_QUEUE = ReflectionUtils.getField(MinecraftServer.class, "j"); + + @Override + public void postToMainThread(Plugin plugin, boolean priority, Runnable task) { + MinecraftServer server = ((CraftServer) plugin.getServer()).getHandle().getServer(); + server.a(Executors.callable(task)); + + // ListenableFutureTask futureTask = constructFutureTask(task); + // Queue> taskQueue = (Queue>) ReflectionUtils.readField(server, + // TASK_QUEUE); + // synchronized (taskQueue) { + // taskQueue.add(futureTask); + // } + } + + @NotNull + protected static PacketPlayOutPlayerInfo.EnumPlayerInfoAction convertEnumPlayerInfoAction( + EnumPlayerInfoAction action) { + PacketPlayOutPlayerInfo.EnumPlayerInfoAction nmsAction; + switch (action) { + case ADD_PLAYER: + nmsAction = PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER; + break; + case UPDATE_GAME_MODE: + nmsAction = PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_GAME_MODE; + break; + case UPDATE_LATENCY: + nmsAction = PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_LATENCY; + break; + case UPDATE_DISPLAY_NAME: + nmsAction = PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_DISPLAY_NAME; + break; + case REMOVE_PLAYER: + default: + nmsAction = PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER; + break; + } + return nmsAction; + } + + public enum NamedEntitySpawnFields { + a, + b, + c, + d, + e, + f, + g, + h, + i, + j; + + Field field; + + NamedEntitySpawnFields() { + field = ReflectionUtils.getField(PacketPlayOutNamedEntitySpawn.class, name()); + } + + public Field getField() { + return field; + } + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java new file mode 100644 index 0000000000..0a354e57b8 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java @@ -0,0 +1,200 @@ +package tc.oc.pgm.util.nms; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.BlockState; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.inventory.DoubleChestInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.material.MaterialData; +import org.bukkit.plugin.Plugin; +import org.bukkit.potion.PotionEffectType; +import tc.oc.pgm.util.attribute.AttributeMap; +import tc.oc.pgm.util.nms.attribute.AttributeMapNoOp; +import tc.oc.pgm.util.nms.entity.fake.FakeEntity; +import tc.oc.pgm.util.nms.entity.fake.FakeEntityNoOp; +import tc.oc.pgm.util.nms.entity.potion.EntityPotion; +import tc.oc.pgm.util.nms.entity.potion.EntityPotionBukkit; +import tc.oc.pgm.util.reflect.MinecraftReflectionUtils; +import tc.oc.pgm.util.reflect.ReflectionUtils; + +public abstract class NMSHacksNoOp implements NMSHacksPlatform { + + @Override + public boolean isCraftItemArrowEntity(Item item) { + return item.getType() == EntityType.ARROW; + } + + @Override + public EntityPotion entityPotion(Location location, ItemStack potionItem) { + return new EntityPotionBukkit(location, potionItem); + } + + @Override + public PotionEffectType getPotionEffectType(String key) { + String strippedKey = key.toLowerCase().replace(" ", "").replace("_", ""); + + // Some effects can be parsed by getByName, this only has cases for those that cannot + switch (strippedKey) { + case "slowness": + return PotionEffectType.SLOW; + case "haste": + return PotionEffectType.FAST_DIGGING; + case "miningfatigue": + return PotionEffectType.SLOW_DIGGING; + case "strength": + return PotionEffectType.INCREASE_DAMAGE; + case "instanthealth": + return PotionEffectType.HEAL; + case "instantdamage": + return PotionEffectType.HARM; + case "jumpboost": + return PotionEffectType.JUMP; + case "nausea": + return PotionEffectType.CONFUSION; + case "resistance": + return PotionEffectType.DAMAGE_RESISTANCE; + default: + return PotionEffectType.getByName(key); + } + } + + @Override + public Enchantment getEnchantment(String key) { + String strippedKey = key.toLowerCase().replace(" ", "").replace("_", ""); + + switch (strippedKey) { + case "protection": + return Enchantment.PROTECTION_ENVIRONMENTAL; + case "fireprotection": + return Enchantment.PROTECTION_FIRE; + case "featherfalling": + return Enchantment.PROTECTION_FALL; + case "blastprotection": + return Enchantment.PROTECTION_EXPLOSIONS; + case "projectileprotection": + return Enchantment.PROTECTION_PROJECTILE; + case "respiration": + return Enchantment.OXYGEN; + case "aquaaffinity": + return Enchantment.WATER_WORKER; + case "thorns": + return Enchantment.THORNS; + case "depthstrider": + return Enchantment.DEPTH_STRIDER; + case "sharpness": + return Enchantment.DAMAGE_ALL; + case "smite": + return Enchantment.DAMAGE_UNDEAD; + case "baneofarthropods": + return Enchantment.DAMAGE_ARTHROPODS; + case "knockback": + return Enchantment.KNOCKBACK; + case "fireaspect": + return Enchantment.FIRE_ASPECT; + case "looting": + return Enchantment.LOOT_BONUS_MOBS; + case "efficiency": + return Enchantment.DIG_SPEED; + case "silktouch": + return Enchantment.SILK_TOUCH; + case "unbreaking": + return Enchantment.DURABILITY; + case "fortune": + return Enchantment.LOOT_BONUS_BLOCKS; + case "power": + return Enchantment.ARROW_DAMAGE; + case "punch": + return Enchantment.ARROW_KNOCKBACK; + case "flame": + return Enchantment.ARROW_FIRE; + case "infinity": + return Enchantment.ARROW_INFINITE; + case "luckofthesea": + return Enchantment.LUCK; + case "lure": + return Enchantment.LURE; + default: + return Enchantment.getByName(key); + } + } + + static Class craftWorldClass = MinecraftReflectionUtils.getCraftBukkitClass("CraftWorld"); + static Method getHandleMethod = ReflectionUtils.getMethod(craftWorldClass, "getHandle"); + static Class nmsWorldClass = MinecraftReflectionUtils.getNMSClass("World"); + static Method getTimeMethod = ReflectionUtils.getMethod(nmsWorldClass, "getTime"); + + @Override + public long getMonotonicTime(World world) { + Object handle = ReflectionUtils.callMethod(getHandleMethod, world); + return (long) ReflectionUtils.callMethod(getTimeMethod, handle); + } + + @Override + public int getPing(Player player) { + return 100; + } + + @Override + public Inventory createFakeInventory(Player viewer, Inventory realInventory) { + return realInventory instanceof DoubleChestInventory + ? Bukkit.createInventory(viewer, realInventory.getSize()) + : Bukkit.createInventory(viewer, realInventory.getType()); + } + + @Override + public FakeEntity fakeWitherSkull(World world) { + return new FakeEntityNoOp(); + } + + @Override + public FakeEntity fakeArmorStand(World world, ItemStack head) { + return new FakeEntityNoOp(); + } + + @Override + public ItemStack craftItemCopy(ItemStack item) { + return item.clone(); + } + + @Override + public Set getBlockStates(Material material) { + // TODO: MaterialData is not version compatible + Set materialDataSet = new HashSet<>(); + for (byte i = 0; i < 16; i++) { + materialDataSet.add(new MaterialData(material, i)); + } + return materialDataSet; + } + + @Override + public void setBlockStateData(BlockState state, MaterialData materialData) { + state.setType(materialData.getItemType()); + state.setData(materialData); + } + + @Override + public double getTPS() { + return 20.0; + } + + @Override + public AttributeMap buildAttributeMap(Player player) { + return new AttributeMapNoOp(); + } + + @Override + public void postToMainThread(Plugin plugin, boolean priority, Runnable task) { + // runs the task on the next tick, not a perfect replacement + plugin.getServer().getScheduler().runTask(plugin, task); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java new file mode 100644 index 0000000000..d110e32ae0 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java @@ -0,0 +1,205 @@ +package tc.oc.pgm.util.nms; + +import com.google.common.collect.SetMultimap; +import java.util.Collection; +import java.util.Set; +import java.util.UUID; +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.block.Block; +import org.bukkit.block.BlockState; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Fireball; +import org.bukkit.entity.Firework; +import org.bukkit.entity.Item; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; +import org.bukkit.material.MaterialData; +import org.bukkit.plugin.Plugin; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scoreboard.NameTagVisibility; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.util.attribute.AttributeMap; +import tc.oc.pgm.util.attribute.AttributeModifier; +import tc.oc.pgm.util.block.RayBlockIntersection; +import tc.oc.pgm.util.nms.entity.fake.FakeEntity; +import tc.oc.pgm.util.nms.entity.potion.EntityPotion; +import tc.oc.pgm.util.skin.Skin; + +public interface NMSHacksPlatform { + + void sendPacket(Player bukkitPlayer, Object packet); + + void sendPacketToViewers(Entity entity, Object packet, boolean excludeSpectators); + + void playDeathAnimation(Player player); + + Object teleportEntityPacket(int entityId, Location location); + + Object entityMetadataPacket(int entityId, Entity entity, boolean complete); + + void skipFireworksLaunch(Firework firework); + + void fakePlayerItemPickup(Player player, Item item); + + boolean isCraftItemArrowEntity(org.bukkit.entity.Item item); + + void freezeEntity(Entity entity); + + void setFireballDirection(Fireball entity, Vector direction); + + void removeAndAddAllTabPlayers(Player viewer); + + void sendLegacyWearing(Player player, int slot, ItemStack item); + + EntityPotion entityPotion(Location location, ItemStack potionItem); + + void sendBlockChange(Location loc, Player player, @Nullable Material material); + + default void updateChunkSnapshot( + ChunkSnapshot snapshot, org.bukkit.block.BlockState blockState) {} + + default void setKnockbackReduction(Player player, float amount) {} + + default void showInvisibles(Player player, boolean showInvisibles) {} + + default void setAffectsSpawning(Player player, boolean affectsSpawning) {} + + void clearArrowsInPlayer(Player player); + + void showBorderWarning(Player player, boolean show); + + PotionEffectType getPotionEffectType(String key); + + org.bukkit.enchantments.Enchantment getEnchantment(String key); + + long getMonotonicTime(World world); + + int getPing(Player player); + + Object entityEquipmentPacket(int entityId, int slot, ItemStack armor); + + void entityAttach(Player player, int entityID, int vehicleID, boolean leash); + + default void resumeServer() {} + + Inventory createFakeInventory(Player viewer, Inventory realInventory); + + FakeEntity fakeWitherSkull(World world); + + FakeEntity fakeArmorStand(World world, ItemStack head); + + Set getBlocks(Chunk bukkitChunk, Material material); + + Object spawnPlayerPacket(int entityId, UUID uuid, Location location, Player player); + + Object destroyEntitiesPacket(int... entityIds); + + Object createPlayerInfoPacket(EnumPlayerInfoAction action); + + void setPotionParticles(Player player, boolean enabled); + + ItemStack craftItemCopy(ItemStack item); + + RayBlockIntersection getTargetedBLock(Player player); + + boolean playerInfoDataListNotEmpty(Object packet); + + Object playerListPacketData( + Object packetPlayOutPlayerInfo, + UUID uuid, + String name, + GameMode gamemode, + int ping, + @Nullable Skin skin, + @Nullable String renderedDisplayName); + + void addPlayerInfoToPacket(Object packet, Object playerInfoData); + + void setSkullMetaOwner(SkullMeta meta, String name, UUID uuid, Skin skin); + + WorldCreator detectWorld(String worldName); + + void setAbsorption(LivingEntity entity, double health); + + double getAbsorption(LivingEntity entity); + + @Deprecated + Set getBlockStates(Material material); + + // TODO: Material api + void setBlockStateData(BlockState state, MaterialData materialData); + + Skin getPlayerSkin(Player player); + + Skin getPlayerSkinForViewer(Player player, Player viewer); + + void updateVelocity(Player player); + + boolean teleportRelative( + Player player, + org.bukkit.util.Vector deltaPos, + float deltaYaw, + float deltaPitch, + PlayerTeleportEvent.TeleportCause cause); + + void sendSpawnEntityPacket(Player player, int entityId, Location location); + + void spawnFreezeEntity(Player player, int entityId, boolean legacy); + + /** + * Test if the given tool is capable of "efficiently" mining the given block. + * + *

Derived from CraftBlock.itemCausesDrops() + */ + boolean canMineBlock(MaterialData blockMaterial, ItemStack tool); + + void resetDimension(World world); + + Set getMaterialCollection(ItemMeta itemMeta, String key); + + void setMaterialCollection(ItemMeta itemMeta, Collection materials, String canPlaceOn); + + void setCanDestroy(ItemMeta itemMeta, Collection materials); + + Set getCanDestroy(ItemMeta itemMeta); + + void setCanPlaceOn(ItemMeta itemMeta, Collection materials); + + Set getCanPlaceOn(ItemMeta itemMeta); + + void copyAttributeModifiers(ItemMeta destination, ItemMeta source); + + void applyAttributeModifiers( + SetMultimap attributeModifiers, ItemMeta meta); + + SetMultimap getAttributeModifiers(ItemMeta meta); + + double getTPS(); + + Object teamPacket( + int operation, + String name, + String displayName, + String prefix, + String suffix, + boolean friendlyFire, + boolean seeFriendlyInvisibles, + NameTagVisibility nameTagVisibility, + Collection players); + + AttributeMap buildAttributeMap(Player player); + + void postToMainThread(Plugin plugin, boolean priority, Runnable task); +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksSportPaper.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksSportPaper.java new file mode 100644 index 0000000000..0869a7bb58 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksSportPaper.java @@ -0,0 +1,268 @@ +package tc.oc.pgm.util.nms; + +import com.google.common.collect.SetMultimap; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import net.minecraft.server.v1_8_R3.DataWatcher; +import net.minecraft.server.v1_8_R3.PacketPlayOutAttachEntity; +import net.minecraft.server.v1_8_R3.PacketPlayOutNamedEntitySpawn; +import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; +import net.minecraft.server.v1_8_R3.PacketPlayOutScoreboardTeam; +import net.minecraft.server.v1_8_R3.PacketPlayOutSpawnEntity; +import net.minecraft.server.v1_8_R3.PacketPlayOutSpawnEntityLiving; +import org.bukkit.Bukkit; +import org.bukkit.ChunkSnapshot; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.BlockState; +import org.bukkit.craftbukkit.v1_8_R3.inventory.CraftItemStack; +import org.bukkit.craftbukkit.v1_8_R3.scoreboard.CraftTeam; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.inventory.DoubleChestInventory; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.material.MaterialData; +import org.bukkit.plugin.Plugin; +import org.bukkit.scoreboard.NameTagVisibility; +import tc.oc.pgm.util.attribute.AttributeModifier; +import tc.oc.pgm.util.skin.Skin; + +public class NMSHacksSportPaper extends NMSHacks1_8 { + @Override + public void updateChunkSnapshot(ChunkSnapshot snapshot, BlockState blockState) { + snapshot.updateBlock(blockState); + } + + @Override + public void setBlockStateData(BlockState state, MaterialData materialData) { + state.setMaterialData(materialData); + } + + @Override + public void setKnockbackReduction(Player player, float amount) { + player.setKnockbackReduction(amount); + } + + @Override + public void showInvisibles(Player player, boolean showInvisibles) { + player.showInvisibles(showInvisibles); + } + + @Override + public void setAffectsSpawning(Player player, boolean affectsSpawning) { + player.spigot().setAffectsSpawning(affectsSpawning); + } + + @Override + public void entityAttach(Player player, int entityID, int vehicleID, boolean leash) { + sendPacket(player, new PacketPlayOutAttachEntity(entityID, vehicleID, leash)); + } + + @Override + public void resumeServer() { + if (Bukkit.getServer().isSuspended()) Bukkit.getServer().setSuspended(false); + } + + @Override + public Inventory createFakeInventory(Player viewer, Inventory realInventory) { + if (realInventory.hasCustomName()) { + return realInventory instanceof DoubleChestInventory + ? Bukkit.createInventory(viewer, realInventory.getSize(), realInventory.getName()) + : Bukkit.createInventory(viewer, realInventory.getType(), realInventory.getName()); + } else { + return realInventory instanceof DoubleChestInventory + ? Bukkit.createInventory(viewer, realInventory.getSize()) + : Bukkit.createInventory(viewer, realInventory.getType()); + } + } + + @Override + public Object spawnPlayerPacket(int entityId, UUID uuid, Location location, Player player) { + DataWatcher dataWatcher = copyDataWatcher(player); + return new PacketPlayOutNamedEntitySpawn( + entityId, + uuid, + location.getX(), + location.getY(), + location.getZ(), + (byte) location.getYaw(), + (byte) location.getPitch(), + CraftItemStack.asNMSCopy(null), + dataWatcher); + } + + @Override + public Object createPlayerInfoPacket(EnumPlayerInfoAction action) { + PacketPlayOutPlayerInfo packet = new PacketPlayOutPlayerInfo(); + packet.a = convertEnumPlayerInfoAction(action); + return packet; + } + + @Override + public void setPotionParticles(Player player, boolean enabled) { + player.setPotionParticles(enabled); + } + + @Override + public boolean playerInfoDataListNotEmpty(Object packet) { + return !((PacketPlayOutPlayerInfo) packet).b.isEmpty(); + } + + @Override + public void addPlayerInfoToPacket(Object packet, Object playerInfoData) { + ((PacketPlayOutPlayerInfo) packet) + .b.add((PacketPlayOutPlayerInfo.PlayerInfoData) playerInfoData); + } + + @Override + public void sendSpawnEntityPacket(Player player, int entityId, Location location) { + sendPacket( + player, + new PacketPlayOutSpawnEntity( + entityId, + location.getX(), + location.getY(), + location.getZ(), + 0, + 0, + 0, + (int) location.getPitch(), + (int) location.getYaw(), + 66, + 0)); + } + + @Override + public void spawnFreezeEntity(Player player, int entityId, boolean legacy) { + if (legacy) { + Location location = player.getLocation().add(0, 0.286, 0); + if (location.getY() < -64) { + location.setY(-64); + player.teleport(location); + } + + sendSpawnEntityPacket(player, entityId, location); + } else { + Location loc = player.getLocation().subtract(0, 1.1, 0); + int flags = 0; + flags |= 0x20; + DataWatcher dataWatcher = new DataWatcher(null); + dataWatcher.a(0, (byte) (byte) flags); + dataWatcher.a(1, (short) (short) 0); + int flags1 = 0; + dataWatcher.a(10, (byte) flags1); + sendPacket( + player, + new PacketPlayOutSpawnEntityLiving( + entityId, + (byte) EntityType.ARMOR_STAND.getTypeId(), + loc.getX(), + loc.getY(), + loc.getZ(), + loc.getYaw(), + loc.getPitch(), + loc.getPitch(), + 0, + 0, + 0, + dataWatcher)); + } + } + + @Override + public Skin getPlayerSkinForViewer(Player player, Player viewer) { + return player.hasFakeSkin(viewer) + ? new Skin(player.getFakeSkin(viewer).getData(), player.getFakeSkin(viewer).getSignature()) + : getPlayerSkin(player); + } + + @Override + public void setCanDestroy(ItemMeta itemMeta, Collection materials) { + itemMeta.setCanDestroy(materials); + } + + @Override + public Set getCanDestroy(ItemMeta itemMeta) { + return itemMeta.getCanDestroy(); + } + + @Override + public void setCanPlaceOn(ItemMeta itemMeta, Collection materials) { + itemMeta.setCanPlaceOn(materials); + } + + @Override + public Set getCanPlaceOn(ItemMeta itemMeta) { + return itemMeta.getCanPlaceOn(); + } + + @Override + public void copyAttributeModifiers(ItemMeta destination, ItemMeta source) { + for (String attribute : source.getModifiedAttributes()) { + for (org.bukkit.attribute.AttributeModifier modifier : + source.getAttributeModifiers(attribute)) { + destination.addAttributeModifier(attribute, modifier); + } + } + } + + @Override + public void applyAttributeModifiers( + SetMultimap attributeModifiers, ItemMeta meta) { + for (Map.Entry entry : attributeModifiers.entries()) { + AttributeModifier attributeModifier = entry.getValue(); + meta.addAttributeModifier( + entry.getKey(), + new org.bukkit.attribute.AttributeModifier( + attributeModifier.getUniqueId(), + attributeModifier.getName(), + attributeModifier.getAmount(), + org.bukkit.attribute.AttributeModifier.Operation.fromOpcode( + attributeModifier.getOperation().ordinal()))); + } + } + + @Override + public double getTPS() { + return Bukkit.getServer().spigot().getTPS()[0]; + } + + @Override + public Object teamPacket( + int operation, + String name, + String displayName, + String prefix, + String suffix, + boolean friendlyFire, + boolean seeFriendlyInvisibles, + NameTagVisibility nameTagVisibility, + Collection players) { + PacketPlayOutScoreboardTeam packet = new PacketPlayOutScoreboardTeam(); + + packet.a = name; + packet.b = displayName; + packet.c = prefix; + packet.d = suffix; + packet.e = nameTagVisibility == null ? null : CraftTeam.bukkitToNotch(nameTagVisibility).e; + // packet.f = color + packet.g = players; + packet.h = operation; + if (friendlyFire) { + packet.i |= 1; + } + if (seeFriendlyInvisibles) { + packet.i |= 2; + } + + return packet; + } + + @Override + public void postToMainThread(Plugin plugin, boolean priority, Runnable task) { + Bukkit.getServer().postToMainThread(plugin, true, task); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/attribute/AttributeInstanceImpl.java b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeInstance1_8.java similarity index 87% rename from util/src/main/java/tc/oc/pgm/util/attribute/AttributeInstanceImpl.java rename to util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeInstance1_8.java index d87f216b1b..8681ac62ad 100644 --- a/util/src/main/java/tc/oc/pgm/util/attribute/AttributeInstanceImpl.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeInstance1_8.java @@ -1,17 +1,20 @@ -package tc.oc.pgm.util.attribute; +package tc.oc.pgm.util.nms.attribute; import static tc.oc.pgm.util.Assert.assertTrue; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import tc.oc.pgm.util.attribute.Attribute; +import tc.oc.pgm.util.attribute.AttributeInstance; +import tc.oc.pgm.util.attribute.AttributeModifier; -public class AttributeInstanceImpl implements AttributeInstance { +public class AttributeInstance1_8 implements AttributeInstance { private final net.minecraft.server.v1_8_R3.AttributeInstance handle; private final Attribute attribute; - public AttributeInstanceImpl( + public AttributeInstance1_8( net.minecraft.server.v1_8_R3.AttributeInstance handle, Attribute attribute) { this.handle = handle; this.attribute = attribute; diff --git a/util/src/main/java/tc/oc/pgm/util/attribute/AttributeMapImpl.java b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMap1_8.java similarity index 72% rename from util/src/main/java/tc/oc/pgm/util/attribute/AttributeMapImpl.java rename to util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMap1_8.java index db05906632..1867a4f73a 100644 --- a/util/src/main/java/tc/oc/pgm/util/attribute/AttributeMapImpl.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMap1_8.java @@ -1,29 +1,28 @@ -package tc.oc.pgm.util.attribute; +package tc.oc.pgm.util.nms.attribute; import static tc.oc.pgm.util.Assert.assertNotNull; import net.minecraft.server.v1_8_R3.AttributeMapBase; import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer; import org.bukkit.entity.Player; +import tc.oc.pgm.util.attribute.Attribute; +import tc.oc.pgm.util.attribute.AttributeInstance; +import tc.oc.pgm.util.attribute.AttributeMap; -public class AttributeMapImpl implements AttributeMap { +public class AttributeMap1_8 implements AttributeMap { private final AttributeMapBase handle; - public AttributeMapImpl(Player player) { + public AttributeMap1_8(Player player) { handle = ((CraftPlayer) player).getHandle().getAttributeMap(); } - public AttributeMapImpl(AttributeMapBase handle) { - this.handle = handle; - } - @Override public AttributeInstance getAttribute(Attribute attribute) { assertNotNull(attribute, "attribute"); net.minecraft.server.v1_8_R3.AttributeInstance nms = handle.a(toMinecraft(attribute.name())); - return (nms == null) ? null : new AttributeInstanceImpl(nms, attribute); + return (nms == null) ? null : new AttributeInstance1_8(nms, attribute); } static String toMinecraft(String bukkit) { diff --git a/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMapNoOp.java b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMapNoOp.java new file mode 100644 index 0000000000..a2f62cf9f7 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMapNoOp.java @@ -0,0 +1,12 @@ +package tc.oc.pgm.util.nms.attribute; + +import tc.oc.pgm.util.attribute.Attribute; +import tc.oc.pgm.util.attribute.AttributeInstance; +import tc.oc.pgm.util.attribute.AttributeMap; + +public class AttributeMapNoOp implements AttributeMap { + @Override + public AttributeInstance getAttribute(Attribute attribute) { + return null; + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntity.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntity.java new file mode 100644 index 0000000000..1cebe9ea13 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntity.java @@ -0,0 +1,44 @@ +package tc.oc.pgm.util.nms.entity.fake; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import tc.oc.pgm.util.nms.NMSHacks; + +public interface FakeEntity { + int entityId(); + + Entity entity(); + + void spawn(Player viewer, Location location, org.bukkit.util.Vector velocity); + + default void spawn(Player viewer, Location location) { + spawn(viewer, location, new org.bukkit.util.Vector(0, 0, 0)); + } + + default void destroy(Player viewer) { + int[] entityIds = new int[] {entityId()}; + NMSHacks.sendPacket(viewer, NMSHacks.destroyEntitiesPacket(entityIds)); + } + + default void teleport(Player viewer, Location location) { + NMSHacks.sendPacket(viewer, NMSHacks.teleportEntityPacket(entityId(), location)); + } + + default void ride(Player viewer, Entity rider) { + int entityID = rider.getEntityId(); + int vehicleID = entityId(); + NMSHacks.entityAttach(viewer, entityID, vehicleID, false); + } + + default void mount(Player viewer, Entity vehicle) { + int entityID = entityId(); + int vehicleID = vehicle.getEntityId(); + NMSHacks.entityAttach(viewer, entityID, vehicleID, false); + } + + default void wear(Player viewer, int slot, ItemStack item) { + NMSHacks.sendPacket(viewer, NMSHacks.entityEquipmentPacket(entityId(), slot, item)); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityImpl1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityImpl1_8.java new file mode 100644 index 0000000000..080655afab --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityImpl1_8.java @@ -0,0 +1,39 @@ +package tc.oc.pgm.util.nms.entity.fake; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; +import tc.oc.pgm.util.nms.NMSHacks; + +public abstract class FakeEntityImpl1_8 + implements FakeEntity { + protected final T entity; + + protected FakeEntityImpl1_8(T entity) { + this.entity = entity; + } + + @Override + public Entity entity() { + return entity.getBukkitEntity(); + } + + @Override + public void spawn(Player viewer, Location location, Vector velocity) { + entity.setPositionRotation( + location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); + entity.motX = velocity.getX(); + entity.motY = velocity.getY(); + entity.motZ = velocity.getZ(); + Object packet = spawnPacket(); + NMSHacks.sendPacket(viewer, packet); + } + + public abstract Object spawnPacket(); + + @Override + public int entityId() { + return entity.getId(); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityNoOp.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityNoOp.java new file mode 100644 index 0000000000..dfce6f7d8f --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityNoOp.java @@ -0,0 +1,22 @@ +package tc.oc.pgm.util.nms.entity.fake; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +public class FakeEntityNoOp implements FakeEntity { + + @Override + public int entityId() { + return 0; + } + + @Override + public Entity entity() { + return null; + } + + @Override + public void spawn(Player viewer, Location location, Vector velocity) {} +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeLivingEntity1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeLivingEntity1_8.java new file mode 100644 index 0000000000..8c1ba13aa0 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeLivingEntity1_8.java @@ -0,0 +1,15 @@ +package tc.oc.pgm.util.nms.entity.fake; + +import net.minecraft.server.v1_8_R3.EntityLiving; +import net.minecraft.server.v1_8_R3.PacketPlayOutSpawnEntityLiving; + +public class FakeLivingEntity1_8 extends FakeEntityImpl1_8 { + + protected FakeLivingEntity1_8(T entity) { + super(entity); + } + + public Object spawnPacket() { + return new PacketPlayOutSpawnEntityLiving(entity); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/armorstand/FakeArmorStand1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/armorstand/FakeArmorStand1_8.java new file mode 100644 index 0000000000..1310960bc9 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/armorstand/FakeArmorStand1_8.java @@ -0,0 +1,39 @@ +package tc.oc.pgm.util.nms.entity.fake.armorstand; + +import net.minecraft.server.v1_8_R3.EntityArmorStand; +import net.minecraft.server.v1_8_R3.NBTTagCompound; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; +import tc.oc.pgm.util.nms.entity.fake.FakeLivingEntity1_8; + +public class FakeArmorStand1_8 extends FakeLivingEntity1_8 { + + private final ItemStack head; + + public FakeArmorStand1_8(World world, ItemStack head) { + super(new EntityArmorStand(((CraftWorld) world).getHandle())); + this.head = head; + + entity.setInvisible(true); + NBTTagCompound tag = entity.getNBTTag(); + if (tag == null) { + tag = new NBTTagCompound(); + } + entity.c(tag); + tag.setBoolean("Silent", true); + tag.setBoolean("Invulnerable", true); + tag.setBoolean("NoGravity", true); + tag.setBoolean("NoAI", true); + entity.f(tag); + } + + @Override + public void spawn(Player viewer, Location location, Vector velocity) { + super.spawn(viewer, location, velocity); + if (head != null) wear(viewer, 4, head); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/wither/FakeWitherSkull1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/wither/FakeWitherSkull1_8.java new file mode 100644 index 0000000000..6bef19ef02 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/wither/FakeWitherSkull1_8.java @@ -0,0 +1,22 @@ +package tc.oc.pgm.util.nms.entity.fake.wither; + +import net.minecraft.server.v1_8_R3.EntityWitherSkull; +import net.minecraft.server.v1_8_R3.PacketPlayOutSpawnEntity; +import org.bukkit.World; +import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import tc.oc.pgm.util.nms.entity.fake.FakeEntityImpl1_8; + +public class FakeWitherSkull1_8 extends FakeEntityImpl1_8 { + public FakeWitherSkull1_8(World world) { + super(new EntityWitherSkull(((CraftWorld) world).getHandle())); + } + + public Object spawnPacket() { + return new PacketPlayOutSpawnEntity(entity, 66); + } + + @Override + public void wear(Player viewer, int slot, ItemStack item) {} +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion.java new file mode 100644 index 0000000000..27c1329699 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion.java @@ -0,0 +1,10 @@ +package tc.oc.pgm.util.nms.entity.potion; + +import org.bukkit.entity.Entity; + +public interface EntityPotion { + + public void spawn(); + + Entity getBukkitEntity(); +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion1_8.java new file mode 100644 index 0000000000..a9604c118f --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion1_8.java @@ -0,0 +1,32 @@ +package tc.oc.pgm.util.nms.entity.potion; + +import org.bukkit.Location; +import org.bukkit.craftbukkit.v1_8_R3.CraftWorld; +import org.bukkit.craftbukkit.v1_8_R3.inventory.CraftItemStack; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.ItemStack; + +public class EntityPotion1_8 implements EntityPotion { + + private net.minecraft.server.v1_8_R3.EntityPotion handle; + + public EntityPotion1_8(Location location, ItemStack potionItem) { + handle = + new net.minecraft.server.v1_8_R3.EntityPotion( + ((CraftWorld) location.getWorld()).getHandle(), + location.getX(), + location.getY(), + location.getZ(), + CraftItemStack.asNMSCopy(potionItem)); + } + + @Override + public void spawn() { + handle.spawnIn(handle.getWorld()); + } + + @Override + public Entity getBukkitEntity() { + return handle.getBukkitEntity(); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotionBukkit.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotionBukkit.java new file mode 100644 index 0000000000..ee65d21a96 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotionBukkit.java @@ -0,0 +1,31 @@ +package tc.oc.pgm.util.nms.entity.potion; + +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.ThrownPotion; +import org.bukkit.inventory.ItemStack; + +public class EntityPotionBukkit implements EntityPotion { + + private final Location location; + private final ItemStack potionItem; + private ThrownPotion bukkitPotionEntity = null; + + public EntityPotionBukkit(Location location, ItemStack potionItem) { + this.location = location; + this.potionItem = potionItem; + } + + @Override + public void spawn() { + this.bukkitPotionEntity = + (ThrownPotion) this.location.getWorld().spawnEntity(location, EntityType.SPLASH_POTION); + bukkitPotionEntity.setItem(potionItem); + } + + @Override + public Entity getBukkitEntity() { + return this.bukkitPotionEntity; + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/reflect/MinecraftReflectionUtils.java b/util/src/main/java/tc/oc/pgm/util/reflect/MinecraftReflectionUtils.java new file mode 100644 index 0000000000..985c02d311 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/reflect/MinecraftReflectionUtils.java @@ -0,0 +1,19 @@ +package tc.oc.pgm.util.reflect; + +import org.bukkit.Bukkit; + +public interface MinecraftReflectionUtils { + String VERSION = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; + + static Class getBukkitClass(String classPath) { + return ReflectionUtils.getClassFromName("org.bukkit." + classPath); + } + + static Class getCraftBukkitClass(String classPath) { + return ReflectionUtils.getClassFromName("org.bukkit.craftbukkit." + VERSION + "." + classPath); + } + + static Class getNMSClass(String classPath) { + return ReflectionUtils.getClassFromName("net.minecraft.server." + VERSION + "." + classPath); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/reflect/ReflectionUtils.java b/util/src/main/java/tc/oc/pgm/util/reflect/ReflectionUtils.java index f60306e6b7..bbb7dcefb7 100644 --- a/util/src/main/java/tc/oc/pgm/util/reflect/ReflectionUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/reflect/ReflectionUtils.java @@ -1,11 +1,14 @@ package tc.oc.pgm.util.reflect; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.util.ClassLogger; public final class ReflectionUtils { + private ReflectionUtils() {} public static Class getClassFromName(String className) { @@ -17,29 +20,42 @@ public static Class getClassFromName(String className) { } } - public static Method getMethod(Class parent, String name) { + public static Method getMethod(Class parent, String name, Class... parameterTypes) { try { - Method method = parent.getDeclaredMethod(name); + Method method = parent.getDeclaredMethod(name, parameterTypes); method.setAccessible(true); return method; } catch (NoSuchMethodException e) { + ClassLogger classLogger = ClassLogger.get(ReflectionUtils.class); + for (Method method : parent.getMethods()) { + classLogger.warning(method.toString()); + } throw new RuntimeException(e); } } - public static void callMethod(Class parent, String name, Object object) { + public static Object callMethod(Method method, Object object, Object... parameters) { try { - Method method = getMethod(parent, name); - method.invoke(object); + return method.invoke(object, parameters); } catch (IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } } - public static void callMethod(Method method, Object object) { + public static Constructor getConstructor(Class parent, Class... parameters) { try { - method.invoke(object); - } catch (IllegalAccessException | InvocationTargetException e) { + Constructor constructor = parent.getConstructor(parameters); + constructor.setAccessible(true); + return constructor; + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + public static Object callConstructor(Constructor constructor, Object... parameters) { + try { + return constructor.newInstance(parameters); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException(e); } } diff --git a/util/src/main/java/tc/oc/pgm/util/tablist/PlayerTabEntry.java b/util/src/main/java/tc/oc/pgm/util/tablist/PlayerTabEntry.java index 5fb273dc23..89384c84ea 100644 --- a/util/src/main/java/tc/oc/pgm/util/tablist/PlayerTabEntry.java +++ b/util/src/main/java/tc/oc/pgm/util/tablist/PlayerTabEntry.java @@ -77,11 +77,7 @@ public Skin getSkin(TabView view) { } // TODO: find different solution for non-SportPaper servers - return this.player.hasFakeSkin(viewer) - ? new Skin( - this.player.getFakeSkin(viewer).getData(), - this.player.getFakeSkin(viewer).getSignature()) - : NMSHacks.getPlayerSkin(this.player); + return NMSHacks.getPlayerSkinForViewer(player, viewer); } @Override diff --git a/util/src/main/java/tc/oc/pgm/util/tablist/TabDisplay.java b/util/src/main/java/tc/oc/pgm/util/tablist/TabDisplay.java index 667d238e09..d2cfaef866 100644 --- a/util/src/main/java/tc/oc/pgm/util/tablist/TabDisplay.java +++ b/util/src/main/java/tc/oc/pgm/util/tablist/TabDisplay.java @@ -12,11 +12,10 @@ import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import net.minecraft.server.v1_8_R3.Packet; -import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; import org.bukkit.GameMode; import org.bukkit.entity.Player; import tc.oc.pgm.util.StringUtils; +import tc.oc.pgm.util.nms.EnumPlayerInfoAction; import tc.oc.pgm.util.nms.NMSHacks; import tc.oc.pgm.util.text.TextTranslations; @@ -50,11 +49,11 @@ public class TabDisplay { private final String[] rendered; // Cached packets used to setup and tear down the player list - private final Packet[] teamCreatePackets; - private final Packet[] teamRemovePackets; + private final Object[] teamCreatePackets; + private final Object[] teamRemovePackets; - private final PacketPlayOutPlayerInfo listAddPacket; - private final PacketPlayOutPlayerInfo listRemovePacket; + private final Object listAddPacket; + private final Object listRemovePacket; public TabDisplay(Player viewer, int width) { // Number of columns is maxPlayers/rows rounded up @@ -64,13 +63,11 @@ public TabDisplay(Player viewer, int width) { this.viewer = viewer; this.rendered = new String[this.slots]; - this.teamCreatePackets = new Packet[this.slots]; - this.teamRemovePackets = new Packet[this.slots]; + this.teamCreatePackets = new Object[this.slots]; + this.teamRemovePackets = new Object[this.slots]; - this.listAddPacket = - NMSHacks.createPlayerInfoPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER); - this.listRemovePacket = - NMSHacks.createPlayerInfoPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER); + this.listAddPacket = NMSHacks.createPlayerInfoPacket(EnumPlayerInfoAction.ADD_PLAYER); + this.listRemovePacket = NMSHacks.createPlayerInfoPacket(EnumPlayerInfoAction.REMOVE_PLAYER); for (int slot = 0; slot < this.slots; ++slot) { Component playerName = this.slotName(slot); @@ -84,19 +81,12 @@ public TabDisplay(Player viewer, int width) { this.teamRemovePackets[slot] = NMSHacks.teamRemovePacket(teamName); UUID uuid = UUID.randomUUID(); - NMSHacks.getPlayerInfoDataList(listAddPacket) - .add( - NMSHacks.playerListPacketData( - listAddPacket, uuid, name, GameMode.SURVIVAL, PING, null, renderedPlayerName)); - NMSHacks.getPlayerInfoDataList(listRemovePacket) - .add(NMSHacks.playerListPacketData(listRemovePacket, uuid, renderedPlayerName)); + NMSHacks.addPlayerInfoToPacket( + listAddPacket, uuid, name, GameMode.SURVIVAL, 9999, null, renderedPlayerName); + NMSHacks.addPlayerInfoToPacket(listRemovePacket, uuid, renderedPlayerName); } } - public int getWidth() { - return width; - } - private int slotIndex(int x, int y) { return y * this.width + x; } diff --git a/util/src/main/java/tc/oc/pgm/util/tablist/TabRender.java b/util/src/main/java/tc/oc/pgm/util/tablist/TabRender.java index e9ea934e97..c979333492 100644 --- a/util/src/main/java/tc/oc/pgm/util/tablist/TabRender.java +++ b/util/src/main/java/tc/oc/pgm/util/tablist/TabRender.java @@ -5,36 +5,29 @@ import java.util.Collections; import java.util.List; import net.kyori.adventure.text.Component; -import net.minecraft.server.v1_8_R3.Packet; -import net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo; import org.bukkit.Location; import org.bukkit.entity.Player; import tc.oc.pgm.util.Audience; +import tc.oc.pgm.util.nms.EnumPlayerInfoAction; import tc.oc.pgm.util.nms.NMSHacks; import tc.oc.pgm.util.text.TextTranslations; public class TabRender { private final TabView view; - private final PacketPlayOutPlayerInfo removePacket; - private final PacketPlayOutPlayerInfo addPacket; - private final PacketPlayOutPlayerInfo updatePacket; - private final PacketPlayOutPlayerInfo updatePingPacket; - private final List deferredPackets; + private final Object removePacket; + private final Object addPacket; + private final Object updatePacket; + private final Object updatePingPacket; + private final List deferredPackets; public TabRender(TabView view) { this.view = view; - this.removePacket = - NMSHacks.createPlayerInfoPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.REMOVE_PLAYER); - this.addPacket = - NMSHacks.createPlayerInfoPacket(PacketPlayOutPlayerInfo.EnumPlayerInfoAction.ADD_PLAYER); - this.updatePacket = - NMSHacks.createPlayerInfoPacket( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_DISPLAY_NAME); - this.updatePingPacket = - NMSHacks.createPlayerInfoPacket( - PacketPlayOutPlayerInfo.EnumPlayerInfoAction.UPDATE_LATENCY); + this.removePacket = NMSHacks.createPlayerInfoPacket(EnumPlayerInfoAction.REMOVE_PLAYER); + this.addPacket = NMSHacks.createPlayerInfoPacket(EnumPlayerInfoAction.ADD_PLAYER); + this.updatePacket = NMSHacks.createPlayerInfoPacket(EnumPlayerInfoAction.UPDATE_DISPLAY_NAME); + this.updatePingPacket = NMSHacks.createPlayerInfoPacket(EnumPlayerInfoAction.UPDATE_LATENCY); this.deferredPackets = new ArrayList<>(); } @@ -42,7 +35,7 @@ private String teamName(int slot) { return "\u0001TabView" + String.format("%03d", slot); } - private void send(Packet packet) { + private void send(Object packet) { NMSHacks.sendPacket(this.view.getViewer(), packet); } @@ -52,28 +45,24 @@ private String getJson(TabEntry entry) { private void appendAddition(TabEntry entry, int index) { String renderedDisplayName = this.getJson(entry); - NMSHacks.getPlayerInfoDataList(this.addPacket) - .add( - NMSHacks.playerListPacketData( - this.addPacket, - entry.getId(), - entry.getName(this.view), - entry.getGamemode(), - entry.getPing(), - entry.getSkin(this.view), - renderedDisplayName)); + NMSHacks.addPlayerInfoToPacket( + this.addPacket, + entry.getId(), + entry.getName(this.view), + entry.getGamemode(), + entry.getPing(), + entry.getSkin(this.view), + renderedDisplayName); // Due to a client bug, display name is ignored in ADD_PLAYER packets, // so we have to send an UPDATE_DISPLAY_NAME afterward. - NMSHacks.getPlayerInfoDataList(this.updatePacket) - .add(NMSHacks.playerListPacketData(this.updatePacket, entry.getId(), renderedDisplayName)); + NMSHacks.addPlayerInfoToPacket(this.updatePacket, entry.getId(), renderedDisplayName); this.updateFakeEntity(entry, true); } private void appendRemoval(TabEntry entry) { - NMSHacks.getPlayerInfoDataList(this.removePacket) - .add(NMSHacks.playerListPacketData(this.removePacket, entry.getId())); + NMSHacks.addPlayerInfoToPacket(this.removePacket, entry.getId()); int entityId = entry.getFakeEntityId(this.view); if (entityId >= 0) { @@ -94,13 +83,13 @@ private void joinSlot(TabEntry entry, int index) { } public void finish() { - if (!NMSHacks.getPlayerInfoDataList(this.removePacket).isEmpty()) this.send(this.removePacket); - if (!NMSHacks.getPlayerInfoDataList(this.addPacket).isEmpty()) this.send(this.addPacket); - if (!NMSHacks.getPlayerInfoDataList(this.updatePacket).isEmpty()) this.send(this.updatePacket); - if (!NMSHacks.getPlayerInfoDataList(this.updatePingPacket).isEmpty()) + if (NMSHacks.playerInfoDataListNotEmpty(this.removePacket)) this.send(this.removePacket); + if (NMSHacks.playerInfoDataListNotEmpty(this.addPacket)) this.send(this.addPacket); + if (NMSHacks.playerInfoDataListNotEmpty(this.updatePacket)) this.send(this.updatePacket); + if (NMSHacks.playerInfoDataListNotEmpty(this.updatePingPacket)) this.send(this.updatePingPacket); - for (Packet packet : this.deferredPackets) { + for (Object packet : this.deferredPackets) { this.send(packet); } } @@ -112,15 +101,8 @@ public void changeSlot(TabEntry entry, int oldIndex, int newIndex) { public void createSlot(TabEntry entry, int index) { String teamName = this.teamName(index); - this.send( - NMSHacks.teamCreatePacket( - teamName, - teamName, - "", - "", - false, - false, - Collections.singleton(entry.getName(this.view)))); + Collection players = Collections.singleton(entry.getName(this.view)); + this.send(NMSHacks.teamCreatePacket(teamName, teamName, "", "", false, false, players)); this.appendAddition(entry, index); } @@ -145,13 +127,11 @@ public void refreshEntry(TabEntry entry, int index) { } public void updateEntry(TabEntry entry, int index) { - NMSHacks.getPlayerInfoDataList(this.updatePacket) - .add(NMSHacks.playerListPacketData(this.updatePacket, entry.getId(), this.getJson(entry))); + NMSHacks.addPlayerInfoToPacket(this.updatePacket, entry.getId(), this.getJson(entry)); } public void updatePing(TabEntry entry, int index) { - NMSHacks.getPlayerInfoDataList(this.updatePingPacket) - .add(NMSHacks.playerListPacketData(this.updatePingPacket, entry.getId(), entry.getPing())); + NMSHacks.addPlayerInfoToPacket(this.updatePingPacket, entry.getId(), entry.getPing()); } public void setHeaderFooter(Component header, Component footer) { From 9f12b996d15cfc18e92487c0eea023e3f53754e8 Mon Sep 17 00:00:00 2001 From: zzuf <11651753+zzufx@users.noreply.github.com> Date: Sat, 8 Jul 2023 18:07:26 -0300 Subject: [PATCH 18/23] Restore Objectives as a translatable gamemode (#1201) Signed-off-by: zzuf <11651753+zzufx@users.noreply.github.com> --- core/src/main/java/tc/oc/pgm/api/map/Gamemode.java | 3 +-- core/src/main/java/tc/oc/pgm/scoreboard/SidebarRenderer.java | 3 ++- util/src/main/i18n/templates/gamemode.properties | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/api/map/Gamemode.java b/core/src/main/java/tc/oc/pgm/api/map/Gamemode.java index bc0f6fb0f8..ffba05d571 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/Gamemode.java +++ b/core/src/main/java/tc/oc/pgm/api/map/Gamemode.java @@ -24,8 +24,7 @@ public enum Gamemode { SCOREBOX("scorebox", "Scorebox", "Scorebox"), SKYWARS("skywars", "Skywars", "Skywars"), SURVIVAL_GAMES("sg", "Survival Games", "SG"), - DEATHMATCH("tdm", "Deathmatch", "TDM"), - OBJECTIVES("obj", "Objectives", "Objectives"); + DEATHMATCH("tdm", "Deathmatch", "TDM"); private final String id; diff --git a/core/src/main/java/tc/oc/pgm/scoreboard/SidebarRenderer.java b/core/src/main/java/tc/oc/pgm/scoreboard/SidebarRenderer.java index c54def5b12..bf1ae95d60 100644 --- a/core/src/main/java/tc/oc/pgm/scoreboard/SidebarRenderer.java +++ b/core/src/main/java/tc/oc/pgm/scoreboard/SidebarRenderer.java @@ -3,6 +3,7 @@ import static net.kyori.adventure.text.Component.empty; import static net.kyori.adventure.text.Component.space; import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.Component.translatable; import java.util.ArrayList; import java.util.Collection; @@ -84,7 +85,7 @@ public Component renderTitle() { } // When there are multiple, primary game modes - games.set(0, text(Gamemode.OBJECTIVES.getFullName(), NamedTextColor.AQUA)); + games.set(0, translatable("gamemode.generic.name", NamedTextColor.AQUA)); break; } diff --git a/util/src/main/i18n/templates/gamemode.properties b/util/src/main/i18n/templates/gamemode.properties index 74240f4237..42858ad77d 100644 --- a/util/src/main/i18n/templates/gamemode.properties +++ b/util/src/main/i18n/templates/gamemode.properties @@ -1,3 +1,5 @@ +gamemode.generic.name = Objectives + objective.name.monument = Monument objective.name.antenna = Antenna From 395eb1c664704f7e81a202f87ab9a56d137b31a8 Mon Sep 17 00:00:00 2001 From: Christopher White Date: Tue, 18 Jul 2023 15:59:24 -0700 Subject: [PATCH 19/23] Fix potion spawners (#1207) Signed-off-by: Christopher White <18whitechristop@gmail.com> --- .../java/tc/oc/pgm/util/nms/entity/potion/EntityPotion1_8.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion1_8.java index a9604c118f..f90f12bda8 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion1_8.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/potion/EntityPotion1_8.java @@ -22,7 +22,7 @@ public EntityPotion1_8(Location location, ItemStack potionItem) { @Override public void spawn() { - handle.spawnIn(handle.getWorld()); + handle.getWorld().addEntity(handle); } @Override From 3c3cbfbe8b648d0bcd792bbbb8e0915bc452cbac Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 18 Jul 2023 23:59:50 +0100 Subject: [PATCH 20/23] Allow username tabbing in channel messages (#1203) Signed-off-by: Pugzy --- .../oc/pgm/command/util/PGMCommandGraph.java | 3 +++ .../tc/oc/pgm/listeners/ChatDispatcher.java | 18 +++++++++++++----- core/src/main/java/tc/oc/pgm/util/Players.java | 8 ++++++++ 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/command/util/PGMCommandGraph.java b/core/src/main/java/tc/oc/pgm/command/util/PGMCommandGraph.java index 749d878d2b..48b8497f22 100644 --- a/core/src/main/java/tc/oc/pgm/command/util/PGMCommandGraph.java +++ b/core/src/main/java/tc/oc/pgm/command/util/PGMCommandGraph.java @@ -89,6 +89,7 @@ import tc.oc.pgm.teams.Team; import tc.oc.pgm.teams.TeamMatchModule; import tc.oc.pgm.util.Audience; +import tc.oc.pgm.util.Players; public class PGMCommandGraph extends CommandGraph { @@ -215,5 +216,7 @@ protected void setupParsers() { registerParser(Filter.class, FilterArgumentParser::new); registerParser(SettingKey.class, new EnumParser<>(SettingKey.class, CommandKeys.SETTING_KEY)); registerParser(SettingValue.class, new SettingValueParser()); + + parsers.registerSuggestionProvider("players", Players::suggestPlayers); } } diff --git a/core/src/main/java/tc/oc/pgm/listeners/ChatDispatcher.java b/core/src/main/java/tc/oc/pgm/listeners/ChatDispatcher.java index 5084c14b92..daf573c6ca 100644 --- a/core/src/main/java/tc/oc/pgm/listeners/ChatDispatcher.java +++ b/core/src/main/java/tc/oc/pgm/listeners/ChatDispatcher.java @@ -105,7 +105,9 @@ public boolean isMuted(MatchPlayer player) { @CommandMethod("g|all [message]") @CommandDescription("Send a message to everyone") public void sendGlobal( - Match match, @NotNull MatchPlayer sender, @Argument("message") @Greedy String message) { + Match match, + @NotNull MatchPlayer sender, + @Argument(value = "message", suggestions = "players") @Greedy String message) { if (Integration.isVanished(sender.getBukkit())) { sendAdmin(match, sender, message); return; @@ -127,7 +129,9 @@ public void sendGlobal( @CommandMethod("t [message]") @CommandDescription("Send a message to your team") public void sendTeam( - Match match, @NotNull MatchPlayer sender, @Argument("message") @Greedy String message) { + Match match, + @NotNull MatchPlayer sender, + @Argument(value = "message", suggestions = "players") @Greedy String message) { if (Integration.isVanished(sender.getBukkit())) { sendAdmin(match, sender, message); return; @@ -161,7 +165,9 @@ public void sendTeam( @CommandDescription("Send a message to operators") @CommandPermission(Permissions.ADMINCHAT) public void sendAdmin( - Match match, @NotNull MatchPlayer sender, @Argument("message") @Greedy String message) { + Match match, + @NotNull MatchPlayer sender, + @Argument(value = "message", suggestions = "players") @Greedy String message) { // If a player managed to send a default message without permissions, reset their chat channel if (!sender.getBukkit().hasPermission(Permissions.ADMINCHAT)) { sender.getSettings().resetValue(SettingKey.CHAT); @@ -196,7 +202,7 @@ public void sendDirect( Match match, @NotNull MatchPlayer sender, @Argument("player") MatchPlayer receiver, - @Argument("message") @Greedy String message) { + @Argument(value = "message", suggestions = "players") @Greedy String message) { if (Integration.isVanished(sender.getBukkit())) throw exception("vanish.chat.deny"); if (receiver.equals(sender)) throw exception("command.message.self"); @@ -264,7 +270,9 @@ private String formatPrivateMessage(String key, CommandSender viewer) { @CommandMethod("reply|r ") @CommandDescription("Reply to a direct message") public void sendReply( - Match match, @NotNull MatchPlayer sender, @Argument("message") @Greedy String message) { + Match match, + @NotNull MatchPlayer sender, + @Argument(value = "message", suggestions = "players") @Greedy String message) { MatchPlayer receiver = manager.getPlayer(getLastMessagedId(sender.getBukkit())); if (receiver == null) throw exception("command.message.noReply", text("/msg")); diff --git a/core/src/main/java/tc/oc/pgm/util/Players.java b/core/src/main/java/tc/oc/pgm/util/Players.java index ba72bac143..2c0b5e133d 100644 --- a/core/src/main/java/tc/oc/pgm/util/Players.java +++ b/core/src/main/java/tc/oc/pgm/util/Players.java @@ -1,5 +1,6 @@ package tc.oc.pgm.util; +import cloud.commandframework.context.CommandContext; import java.util.List; import java.util.stream.Collectors; import org.bukkit.Bukkit; @@ -9,6 +10,7 @@ import tc.oc.pgm.api.Permissions; import tc.oc.pgm.api.integration.Integration; import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.command.util.CommandKeys; public class Players { @@ -53,4 +55,10 @@ public static MatchPlayer getMatchPlayer(CommandSender sender, String query) { public static boolean isFriend(CommandSender viewer, Player other) { return viewer instanceof Player && Integration.isFriend((Player) viewer, other); } + + public static List suggestPlayers(CommandContext context, String input) { + List queue = context.get(CommandKeys.INPUT_QUEUE); + + return getPlayerNames(context.getSender(), queue.isEmpty() ? "" : queue.get(queue.size() - 1)); + } } From 7fa2a4d302a37b485159c399b57ddbd1402cd390 Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Wed, 19 Jul 2023 01:00:19 +0200 Subject: [PATCH 21/23] Add support for world folder in variants (#1204) Signed-off-by: Pablo Herrera --- core/src/main/java/tc/oc/pgm/api/map/MapInfo.java | 4 ++++ core/src/main/java/tc/oc/pgm/api/map/MapSource.java | 3 ++- core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java | 9 +++++++++ .../main/java/tc/oc/pgm/map/source/SystemMapSource.java | 8 ++++---- core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java | 2 +- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java b/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java index 77a94e56ef..3ef78245bc 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java +++ b/core/src/main/java/tc/oc/pgm/api/map/MapInfo.java @@ -27,6 +27,10 @@ public interface MapInfo extends Comparable, Cloneable { @Nullable String getVariant(); + /** @return the subfolder in which the world is in, or null for the parent folder */ + @Nullable + String getWorldFolder(); + /** * Get the proto of the map's {@link org.jdom2.Document}. * diff --git a/core/src/main/java/tc/oc/pgm/api/map/MapSource.java b/core/src/main/java/tc/oc/pgm/api/map/MapSource.java index 1b745f5fed..f35e9c689a 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/MapSource.java +++ b/core/src/main/java/tc/oc/pgm/api/map/MapSource.java @@ -39,10 +39,11 @@ public interface MapSource { /** * Download the {@link org.bukkit.World} files to a local directory. * + * @param folder subfolder to download, null for parent * @param dir An existent, but empty directory. * @throws MapMissingException If an error occurs while creating the files. */ - void downloadTo(File dir) throws MapMissingException; + void downloadTo(String folder, File dir) throws MapMissingException; /** * Get an {@link InputStream} of the map's xml document. diff --git a/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java b/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java index 8ffb4da8a3..2fa4a0c66f 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java @@ -40,6 +40,7 @@ public class MapInfoImpl implements MapInfo { private final String id; private final String variant; + private final String worldFolder; private final Version proto; private final Version version; private final Phase phase; @@ -66,6 +67,7 @@ public MapInfoImpl(MapSource source, Element root) throws InvalidXMLException { this.variant = source.getVariant(); String tmpName = assertNotNull(Node.fromRequiredChildOrAttr(root, "name").getValueNormalize()); + String tmpWorld = null; if (variant != null) { Element variantEl = root.getChildren("variant").stream() @@ -76,7 +78,9 @@ public MapInfoImpl(MapSource source, Element root) throws InvalidXMLException { boolean override = XMLUtils.parseBoolean(Node.fromAttr(variantEl, "override"), false); tmpName = (override ? "" : tmpName + ": ") + variantEl.getTextNormalize(); + tmpWorld = variantEl.getAttributeValue("world"); } + this.worldFolder = tmpWorld; this.name = tmpName; this.normalizedName = StringUtils.normalize(name); @@ -126,6 +130,11 @@ public String getVariant() { return variant; } + @Override + public String getWorldFolder() { + return worldFolder; + } + @Override public Version getProto() { return proto; diff --git a/core/src/main/java/tc/oc/pgm/map/source/SystemMapSource.java b/core/src/main/java/tc/oc/pgm/map/source/SystemMapSource.java index 56253fda8b..98b515dddf 100644 --- a/core/src/main/java/tc/oc/pgm/map/source/SystemMapSource.java +++ b/core/src/main/java/tc/oc/pgm/map/source/SystemMapSource.java @@ -33,8 +33,8 @@ public SystemMapSource(Path dir, @Nullable String variant) { this.storedIncludes = Sets.newHashSet(); } - private File getDirectory() throws MapMissingException { - final File dir = this.dir.toFile(); + private File getDirectory(String subdir) throws MapMissingException { + final File dir = subdir == null ? this.dir.toFile() : this.dir.resolve(subdir).toFile(); if (!dir.exists()) { throw new MapMissingException(dir.getPath(), "Unable to find map folder (was it moved?)"); @@ -73,8 +73,8 @@ public String getId() { } @Override - public void downloadTo(File dst) throws MapMissingException { - final File src = getDirectory(); + public void downloadTo(String worldDir, File dst) throws MapMissingException { + final File src = getDirectory(worldDir); try { FileUtils.copy(src, dst, true); } catch (IOException e) { diff --git a/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java b/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java index 7eb589a63c..3b47f15ece 100644 --- a/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java +++ b/core/src/main/java/tc/oc/pgm/match/MatchFactoryImpl.java @@ -230,7 +230,7 @@ private InitWorldStage advanceSync() throws MapMissingException { final File dir = getDirectory(); if (dir.mkdirs()) { - map.getInfo().getSource().downloadTo(dir); + map.getInfo().getSource().downloadTo(map.getInfo().getWorldFolder(), dir); } else { throw new MapMissingException(dir.getPath(), "Unable to mkdirs world directory"); } From 51ac7911dec36073829515db38e066049805d009 Mon Sep 17 00:00:00 2001 From: Christopher White Date: Sun, 6 Aug 2023 18:43:18 -1000 Subject: [PATCH 22/23] Support 1.9 using ProtocolLib (#1199) Signed-off-by: Christopher White <18whitechristop@gmail.com> --- core/pom.xml | 1 + core/src/main/java/tc/oc/pgm/PGMConfig.java | 7 + core/src/main/java/tc/oc/pgm/PGMPlugin.java | 13 + core/src/main/java/tc/oc/pgm/api/Config.java | 7 + .../pgm/flag/LegacyFlagBeamMatchModule.java | 4 +- .../tc/oc/pgm/shield/ShieldPlayerModule.java | 12 +- core/src/main/resources/config.yml | 1 + core/src/main/resources/plugin.yml | 2 +- pom.xml | 22 + .../tc/oc/pgm/util/attribute/Attribute.java | 8 + .../tc/oc/pgm/util/bukkit/BukkitUtils.java | 2 +- .../java/tc/oc/pgm/util/bukkit/Platform.java | 95 +++ .../java/tc/oc/pgm/util/nms/NMSHacks.java | 34 +- .../java/tc/oc/pgm/util/nms/NMSHacksNoOp.java | 127 +++- .../tc/oc/pgm/util/nms/NMSHacksPlatform.java | 8 +- .../attribute/AttributeInstanceBukkit.java | 66 ++ .../nms/attribute/AttributeMapBukkit.java | 23 + .../nms/attribute/AttributeUtilBukkit.java | 76 +++ .../pgm/util/nms/entity/fake/FakeEntity.java | 7 +- .../nms/entity/fake/FakeEntityImpl1_8.java | 6 - .../util/nms/entity/fake/FakeEntityNoOp.java | 6 - .../entity/fake/FakeEntityProtocolLib.java | 12 + .../armorstand/FakeArmorStandProtocolLib.java | 23 + .../wither/FakeWitherSkullProtocolLib.java | 15 + .../java/tc/oc/pgm/util/nms/reflect/Refl.java | 165 +++++ .../tc/oc/pgm/util/nms/reflect/Reflect.java | 52 ++ .../pgm/util/nms/reflect/ReflectionProxy.java | 352 ++++++++++ .../pgm/util/nms/{ => v1_8}/NMSHacks1_8.java | 114 ++-- .../nms/{ => v1_8}/NMSHacksSportPaper.java | 60 +- .../tc/oc/pgm/util/nms/v1_9/NMSHacks1_9.java | 154 +++++ .../util/nms/v1_9/NMSHacksProtocolLib.java | 642 ++++++++++++++++++ .../reflect/MinecraftReflectionUtils.java | 32 +- .../oc/pgm/util/tablist/TablistResizer.java | 29 + 33 files changed, 2027 insertions(+), 150 deletions(-) create mode 100644 util/src/main/java/tc/oc/pgm/util/bukkit/Platform.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeInstanceBukkit.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMapBukkit.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeUtilBukkit.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityProtocolLib.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/fake/armorstand/FakeArmorStandProtocolLib.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/entity/fake/wither/FakeWitherSkullProtocolLib.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/reflect/Refl.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/reflect/Reflect.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/reflect/ReflectionProxy.java rename util/src/main/java/tc/oc/pgm/util/nms/{ => v1_8}/NMSHacks1_8.java (92%) rename util/src/main/java/tc/oc/pgm/util/nms/{ => v1_8}/NMSHacksSportPaper.java (86%) create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/v1_9/NMSHacks1_9.java create mode 100644 util/src/main/java/tc/oc/pgm/util/nms/v1_9/NMSHacksProtocolLib.java create mode 100644 util/src/main/java/tc/oc/pgm/util/tablist/TablistResizer.java diff --git a/core/pom.xml b/core/pom.xml index 4483c1ed84..13522ae302 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -69,6 +69,7 @@ org.slf4j:slf4j-api:* fr.minuskube.inv:smart-invs net.objecthunter:exp4j:* + io.github.bananapuncher714:nbteditor:* diff --git a/core/src/main/java/tc/oc/pgm/PGMConfig.java b/core/src/main/java/tc/oc/pgm/PGMConfig.java index 0f7ecf365b..8cd8b94efe 100644 --- a/core/src/main/java/tc/oc/pgm/PGMConfig.java +++ b/core/src/main/java/tc/oc/pgm/PGMConfig.java @@ -90,6 +90,7 @@ public final class PGMConfig implements Config { // ui.* private final boolean showSideBar; private final boolean showTabList; + private final boolean resizeTabList; private final boolean showTabListPing; private final boolean showProximity; private final boolean showFireworks; @@ -183,6 +184,7 @@ public final class PGMConfig implements Config { this.showProximity = parseBoolean(config.getString("ui.proximity", "false")); this.showSideBar = parseBoolean(config.getString("ui.sidebar", "true")); this.showTabList = parseBoolean(config.getString("ui.tablist", "true")); + this.resizeTabList = parseBoolean(config.getString("ui.tablist-resize", "false")); this.showTabListPing = parseBoolean(config.getString("ui.ping", "true")); this.participantsSeeObservers = parseBoolean(config.getString("ui.participants-see-observers", "true")); @@ -573,6 +575,11 @@ public boolean showTabList() { return showTabList; } + @Override + public boolean resizeTabList() { + return resizeTabList; + } + @Override public boolean showTabListPing() { return showTabListPing; diff --git a/core/src/main/java/tc/oc/pgm/PGMPlugin.java b/core/src/main/java/tc/oc/pgm/PGMPlugin.java index c36b9fd373..5475d3698b 100644 --- a/core/src/main/java/tc/oc/pgm/PGMPlugin.java +++ b/core/src/main/java/tc/oc/pgm/PGMPlugin.java @@ -73,6 +73,8 @@ import tc.oc.pgm.util.listener.ItemTransferListener; import tc.oc.pgm.util.listener.PlayerBlockListener; import tc.oc.pgm.util.listener.PlayerMoveListener; +import tc.oc.pgm.util.nms.NMSHacks; +import tc.oc.pgm.util.tablist.TablistResizer; import tc.oc.pgm.util.text.TextException; import tc.oc.pgm.util.text.TextTranslations; import tc.oc.pgm.util.xml.InvalidXMLException; @@ -114,6 +116,9 @@ public void onEnable() { return; // Indicates the plugin failed to load, so exit early } + // Sanity test PGM is running on a supported version before doing any work + NMSHacks.allocateEntityId(); + Permissions.registerAll(); final CommandSender console = getServer().getConsoleSender(); @@ -214,6 +219,14 @@ public void onEnable() { if (config.showTabList()) { matchTabManager = new MatchTabManager(this); + + if (config.resizeTabList()) { + if (this.getServer().getPluginManager().isPluginEnabled("ProtocolLib")) { + TablistResizer.registerAdapter(this); + } else { + logger.warning("ProtocolLib is required when 'ui.resize' is enabled"); + } + } } if (!config.getUptimeLimit().isNegative()) { diff --git a/core/src/main/java/tc/oc/pgm/api/Config.java b/core/src/main/java/tc/oc/pgm/api/Config.java index 7773225963..61af12f148 100644 --- a/core/src/main/java/tc/oc/pgm/api/Config.java +++ b/core/src/main/java/tc/oc/pgm/api/Config.java @@ -208,6 +208,13 @@ public interface Config { */ boolean showTabList(); + /** + * Gets whether the tab list should be resized to 4 rows for 1.7 players. + * + * @return If the tab list will be resized. + */ + boolean resizeTabList(); + /** * Gets whether the tab list is should show real ping. * diff --git a/core/src/main/java/tc/oc/pgm/flag/LegacyFlagBeamMatchModule.java b/core/src/main/java/tc/oc/pgm/flag/LegacyFlagBeamMatchModule.java index c6bfd52a44..d618d8978a 100644 --- a/core/src/main/java/tc/oc/pgm/flag/LegacyFlagBeamMatchModule.java +++ b/core/src/main/java/tc/oc/pgm/flag/LegacyFlagBeamMatchModule.java @@ -189,8 +189,8 @@ public void show(MatchPlayer player) { spawn(bukkit, base(player)); segments.forEach(segment -> spawn(bukkit, segment)); range(1, segments.size()) - .forEachOrdered(i -> segments.get(i - 1).ride(bukkit, segments.get(i).entity())); - base(player).ride(bukkit, segments.get(0).entity()); + .forEachOrdered(i -> segments.get(i - 1).ride(bukkit, segments.get(i).entityId())); + base(player).ride(bukkit, segments.get(0).entityId()); update(player); } diff --git a/core/src/main/java/tc/oc/pgm/shield/ShieldPlayerModule.java b/core/src/main/java/tc/oc/pgm/shield/ShieldPlayerModule.java index 3529efdf72..89048a40af 100644 --- a/core/src/main/java/tc/oc/pgm/shield/ShieldPlayerModule.java +++ b/core/src/main/java/tc/oc/pgm/shield/ShieldPlayerModule.java @@ -56,6 +56,16 @@ public void remove() { addAbsorption(-shieldHealth); } + static Sound RECHARGE_SOUND = resolveRechaseSound(); + + static Sound resolveRechaseSound() { + try { + return Sound.ORB_PICKUP; + } catch (Throwable t) { + return Sound.valueOf("ENTITY_EXPERIENCE_ORB_PICKUP"); + } + } + /** * Recharge the shield to its maximum health. If the player has more absorption than the current * shield strength, the excess is preserved. @@ -66,7 +76,7 @@ void recharge() { logger.fine("Recharging shield: shield=" + shieldHealth + " delta=" + delta); shieldHealth = parameters.maxHealth; addAbsorption(delta); - bukkit.playSound(bukkit.getLocation(), Sound.ORB_PICKUP, 1, 2); + bukkit.playSound(bukkit.getLocation(), RECHARGE_SOUND, 1, 2); } } diff --git a/core/src/main/resources/config.yml b/core/src/main/resources/config.yml index d083bb805e..8b0b6f1b05 100644 --- a/core/src/main/resources/config.yml +++ b/core/src/main/resources/config.yml @@ -74,6 +74,7 @@ gameplay: ui: sidebar: true # Enable the side bar? tablist: true # Enable the tab list? + tablist-resize: false # Resize the tab list for 1.7 players? Requires ProtocolLib ping: false # Should tab list show real ping? proximity: false # Should the proximity of objectives be visible? fireworks: true # Spawn fireworks after objectives are completed? diff --git a/core/src/main/resources/plugin.yml b/core/src/main/resources/plugin.yml index c33343788e..5536ab8551 100644 --- a/core/src/main/resources/plugin.yml +++ b/core/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: PGM -softdepend: [ViaVersion] +softdepend: [ViaVersion, ProtocolLib] description: ${project.parent.description} main: ${project.mainClass} version: ${project.version} (git-${git.commit.id.abbrev}) diff --git a/pom.xml b/pom.xml index 43a7bed1f4..a1960c56eb 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,14 @@ pgm-fyi https://repo.pgm.fyi/snapshots + + dmulloy2-repo + https://repo.dmulloy2.net/repository/public/ + + + CodeMC + https://repo.codemc.org/repository/maven-public/ + @@ -98,6 +106,13 @@ + + + com.comphenix.protocol + ProtocolLib + 5.0.0 + + org.jdom @@ -173,6 +188,13 @@ 1.2.1 compile + + + + io.github.bananapuncher714 + nbteditor + 7.18.5 + diff --git a/util/src/main/java/tc/oc/pgm/util/attribute/Attribute.java b/util/src/main/java/tc/oc/pgm/util/attribute/Attribute.java index 97f5412a66..58f81cdbb9 100644 --- a/util/src/main/java/tc/oc/pgm/util/attribute/Attribute.java +++ b/util/src/main/java/tc/oc/pgm/util/attribute/Attribute.java @@ -15,6 +15,14 @@ public enum Attribute { GENERIC_MOVEMENT_SPEED("generic.movementSpeed"), /** Attack damage of an Entity. */ GENERIC_ATTACK_DAMAGE("generic.attackDamage"), + /** Attack speed of an Entity */ + GENERIC_ATTACK_SPEED("generic.attackSpeed"), + /** Armor of an Entity */ + GENERIC_ARMOR("generic.armor"), + /** Armor Toughness of an Entity */ + GENERIC_ARMOR_TOUGHNESS("generic.armorToughness"), + /** Luck */ + GENERIC_LUCK("generic.luck"), /** Strength with which a horse will jump. */ HORSE_JUMP_STRENGTH("horse.jumpStrength"), /** Chance of a zombie to spawn reinforcements. */ diff --git a/util/src/main/java/tc/oc/pgm/util/bukkit/BukkitUtils.java b/util/src/main/java/tc/oc/pgm/util/bukkit/BukkitUtils.java index 6175fd5614..55dc31cbd4 100644 --- a/util/src/main/java/tc/oc/pgm/util/bukkit/BukkitUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/bukkit/BukkitUtils.java @@ -23,7 +23,7 @@ static Plugin getPlugin() { return PLUGIN.get(); } - Boolean isSportPaper = Bukkit.getServer().getVersion().contains("SportPaper"); + Boolean isSportPaper = Platform.SERVER_PLATFORM == Platform.SPORTPAPER_1_8; static boolean isSportPaper() { return isSportPaper; diff --git a/util/src/main/java/tc/oc/pgm/util/bukkit/Platform.java b/util/src/main/java/tc/oc/pgm/util/bukkit/Platform.java new file mode 100644 index 0000000000..5774f56fd4 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/bukkit/Platform.java @@ -0,0 +1,95 @@ +package tc.oc.pgm.util.bukkit; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import org.bukkit.Bukkit; +import tc.oc.pgm.util.ClassLogger; +import tc.oc.pgm.util.nms.NMSHacksNoOp; +import tc.oc.pgm.util.nms.NMSHacksPlatform; +import tc.oc.pgm.util.nms.v1_8.NMSHacksSportPaper; +import tc.oc.pgm.util.nms.v1_9.NMSHacks1_9; +import tc.oc.pgm.util.reflect.ReflectionUtils; + +public enum Platform { + UNKNOWN("UNKNOWN", "UNKNOWN", NMSHacksNoOp.class, false), + SPORTPAPER_1_8("SportPaper", "1.8", NMSHacksSportPaper.class, false), + SPIGOT_1_8( + "Spigot", + "1.8", // NMSHacks1_8 causes issues with other versions, get it dynamically + (Class) + ReflectionUtils.getClassFromName("tc.oc.pgm.util.nms.v1_8.NMSHacks1_8"), + false), + PAPER_1_8( + "Paper", + "1.8", // NMSHacks1_8 causes issues with other versions, get it dynamically + (Class) + ReflectionUtils.getClassFromName("tc.oc.pgm.util.nms.v1_8.NMSHacks1_8"), + false), + SPIGOT_1_9("Spigot", "1.9", NMSHacks1_9.class, true), + PAPER_1_9("Paper", "1.9", NMSHacks1_9.class, true); + + private static ClassLogger logger = ClassLogger.get(Platform.class);; + public static Platform SERVER_PLATFORM = computeServerPlatform(); + + private static Platform computeServerPlatform() { + String versionString = Bukkit.getServer().getVersion(); + for (Platform platform : Platform.values()) { + if (versionString.contains(platform.variant) + && versionString.contains("MC: " + platform.majorVersion)) { + return platform; + } + } + return UNKNOWN; + } + + private final String variant; + private final String majorVersion; + private final Class nmsHacksClass; + private final boolean requiresProtocolLib; + + Platform( + String variant, + String majorVersion, + Class nmsHacksClass, + boolean requiresProtocolLib) { + this.variant = variant; + this.majorVersion = majorVersion; + this.nmsHacksClass = nmsHacksClass; + this.requiresProtocolLib = requiresProtocolLib; + } + + public NMSHacksPlatform getNMSHacks() { + if (this == UNKNOWN) { + Bukkit.getServer().getPluginManager().disablePlugin(BukkitUtils.getPlugin()); + throw new UnsupportedOperationException("UNKNOWN Platform!"); + } + if (this.requiresProtocolLib + && !Bukkit.getServer().getPluginManager().isPluginEnabled("ProtocolLib")) { + Bukkit.getServer().getPluginManager().disablePlugin(BukkitUtils.getPlugin()); + throw new UnsupportedOperationException( + "ProtocolLib is required for PGM to run on " + this.toString()); + } + if (this == SERVER_PLATFORM) { + try { + logger.info("Detected server: " + this.toString()); + Constructor constructor = nmsHacksClass.getConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (InvocationTargetException + | InstantiationException + | IllegalAccessException + | NoSuchMethodException e) { + Bukkit.getServer().getPluginManager().disablePlugin(BukkitUtils.getPlugin()); + throw new RuntimeException(e); + } + } else { + Bukkit.getServer().getPluginManager().disablePlugin(BukkitUtils.getPlugin()); + throw new UnsupportedOperationException("getNMSHacks called for incorrect platform!"); + } + } + + @Override + public String toString() { + return this.variant + " (" + this.majorVersion + ")"; + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java index a4edf6303a..2cbe12a538 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks.java @@ -6,7 +6,6 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Logger; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.ChunkSnapshot; @@ -34,37 +33,17 @@ import org.bukkit.scoreboard.NameTagVisibility; import org.bukkit.util.Vector; import org.jetbrains.annotations.Nullable; -import tc.oc.pgm.util.ClassLogger; import tc.oc.pgm.util.attribute.AttributeMap; import tc.oc.pgm.util.attribute.AttributeModifier; import tc.oc.pgm.util.block.RayBlockIntersection; -import tc.oc.pgm.util.bukkit.BukkitUtils; +import tc.oc.pgm.util.bukkit.Platform; import tc.oc.pgm.util.nms.entity.fake.FakeEntity; import tc.oc.pgm.util.nms.entity.potion.EntityPotion; import tc.oc.pgm.util.skin.Skin; public interface NMSHacks { - NMSHacksPlatform INSTANCE = chooseNMSHacks(); - - static NMSHacksPlatform chooseNMSHacks() { - NMSHacksPlatform choice; - Logger logger = ClassLogger.get(NMSHacks.class); - - try { - if (BukkitUtils.isSportPaper()) { - choice = new NMSHacksSportPaper(); - logger.info("Using NMSHacksSportPaper"); - } else { - choice = new NMSHacks1_8(); - logger.info("Using NMSHacks1_8"); - } - } catch (Throwable throwable) { - logger.severe("You are trying to run PGM on an unsupported version!"); - throw throwable; - } - return choice; - } + NMSHacksPlatform INSTANCE = Platform.SERVER_PLATFORM.getNMSHacks(); AtomicInteger ENTITY_IDS = new AtomicInteger(Integer.MAX_VALUE); @@ -317,10 +296,19 @@ static boolean teleportRelative( return INSTANCE.teleportRelative(player, deltaPos, deltaYaw, deltaPitch, cause); } + static void sendSpawnEntityPacket( + Player player, int entityId, Location location, Vector velocity) { + INSTANCE.sendSpawnEntityPacket(player, entityId, location, velocity); + } + static void spawnFreezeEntity(Player player, int entityId, boolean legacy) { INSTANCE.spawnFreezeEntity(player, entityId, legacy); } + static void spawnFakeArmorStand(Player player, int entityId, Location location, Vector velocity) { + INSTANCE.spawnFakeArmorStand(player, entityId, location, velocity); + } + /** * Test if the given tool is capable of "efficiently" mining the given block. * diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java index 0a354e57b8..d8cf92f7e5 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksNoOp.java @@ -1,33 +1,49 @@ package tc.oc.pgm.util.nms; -import java.lang.reflect.Method; +import com.google.common.collect.SetMultimap; +import com.mojang.authlib.GameProfile; +import java.util.Collection; import java.util.HashSet; import java.util.Set; +import java.util.UUID; import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.WorldCreator; +import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.EntityType; import org.bukkit.entity.Item; +import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; +import org.bukkit.event.player.PlayerTeleportEvent; import org.bukkit.inventory.DoubleChestInventory; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.material.MaterialData; import org.bukkit.plugin.Plugin; import org.bukkit.potion.PotionEffectType; +import org.bukkit.util.Vector; import tc.oc.pgm.util.attribute.AttributeMap; +import tc.oc.pgm.util.attribute.AttributeModifier; import tc.oc.pgm.util.nms.attribute.AttributeMapNoOp; import tc.oc.pgm.util.nms.entity.fake.FakeEntity; import tc.oc.pgm.util.nms.entity.fake.FakeEntityNoOp; import tc.oc.pgm.util.nms.entity.potion.EntityPotion; import tc.oc.pgm.util.nms.entity.potion.EntityPotionBukkit; -import tc.oc.pgm.util.reflect.MinecraftReflectionUtils; -import tc.oc.pgm.util.reflect.ReflectionUtils; +import tc.oc.pgm.util.nms.reflect.Refl; +import tc.oc.pgm.util.nms.reflect.ReflectionProxy; +import tc.oc.pgm.util.skin.Skin; +import tc.oc.pgm.util.skin.Skins; public abstract class NMSHacksNoOp implements NMSHacksPlatform { + static Refl refl = ReflectionProxy.getProxy(Refl.class); @Override public boolean isCraftItemArrowEntity(Item item) { @@ -128,20 +144,14 @@ public Enchantment getEnchantment(String key) { } } - static Class craftWorldClass = MinecraftReflectionUtils.getCraftBukkitClass("CraftWorld"); - static Method getHandleMethod = ReflectionUtils.getMethod(craftWorldClass, "getHandle"); - static Class nmsWorldClass = MinecraftReflectionUtils.getNMSClass("World"); - static Method getTimeMethod = ReflectionUtils.getMethod(nmsWorldClass, "getTime"); - @Override public long getMonotonicTime(World world) { - Object handle = ReflectionUtils.callMethod(getHandleMethod, world); - return (long) ReflectionUtils.callMethod(getTimeMethod, handle); + return refl.getWorldTime(refl.getWorldHandle(world)); } @Override public int getPing(Player player) { - return 100; + return refl.getPlayerPing(refl.getPlayerHandle(player)); } @Override @@ -166,6 +176,52 @@ public ItemStack craftItemCopy(ItemStack item) { return item.clone(); } + @Override + public Set getBlocks(Chunk bukkitChunk, Material material) { + Set blockSet = new HashSet<>(); + ChunkSnapshot chunkSnapshot = bukkitChunk.getChunkSnapshot(); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int highestBlockY = chunkSnapshot.getHighestBlockYAt(x, z); + for (int y = 0; y < highestBlockY; y++) { + Block block = bukkitChunk.getBlock(x, y, z); + if (block.getType() == material) { + blockSet.add(block); + } + } + } + } + + return blockSet; + } + + @Override + public void setSkullMetaOwner(SkullMeta meta, String name, UUID uuid, Skin skin) { + GameProfile gameProfile = new GameProfile(uuid, name); + Skins.setProperties(skin, gameProfile.getProperties()); + refl.setSkullProfile(meta, gameProfile); + } + + @Override + public WorldCreator detectWorld(String worldName) { + return null; // Usage handles this nicely + } + + @Override + public void setAbsorption(LivingEntity entity, double health) { + refl.setAbsorptionHearts(refl.getCraftEntityHandle(entity), (float) health); + } + + @Override + public double getAbsorption(LivingEntity entity) { + return refl.getAbsorptionHearts(refl.getCraftEntityHandle(entity)); + } + + @Override + public Skin getPlayerSkinForViewer(Player player, Player viewer) { + return getPlayerSkin(player); // not possible here outside of sportpaper + } + @Override public Set getBlockStates(Material material) { // TODO: MaterialData is not version compatible @@ -176,6 +232,48 @@ public Set getBlockStates(Material material) { return materialDataSet; } + @Override + public boolean teleportRelative( + Player player, + Vector deltaPos, + float deltaYaw, + float deltaPitch, + PlayerTeleportEvent.TeleportCause cause) { + // From = Players current Location + Location from = player.getLocation(); + // To = Players new Location if Teleport is Successful + Location to = from.clone().add(deltaPos); + to.setYaw(to.getYaw() + deltaYaw); + to.setPitch(to.getPitch() + deltaPitch); + + return player.teleport(to, cause); + } + + @Override + public void resetDimension(World world) { + // NoOp Other dimensions dont currently work in PGM + } + + @Override + public void setCanDestroy(ItemMeta itemMeta, Collection materials) { + setMaterialCollection(itemMeta, materials, "CanDestroy"); + } + + @Override + public Set getCanDestroy(ItemMeta itemMeta) { + return getMaterialCollection(itemMeta, "CanDestroy"); + } + + @Override + public void setCanPlaceOn(ItemMeta itemMeta, Collection materials) { + setMaterialCollection(itemMeta, materials, "CanPlaceOn"); + } + + @Override + public Set getCanPlaceOn(ItemMeta itemMeta) { + return getMaterialCollection(itemMeta, "CanPlaceOn"); + } + @Override public void setBlockStateData(BlockState state, MaterialData materialData) { state.setType(materialData.getItemType()); @@ -187,6 +285,13 @@ public double getTPS() { return 20.0; } + @Override + public void copyAttributeModifiers(ItemMeta destination, ItemMeta source) { + SetMultimap attributeModifiers = getAttributeModifiers(source); + attributeModifiers.putAll(getAttributeModifiers(destination)); + applyAttributeModifiers(attributeModifiers, destination); + } + @Override public AttributeMap buildAttributeMap(Player player) { return new AttributeMapNoOp(); diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java index d110e32ae0..4894d88f30 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksPlatform.java @@ -154,10 +154,16 @@ boolean teleportRelative( float deltaPitch, PlayerTeleportEvent.TeleportCause cause); - void sendSpawnEntityPacket(Player player, int entityId, Location location); + void sendSpawnEntityPacket(Player player, int entityId, Location location, Vector velocity); + + default void sendSpawnEntityPacket(Player player, int entityId, Location location) { + sendSpawnEntityPacket(player, entityId, location, new Vector()); + } void spawnFreezeEntity(Player player, int entityId, boolean legacy); + void spawnFakeArmorStand(Player player, int entityId, Location location, Vector velocity); + /** * Test if the given tool is capable of "efficiently" mining the given block. * diff --git a/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeInstanceBukkit.java b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeInstanceBukkit.java new file mode 100644 index 0000000000..55f2cd5b5d --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeInstanceBukkit.java @@ -0,0 +1,66 @@ +package tc.oc.pgm.util.nms.attribute; + +import java.util.ArrayList; +import java.util.Collection; +import tc.oc.pgm.util.attribute.Attribute; +import tc.oc.pgm.util.attribute.AttributeInstance; +import tc.oc.pgm.util.attribute.AttributeModifier; + +/** Attribute Instance for 1.9+ */ +public class AttributeInstanceBukkit implements AttributeInstance { + + private final org.bukkit.attribute.AttributeInstance bukkitAttributeInstance; + + public AttributeInstanceBukkit(org.bukkit.attribute.AttributeInstance bukkitAttributeInstance) { + this.bukkitAttributeInstance = bukkitAttributeInstance; + } + + @Override + public Attribute getAttribute() { + return AttributeUtilBukkit.convertAttribute(bukkitAttributeInstance.getAttribute()); + } + + @Override + public double getBaseValue() { + return bukkitAttributeInstance.getBaseValue(); + } + + @Override + public void setBaseValue(double d) { + bukkitAttributeInstance.setBaseValue(d); + } + + @Override + public Collection getModifiers() { + Collection convertedModifiers = new ArrayList<>(); + + for (org.bukkit.attribute.AttributeModifier modifier : bukkitAttributeInstance.getModifiers()) { + + AttributeModifier attributeModifier = AttributeUtilBukkit.convertAttributeModifier(modifier); + + convertedModifiers.add(attributeModifier); + } + + return convertedModifiers; + } + + @Override + public void addModifier(AttributeModifier modifier) { + bukkitAttributeInstance.addModifier(AttributeUtilBukkit.convertAttributeModifier(modifier)); + } + + @Override + public void removeModifier(AttributeModifier modifier) { + bukkitAttributeInstance.removeModifier(AttributeUtilBukkit.convertAttributeModifier(modifier)); + } + + @Override + public double getValue() { + return bukkitAttributeInstance.getValue(); + } + + @Override + public double getDefaultValue() { + return bukkitAttributeInstance.getDefaultValue(); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMapBukkit.java b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMapBukkit.java new file mode 100644 index 0000000000..64e3ecfde0 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeMapBukkit.java @@ -0,0 +1,23 @@ +package tc.oc.pgm.util.nms.attribute; + +import org.bukkit.entity.Player; +import tc.oc.pgm.util.attribute.Attribute; +import tc.oc.pgm.util.attribute.AttributeInstance; +import tc.oc.pgm.util.attribute.AttributeMap; + +public class AttributeMapBukkit implements AttributeMap { + private Player player; + + public AttributeMapBukkit(Player player) { + this.player = player; + } + + @Override + public AttributeInstance getAttribute(Attribute attribute) { + org.bukkit.attribute.Attribute bukkitAttribute = + AttributeUtilBukkit.convertAttribute(attribute); + if (bukkitAttribute == null) return null; + org.bukkit.attribute.AttributeInstance attributeInstance = player.getAttribute(bukkitAttribute); + return attributeInstance == null ? null : new AttributeInstanceBukkit(attributeInstance); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeUtilBukkit.java b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeUtilBukkit.java new file mode 100644 index 0000000000..169a7b41ef --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/attribute/AttributeUtilBukkit.java @@ -0,0 +1,76 @@ +package tc.oc.pgm.util.nms.attribute; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import tc.oc.pgm.util.attribute.Attribute; +import tc.oc.pgm.util.attribute.AttributeModifier; + +/** Util to improve performance converting attributes between bukkit and PGM */ +public interface AttributeUtilBukkit { + + Map attributeCacheToBukkit = + new EnumMap<>(Attribute.class); + Map attributeCacheToPGM = + new EnumMap<>(org.bukkit.attribute.Attribute.class); + + EnumSet missingAttributes = findMissingAttributes(); + + static EnumSet findMissingAttributes() { + Set missingAttributes = new HashSet<>(); + + for (Attribute attribute : Attribute.values()) { + try { + org.bukkit.attribute.Attribute.valueOf(attribute.name()); + } catch (Exception e) { + missingAttributes.add(attribute); + } + } + + return EnumSet.copyOf(missingAttributes); + } + + static org.bukkit.attribute.Attribute convertAttribute(Attribute attribute) { + if (missingAttributes.contains(attribute)) return null; + org.bukkit.attribute.Attribute cachedAttribute = attributeCacheToBukkit.get(attribute); + if (cachedAttribute == null) { + cachedAttribute = org.bukkit.attribute.Attribute.valueOf(attribute.name()); + attributeCacheToBukkit.put(attribute, cachedAttribute); + } + return cachedAttribute; + } + + static Attribute convertAttribute(org.bukkit.attribute.Attribute attribute) { + Attribute cachedAttribute = attributeCacheToPGM.get(attribute); + if (cachedAttribute == null) { + cachedAttribute = Attribute.valueOf(attribute.name()); + attributeCacheToPGM.put(attribute, cachedAttribute); + } + return cachedAttribute; + } + + @NotNull + static AttributeModifier convertAttributeModifier( + org.bukkit.attribute.AttributeModifier modifier) { + return new AttributeModifier( + modifier.getUniqueId(), + modifier.getName(), + modifier.getAmount(), + AttributeModifier.Operation.values()[modifier.getOperation().ordinal()]); + } + + @NotNull + static org.bukkit.attribute.AttributeModifier convertAttributeModifier( + AttributeModifier modifier) { + + return new org.bukkit.attribute.AttributeModifier( + modifier.getUniqueId(), + modifier.getName(), + modifier.getAmount(), + org.bukkit.attribute.AttributeModifier.Operation.values()[ + modifier.getOperation().ordinal()]); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntity.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntity.java index 1cebe9ea13..27228aacc2 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntity.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntity.java @@ -9,8 +9,6 @@ public interface FakeEntity { int entityId(); - Entity entity(); - void spawn(Player viewer, Location location, org.bukkit.util.Vector velocity); default void spawn(Player viewer, Location location) { @@ -26,10 +24,9 @@ default void teleport(Player viewer, Location location) { NMSHacks.sendPacket(viewer, NMSHacks.teleportEntityPacket(entityId(), location)); } - default void ride(Player viewer, Entity rider) { - int entityID = rider.getEntityId(); + default void ride(Player viewer, int riderID) { int vehicleID = entityId(); - NMSHacks.entityAttach(viewer, entityID, vehicleID, false); + NMSHacks.entityAttach(viewer, riderID, vehicleID, false); } default void mount(Player viewer, Entity vehicle) { diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityImpl1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityImpl1_8.java index 080655afab..1f8e818a64 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityImpl1_8.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityImpl1_8.java @@ -1,7 +1,6 @@ package tc.oc.pgm.util.nms.entity.fake; import org.bukkit.Location; -import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.util.Vector; import tc.oc.pgm.util.nms.NMSHacks; @@ -14,11 +13,6 @@ protected FakeEntityImpl1_8(T entity) { this.entity = entity; } - @Override - public Entity entity() { - return entity.getBukkitEntity(); - } - @Override public void spawn(Player viewer, Location location, Vector velocity) { entity.setPositionRotation( diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityNoOp.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityNoOp.java index dfce6f7d8f..ef033de924 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityNoOp.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityNoOp.java @@ -1,7 +1,6 @@ package tc.oc.pgm.util.nms.entity.fake; import org.bukkit.Location; -import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.util.Vector; @@ -12,11 +11,6 @@ public int entityId() { return 0; } - @Override - public Entity entity() { - return null; - } - @Override public void spawn(Player viewer, Location location, Vector velocity) {} } diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityProtocolLib.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityProtocolLib.java new file mode 100644 index 0000000000..7fdcf17291 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/FakeEntityProtocolLib.java @@ -0,0 +1,12 @@ +package tc.oc.pgm.util.nms.entity.fake; + +import tc.oc.pgm.util.nms.NMSHacks; + +public abstract class FakeEntityProtocolLib implements FakeEntity { + private final int entityID = NMSHacks.allocateEntityId(); + + @Override + public int entityId() { + return entityID; + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/armorstand/FakeArmorStandProtocolLib.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/armorstand/FakeArmorStandProtocolLib.java new file mode 100644 index 0000000000..eafc30c7b0 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/armorstand/FakeArmorStandProtocolLib.java @@ -0,0 +1,23 @@ +package tc.oc.pgm.util.nms.entity.fake.armorstand; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.util.Vector; +import tc.oc.pgm.util.nms.NMSHacks; +import tc.oc.pgm.util.nms.entity.fake.FakeEntityProtocolLib; + +public class FakeArmorStandProtocolLib extends FakeEntityProtocolLib { + + ItemStack head; + + public FakeArmorStandProtocolLib(ItemStack head) { + this.head = head; + } + + @Override + public void spawn(Player viewer, Location location, Vector velocity) { + NMSHacks.spawnFakeArmorStand(viewer.getPlayer(), entityId(), location, velocity); + wear(viewer, 4, head); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/wither/FakeWitherSkullProtocolLib.java b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/wither/FakeWitherSkullProtocolLib.java new file mode 100644 index 0000000000..8be4d34149 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/entity/fake/wither/FakeWitherSkullProtocolLib.java @@ -0,0 +1,15 @@ +package tc.oc.pgm.util.nms.entity.fake.wither; + +import org.bukkit.Location; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; +import tc.oc.pgm.util.nms.NMSHacks; +import tc.oc.pgm.util.nms.entity.fake.FakeEntityProtocolLib; + +public class FakeWitherSkullProtocolLib extends FakeEntityProtocolLib { + + @Override + public void spawn(Player viewer, Location location, Vector velocity) { + NMSHacks.sendSpawnEntityPacket(viewer.getPlayer(), entityId(), location, velocity); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/reflect/Refl.java b/util/src/main/java/tc/oc/pgm/util/nms/reflect/Refl.java new file mode 100644 index 0000000000..90f4c9b388 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/reflect/Refl.java @@ -0,0 +1,165 @@ +package tc.oc.pgm.util.nms.reflect; + +import java.util.Map; +import java.util.concurrent.Callable; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.inventory.meta.SkullMeta; + +public interface Refl { + + @Reflect.CB("inventory.CraftMetaItem.unhandledTags") + Map getUnhandledTags(ItemMeta item); + + @Reflect.CB("inventory.CraftMetaSkull.profile") + void setSkullProfile(SkullMeta meta, Object profile); + + @Reflect.CB("entity.CraftPlayer.getHandle()") + Object getPlayerHandle(Player player); + + @Reflect.NMS("EntityPlayer.ping") + int getPlayerPing(Object handle); + + @Reflect.CB("CraftWorld.getHandle()") + Object getWorldHandle(World world); + + @Reflect.NMS("World.getTime()") + long getWorldTime(Object handle); + + @Reflect.CB("CraftServer.getHandle()") + Object getCraftServerHandle(Server server); + + @Reflect.CB("entity.CraftLivingEntity.getHandle()") + Object getCraftEntityHandle(LivingEntity entity); + + @Reflect.NMS("EntityLiving.getAbsorptionHearts()") + float getAbsorptionHearts(Object entity); + + @Reflect.NMS(value = "EntityLiving.setAbsorptionHearts()", parameters = float.class) + void setAbsorptionHearts(Object entity, float hearts); + + @Reflect.NMS("DedicatedPlayerList.getServer()") + Object getNMSServer(Object handle); + + @Reflect.NMS(value = "MinecraftServer.a()", parameters = Callable.class) + void addCallableToMainThread(Object nmsServer, Callable callable); + + @Reflect.NMS(value = "Item.canDestroySpecialBlock()", parameters = IBlockData.class) + boolean canDestroySpecialBlock(Object nmsItem, Object blockData); + + @Reflect.NMS("Material.isAlwaysDestroyable") + boolean isAlwaysDestroyable(Object material); + + @Reflect.CB("inventory.CraftItemStack") + interface CraftItemStack { + @Reflect.StaticMethod(value = "asCraftCopy", parameters = ItemStack.class) + ItemStack asCraftCopy(ItemStack itemStack); + } + + @Reflect.NMS("NBTBase") + interface NBTBase {} + + @Reflect.NMS("NBTTagString") + interface NBTTagString { + + @Reflect.Constructor(String.class) + Object build(String string); + + @Reflect.Method("a_") + String getString(Object item); + }; + + @Reflect.NMS("NBTTagList") + interface NBTTagList { + + @Reflect.Field("list") + Object getListField(Object self); + + @Reflect.Constructor + Object build(); + + @Reflect.Method("size") + int size(Object self); + + @Reflect.Method(value = "get", parameters = int.class) + Object get(Object self, int index); + + @Reflect.Method(value = "add", parameters = NBTBase.class) + void add(Object self, Object value); + + @Reflect.Method("isEmpty") + boolean isEmpty(Object self); + } + + @Reflect.NMS("NBTTagCompound") + interface NBTTagCompound { + + @Reflect.Constructor + Object build(); + + @Reflect.Method(value = "getString", parameters = String.class) + String getString(Object self, String parameter); + + @Reflect.Method(value = "getLong", parameters = String.class) + long getLong(Object self, String parameter); + + @Reflect.Method(value = "getDouble", parameters = String.class) + double getDouble(Object self, String parameter); + + @Reflect.Method(value = "getInt", parameters = String.class) + int getInt(Object self, String parameter); + + @Reflect.Method( + value = "setString", + parameters = {String.class, String.class}) + void setString(Object self, String parameter, String value); + + @Reflect.Method( + value = "setLong", + parameters = {String.class, long.class}) + void setLong(Object self, String parameter, long value); + + @Reflect.Method( + value = "setDouble", + parameters = {String.class, double.class}) + void setDouble(Object self, String parameter, double value); + + @Reflect.Method( + value = "setInt", + parameters = {String.class, int.class}) + void setInt(Object self, String parameter, int value); + } + + @Reflect.CB("util.CraftMagicNumbers") + interface CraftMagicNumbers { + @Reflect.StaticMethod(value = "getItem", parameters = Material.class) + Object getItem(Material material); + + @Reflect.StaticMethod(value = "getBlock", parameters = Material.class) + Object getBlock(Material material); + } + + @Reflect.NMS("Block") + interface Block { + + @Reflect.StaticMethod(value = "getByName", parameters = String.class) + Object getBlockByName(String name); + + @Reflect.StaticMethod(value = "getId", parameters = Block.class) + int getId(Object self); + + @Reflect.Method("getBlockData") + Object getBlockData(Object self); + + @Reflect.Method("q") + Object getMaterial(Object self); + } + + @Reflect.NMS("IBlockData") + interface IBlockData {} +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/reflect/Reflect.java b/util/src/main/java/tc/oc/pgm/util/nms/reflect/Reflect.java new file mode 100644 index 0000000000..459ff3bcf1 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/reflect/Reflect.java @@ -0,0 +1,52 @@ +package tc.oc.pgm.util.nms.reflect; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Reflect { + @Retention(RetentionPolicy.RUNTIME) + @interface NMS { + String value(); + + Class[] parameters() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface CB { + String value(); + + Class[] parameters() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface B { + String value(); + + Class[] parameters() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface StaticMethod { + String value(); + + Class[] parameters() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Method { + String value(); + + Class[] parameters() default {}; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Field { + String value(); + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Constructor { + Class[] value() default {}; + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/reflect/ReflectionProxy.java b/util/src/main/java/tc/oc/pgm/util/nms/reflect/ReflectionProxy.java new file mode 100644 index 0000000000..00b96721e8 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/reflect/ReflectionProxy.java @@ -0,0 +1,352 @@ +package tc.oc.pgm.util.nms.reflect; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.util.reflect.MinecraftReflectionUtils; +import tc.oc.pgm.util.reflect.ReflectionUtils; + +public class ReflectionProxy implements InvocationHandler { + + static Map> functionMap = new ConcurrentHashMap<>(); + + static ReflectionProxy INSTANCE = new ReflectionProxy(); + + public static T getProxy(Class classType) { + return (T) + Proxy.newProxyInstance( + ReflectionProxy.class.getClassLoader(), new Class[] {classType}, INSTANCE); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return functionMap + .computeIfAbsent( + method, + (input) -> { + Class declaringClass = method.getDeclaringClass(); + Class parentClass = getAnnotatedClass(declaringClass); + if (parentClass == null) { + return processComponent(method, args); + } else { + return processComponent(method, args, parentClass); + } + }) + .apply(args); + } + + @Nullable + private static Function processComponent(Method method, Object[] args) { + if (method.isAnnotationPresent(Reflect.NMS.class)) { + return processNMSComponent(method, args); + } else if (method.isAnnotationPresent(Reflect.CB.class)) { + return processCBComponent(method, args); + } else if (method.isAnnotationPresent(Reflect.B.class)) { + return processBukkitComponent(method, args); + } + throw new RuntimeException("Error processing Method: " + method); + } + + @Nullable + private static Function processComponent( + Method method, Object[] args, Class parentClass) { + if (method.isAnnotationPresent(Reflect.StaticMethod.class)) { + return processStaticMethod(method, parentClass); + } else if (method.isAnnotationPresent(Reflect.Method.class)) { + return processMethod(method, args, parentClass); + } else if (method.isAnnotationPresent(Reflect.Field.class)) { + return processField(method, args, parentClass); + } else if (method.isAnnotationPresent(Reflect.Constructor.class)) { + return processConstructor(method, parentClass); + } + throw new RuntimeException("Error processing method: " + method); + } + + private static Function processStaticMethod( + Method method, Class parentClass) { + for (Reflect.StaticMethod staticMethod : + method.getDeclaredAnnotationsByType(Reflect.StaticMethod.class)) { + try { + Method reflectedMethod = + parentClass.getMethod( + staticMethod.value(), processParameters(staticMethod.parameters())); + return (input) -> ReflectionUtils.callMethod(reflectedMethod, null, input); + } catch (NoSuchMethodException e) { + } + } + throw new RuntimeException("Method not found for " + method); + } + + private static Function processMethod( + Method method, Object[] args, Class parentClass) { + for (Reflect.Method staticMethod : method.getDeclaredAnnotationsByType(Reflect.Method.class)) { + try { + Method reflectedMethod = + parentClass.getMethod( + staticMethod.value(), processParameters(staticMethod.parameters())); + return callMethod(args, reflectedMethod); + } catch (NoSuchMethodException ignored) { + } + } + throw new RuntimeException("Method not found for " + method); + } + + @Nullable + private static Function processField( + Method method, Object[] args, Class parentClass) { + for (Reflect.Field field : method.getDeclaredAnnotationsByType(Reflect.Field.class)) { + try { + Field reflectedField = parentClass.getDeclaredField(field.value()); + reflectedField.setAccessible(true); + return performActionOnField(args, reflectedField); + } catch (NoSuchFieldException ignored) { + } + } + throw new RuntimeException("Field not found for " + method); + } + + @NotNull + private static Function processConstructor( + Method method, Class parentClass) { + for (Reflect.Constructor constructor : + method.getDeclaredAnnotationsByType(Reflect.Constructor.class)) { + try { + Constructor reflectedConstructor = + parentClass.getConstructor(processParameters(constructor.value())); + return (input) -> ReflectionUtils.callConstructor(reflectedConstructor, input); + } catch (NoSuchMethodException ignored) { + } + } + throw new RuntimeException("Constructor not found for " + method); + } + + @Nullable + private static Function processBukkitComponent(Method method, Object[] args) { + for (Reflect.B bukkit : method.getDeclaredAnnotationsByType(Reflect.B.class)) { + String methodPath = bukkit.value(); + Class[] parameters = bukkit.parameters(); + + String[] split = splitClassAndMethod(methodPath); + + String className = split[0]; + String methodName = split[1]; + + Class parentClass; + try { + parentClass = MinecraftReflectionUtils.getBukkitClass(className); + } catch (RuntimeException ignored) { + continue; + } + + if (methodName.endsWith("()")) { + Method reflectedMethod = parseStandaloneMethod(parentClass, methodName, parameters); + if (reflectedMethod != null) { + return callMethod(args, reflectedMethod); + } + } else { + Field reflectedField = parseStandaloneField(parentClass, methodName); + if (reflectedField != null) { + return performActionOnField(args, reflectedField); + } + } + } + throw new RuntimeException("Error processing Method: " + method); + } + + @Nullable + private static Function processCBComponent(Method method, Object[] args) { + for (Reflect.CB cb : method.getDeclaredAnnotationsByType(Reflect.CB.class)) { + String methodPath = cb.value(); + Class[] parameters = cb.parameters(); + + String[] split = splitClassAndMethod(methodPath); + + String className = split[0]; + String methodName = split[1]; + + Class parentClass; + try { + parentClass = MinecraftReflectionUtils.getCraftBukkitClass(className); + } catch (RuntimeException ignored) { + continue; + } + + if (methodName.endsWith("()")) { + Method reflectedMethod = parseStandaloneMethod(parentClass, methodName, parameters); + if (reflectedMethod != null) { + return callMethod(args, reflectedMethod); + } + } else { + Field reflectedField = parseStandaloneField(parentClass, methodName); + if (reflectedField != null) { + return performActionOnField(args, reflectedField); + } + } + } + throw new RuntimeException("Error processing Method: " + method); + } + + @Nullable + private static Function processNMSComponent(Method method, Object[] args) { + for (Reflect.NMS nms : method.getDeclaredAnnotationsByType(Reflect.NMS.class)) { + String methodPath = nms.value(); + Class[] parameters = nms.parameters(); + + String[] split = splitClassAndMethod(methodPath); + + String className = split[0]; + String methodName = split[1]; + + Class parentClass; + parentClass = findNMSClass(className); + if (parentClass == null) continue; + + if (methodName.endsWith("()")) { + Method reflectedMethod = parseStandaloneMethod(parentClass, methodName, parameters); + if (reflectedMethod != null) { + return callMethod(args, reflectedMethod); + } + } else { + Field reflectedField = parseStandaloneField(parentClass, methodName); + if (reflectedField != null) { + return performActionOnField(args, reflectedField); + } + } + } + throw new RuntimeException("Error processing Method: " + method); + } + + @Nullable + private static Function performActionOnField( + Object[] args, Field reflectedField) { + if (args.length == 0) { + throw new UnsupportedOperationException("Annotated fields must have more than 0 parameters!"); + } else if (args.length == 1) { + return (input) -> ReflectionUtils.readField(input[0], reflectedField); + } else if (args.length == 2) { + return (input) -> { + ReflectionUtils.setField(input[0], input[1], reflectedField); + return null; + }; + } else { + throw new UnsupportedOperationException("Annotated fields must have less than 3 parameters!"); + } + } + + private static Function callMethod(Object[] args, Method reflectedMethod) { + if (args.length > 1) { + return (input) -> + ReflectionUtils.callMethod( + reflectedMethod, input[0], Arrays.copyOfRange(input, 1, input.length)); + } else { + return (input) -> ReflectionUtils.callMethod(reflectedMethod, input[0]); + } + } + + @Nullable + private static Field parseStandaloneField(Class parentClass, String methodName) { + Field reflectedField; + try { + reflectedField = parentClass.getDeclaredField(methodName); + reflectedField.setAccessible(true); + } catch (NoSuchFieldException e) { + return null; + } + return reflectedField; + } + + @Nullable + private static Method parseStandaloneMethod( + Class parentClass, String methodName, Class[] parameters) { + Method reflectedMethod; + methodName = methodName.substring(0, methodName.length() - 2); + try { + if (parameters.length > 0) { + reflectedMethod = parentClass.getMethod(methodName, processParameters(parameters)); + } else { + reflectedMethod = parentClass.getMethod(methodName); + } + reflectedMethod.setAccessible(true); + } catch (NoSuchMethodException e) { + return null; + } + return reflectedMethod; + } + + private static Class[] processParameters(Class[] givenParameters) { + Class[] parameters = new Class[givenParameters.length]; + + for (int i = 0; i < givenParameters.length; i++) { + try { + Class annotatedClass = getAnnotatedClass(givenParameters[i]); + if (annotatedClass != null) { + parameters[i] = annotatedClass; + break; + } + } catch (RuntimeException ignored) { + } + parameters[i] = givenParameters[i]; + } + + return parameters; + } + + @NotNull + private static String[] splitClassAndMethod(String fieldMethodPath) { + String[] split = new String[2]; + int index = fieldMethodPath.lastIndexOf("."); + split[0] = fieldMethodPath.substring(0, index); + split[1] = fieldMethodPath.substring(index + 1); + return split; + } + + private static Class getAnnotatedClass(Class declaringClass) { + if (declaringClass.isAnnotationPresent(Reflect.NMS.class)) { + for (Reflect.NMS nms : declaringClass.getDeclaredAnnotationsByType(Reflect.NMS.class)) { + Class parentClass = findNMSClass(nms.value()); + if (parentClass != null) { + return parentClass; + } + } + throw new RuntimeException("Class not found for " + declaringClass); + } else if (declaringClass.isAnnotationPresent(Reflect.CB.class)) { + for (Reflect.CB cb : declaringClass.getDeclaredAnnotationsByType(Reflect.CB.class)) { + try { + return MinecraftReflectionUtils.getCraftBukkitClass(cb.value()); + } catch (RuntimeException ignored) { + } + } + throw new RuntimeException("Class not found for " + declaringClass); + } else if (declaringClass.isAnnotationPresent(Reflect.B.class)) { + for (Reflect.B bukkit : declaringClass.getDeclaredAnnotationsByType(Reflect.B.class)) { + try { + return MinecraftReflectionUtils.getBukkitClass(bukkit.value()); + } catch (RuntimeException ignored) { + } + } + throw new RuntimeException("Class not found for " + declaringClass); + } + return null; + } + + private static Class findNMSClass(String className) { + Class parentClass = null; + try { + parentClass = MinecraftReflectionUtils.getNMSClassLegacy(className); + } catch (RuntimeException ignored) { + } + try { + parentClass = MinecraftReflectionUtils.getNMSClassNew(className); + } catch (RuntimeException ignored) { + } + return parentClass; + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks1_8.java b/util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacks1_8.java similarity index 92% rename from util/src/main/java/tc/oc/pgm/util/nms/NMSHacks1_8.java rename to util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacks1_8.java index b9e3886b75..657681fc29 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacks1_8.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacks1_8.java @@ -1,9 +1,8 @@ -package tc.oc.pgm.util.nms; +package tc.oc.pgm.util.nms.v1_8; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.SetMultimap; -import com.google.common.util.concurrent.ListenableFutureTask; import com.mojang.authlib.GameProfile; import com.mojang.authlib.properties.Property; import java.lang.reflect.Constructor; @@ -109,17 +108,23 @@ import tc.oc.pgm.util.block.RayBlockIntersection; import tc.oc.pgm.util.bukkit.BukkitUtils; import tc.oc.pgm.util.bukkit.ViaUtils; +import tc.oc.pgm.util.nms.EnumPlayerInfoAction; +import tc.oc.pgm.util.nms.NMSHacksNoOp; import tc.oc.pgm.util.nms.attribute.AttributeMap1_8; import tc.oc.pgm.util.nms.entity.fake.FakeEntity; import tc.oc.pgm.util.nms.entity.fake.armorstand.FakeArmorStand1_8; import tc.oc.pgm.util.nms.entity.fake.wither.FakeWitherSkull1_8; import tc.oc.pgm.util.nms.entity.potion.EntityPotion; import tc.oc.pgm.util.nms.entity.potion.EntityPotion1_8; +import tc.oc.pgm.util.reflect.MinecraftReflectionUtils; import tc.oc.pgm.util.reflect.ReflectionUtils; import tc.oc.pgm.util.skin.Skin; import tc.oc.pgm.util.skin.Skins; -public class NMSHacks1_8 extends NMSHacksNoOp { +class NMSHacks1_8 extends NMSHacksNoOp { + + public NMSHacks1_8() {} + @Override public void sendPacket(Player bukkitPlayer, Object packet) { if (bukkitPlayer.isOnline()) { @@ -654,11 +659,6 @@ public Skin getPlayerSkin(Player player) { return Skins.fromProperties(craftPlayer.getProfile().getProperties()); } - @Override - public Skin getPlayerSkinForViewer(Player player, Player viewer) { - return getPlayerSkin(player); - } - @Override public void updateVelocity(Player player) { EntityPlayer handle = ((CraftPlayer) player).getHandle(); @@ -743,7 +743,8 @@ public Field getField() { } @Override - public void sendSpawnEntityPacket(Player player, int entityId, Location location) { + public void sendSpawnEntityPacket( + Player player, int entityId, Location location, Vector velocity) { PacketPlayOutSpawnEntity packet = new PacketPlayOutSpawnEntity(); ReflectionUtils.setField(packet, entityId, EntitySpawnFields.a.getField()); @@ -779,42 +780,47 @@ public void spawnFreezeEntity(Player player, int entityId, boolean legacy) { } else { Location loc = player.getLocation().subtract(0, 1.1, 0); - DataWatcher dataWatcher = new DataWatcher(null); - int flags = 0; - flags |= 0x20; - dataWatcher.a(0, (byte) flags); - dataWatcher.a(1, (short) 0); - int flags1 = 0; - dataWatcher.a(10, (byte) flags1); - PacketPlayOutSpawnEntityLiving packet = new PacketPlayOutSpawnEntityLiving(); - - ReflectionUtils.setField(packet, entityId, LivingEntitySpawnFields.a.getField()); - ReflectionUtils.setField( - packet, (byte) EntityType.ARMOR_STAND.getTypeId(), LivingEntitySpawnFields.b.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(loc.getX() * 32.0D), LivingEntitySpawnFields.c.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(loc.getY() * 32.0D), LivingEntitySpawnFields.d.getField()); - ReflectionUtils.setField( - packet, MathHelper.floor(loc.getZ() * 32.0D), LivingEntitySpawnFields.e.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) loc.getYaw()) * 256.0F / 360.0F)), - LivingEntitySpawnFields.i.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) loc.getPitch()) * 256.0F / 360.0F)), - LivingEntitySpawnFields.j.getField()); - ReflectionUtils.setField( - packet, - (byte) ((int) (((byte) loc.getPitch()) * 256.0F / 360.0F)), - LivingEntitySpawnFields.k.getField()); - ReflectionUtils.setField(packet, dataWatcher, LivingEntitySpawnFields.l.getField()); - - sendPacket(player, packet); + spawnFakeArmorStand(player, entityId, loc, new Vector()); } } + @Override + public void spawnFakeArmorStand(Player player, int entityId, Location location, Vector velocity) { + DataWatcher dataWatcher = new DataWatcher(null); + int flags = 0; + flags |= 0x20; + dataWatcher.a(0, (byte) flags); + dataWatcher.a(1, (short) 0); + int flags1 = 0; + dataWatcher.a(10, (byte) flags1); + PacketPlayOutSpawnEntityLiving packet = new PacketPlayOutSpawnEntityLiving(); + + ReflectionUtils.setField(packet, entityId, LivingEntitySpawnFields.a.getField()); + ReflectionUtils.setField( + packet, (byte) EntityType.ARMOR_STAND.getTypeId(), LivingEntitySpawnFields.b.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(location.getX() * 32.0D), LivingEntitySpawnFields.c.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(location.getY() * 32.0D), LivingEntitySpawnFields.d.getField()); + ReflectionUtils.setField( + packet, MathHelper.floor(location.getZ() * 32.0D), LivingEntitySpawnFields.e.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) location.getYaw()) * 256.0F / 360.0F)), + LivingEntitySpawnFields.i.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) location.getPitch()) * 256.0F / 360.0F)), + LivingEntitySpawnFields.j.getField()); + ReflectionUtils.setField( + packet, + (byte) ((int) (((byte) location.getPitch()) * 256.0F / 360.0F)), + LivingEntitySpawnFields.k.getField()); + ReflectionUtils.setField(packet, dataWatcher, LivingEntitySpawnFields.l.getField()); + + sendPacket(player, packet); + } + @Override public boolean canMineBlock(MaterialData blockMaterial, ItemStack tool) { if (!blockMaterial.getItemType().isBlock()) { @@ -848,7 +854,7 @@ public void resetDimension(World world) { Field unhandledTagsField = ReflectionUtils.getField( - "org.bukkit.craftbukkit.v1_8_R3.inventory.CraftMetaItem", "unhandledTags"); + MinecraftReflectionUtils.getCraftBukkitClass("inventory.CraftMetaItem"), "unhandledTags"); static Field nbtListField = ReflectionUtils.getField(NBTTagList.class, "list"); @Override @@ -911,14 +917,6 @@ public Set getCanPlaceOn(ItemMeta itemMeta) { return getMaterialCollection(itemMeta, "CanPlaceOn"); } - @Override - public void copyAttributeModifiers(ItemMeta destination, ItemMeta source) { - SetMultimap attributeModifiers = - getAttributeModifiers(source); - attributeModifiers.putAll(getAttributeModifiers(destination)); - applyAttributeModifiers(attributeModifiers, destination); - } - @Override public void applyAttributeModifiers( SetMultimap attributeModifiers, ItemMeta meta) { @@ -1048,24 +1046,10 @@ public AttributeMap buildAttributeMap(Player player) { return new AttributeMap1_8(player); } - public ListenableFutureTask constructFutureTask(Runnable task) { - final ListenableFutureTask future = ListenableFutureTask.create(task, null); - return future; - } - - static Field TASK_QUEUE = ReflectionUtils.getField(MinecraftServer.class, "j"); - @Override public void postToMainThread(Plugin plugin, boolean priority, Runnable task) { MinecraftServer server = ((CraftServer) plugin.getServer()).getHandle().getServer(); server.a(Executors.callable(task)); - - // ListenableFutureTask futureTask = constructFutureTask(task); - // Queue> taskQueue = (Queue>) ReflectionUtils.readField(server, - // TASK_QUEUE); - // synchronized (taskQueue) { - // taskQueue.add(futureTask); - // } } @NotNull diff --git a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksSportPaper.java b/util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacksSportPaper.java similarity index 86% rename from util/src/main/java/tc/oc/pgm/util/nms/NMSHacksSportPaper.java rename to util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacksSportPaper.java index 0869a7bb58..1221040eee 100644 --- a/util/src/main/java/tc/oc/pgm/util/nms/NMSHacksSportPaper.java +++ b/util/src/main/java/tc/oc/pgm/util/nms/v1_8/NMSHacksSportPaper.java @@ -1,4 +1,4 @@ -package tc.oc.pgm.util.nms; +package tc.oc.pgm.util.nms.v1_8; import com.google.common.collect.SetMultimap; import java.util.Collection; @@ -27,7 +27,9 @@ import org.bukkit.material.MaterialData; import org.bukkit.plugin.Plugin; import org.bukkit.scoreboard.NameTagVisibility; +import org.bukkit.util.Vector; import tc.oc.pgm.util.attribute.AttributeModifier; +import tc.oc.pgm.util.nms.EnumPlayerInfoAction; import tc.oc.pgm.util.skin.Skin; public class NMSHacksSportPaper extends NMSHacks1_8 { @@ -118,7 +120,8 @@ public void addPlayerInfoToPacket(Object packet, Object playerInfoData) { } @Override - public void sendSpawnEntityPacket(Player player, int entityId, Location location) { + public void sendSpawnEntityPacket( + Player player, int entityId, Location location, Vector velocity) { sendPacket( player, new PacketPlayOutSpawnEntity( @@ -126,9 +129,9 @@ public void sendSpawnEntityPacket(Player player, int entityId, Location location location.getX(), location.getY(), location.getZ(), - 0, - 0, - 0, + (int) (velocity.getX() * 8000), + (int) (velocity.getY() * 8000), + (int) (velocity.getZ() * 8000), (int) location.getPitch(), (int) location.getYaw(), 66, @@ -147,31 +150,34 @@ public void spawnFreezeEntity(Player player, int entityId, boolean legacy) { sendSpawnEntityPacket(player, entityId, location); } else { Location loc = player.getLocation().subtract(0, 1.1, 0); - int flags = 0; - flags |= 0x20; - DataWatcher dataWatcher = new DataWatcher(null); - dataWatcher.a(0, (byte) (byte) flags); - dataWatcher.a(1, (short) (short) 0); - int flags1 = 0; - dataWatcher.a(10, (byte) flags1); - sendPacket( - player, - new PacketPlayOutSpawnEntityLiving( - entityId, - (byte) EntityType.ARMOR_STAND.getTypeId(), - loc.getX(), - loc.getY(), - loc.getZ(), - loc.getYaw(), - loc.getPitch(), - loc.getPitch(), - 0, - 0, - 0, - dataWatcher)); + Vector velocity = new Vector(); + spawnFakeArmorStand(player, entityId, loc, velocity); } } + @Override + public void spawnFakeArmorStand(Player player, int entityId, Location loc, Vector velocity) { + DataWatcher dataWatcher = new DataWatcher(null); + dataWatcher.a(0, (byte) 0x20); + dataWatcher.a(1, (short) 0); + dataWatcher.a(10, (byte) 0); + sendPacket( + player, + new PacketPlayOutSpawnEntityLiving( + entityId, + (byte) EntityType.ARMOR_STAND.getTypeId(), + loc.getX(), + loc.getY(), + loc.getZ(), + loc.getYaw(), + loc.getPitch(), + loc.getPitch(), + (int) (velocity.getX() * 8000), + (int) (velocity.getY() * 8000), + (int) (velocity.getZ() * 8000), + dataWatcher)); + } + @Override public Skin getPlayerSkinForViewer(Player player, Player viewer) { return player.hasFakeSkin(viewer) diff --git a/util/src/main/java/tc/oc/pgm/util/nms/v1_9/NMSHacks1_9.java b/util/src/main/java/tc/oc/pgm/util/nms/v1_9/NMSHacks1_9.java new file mode 100644 index 0000000000..1143fb92d6 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/v1_9/NMSHacks1_9.java @@ -0,0 +1,154 @@ +package tc.oc.pgm.util.nms.v1_9; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.SetMultimap; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Executors; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.material.MaterialData; +import org.bukkit.plugin.Plugin; +import tc.oc.pgm.util.attribute.AttributeMap; +import tc.oc.pgm.util.attribute.AttributeModifier; +import tc.oc.pgm.util.nms.attribute.AttributeMapBukkit; +import tc.oc.pgm.util.nms.reflect.Refl; +import tc.oc.pgm.util.nms.reflect.ReflectionProxy; + +public class NMSHacks1_9 extends NMSHacksProtocolLib { + static Refl refl = ReflectionProxy.getProxy(Refl.class); + static Refl.NBTTagString nbtTagString = ReflectionProxy.getProxy(Refl.NBTTagString.class); + static Refl.NBTTagList nbtTagList = ReflectionProxy.getProxy(Refl.NBTTagList.class); + static Refl.NBTTagCompound nbtTagCompound = ReflectionProxy.getProxy(Refl.NBTTagCompound.class); + static Refl.CraftMagicNumbers craftMagicNumbers = + ReflectionProxy.getProxy(Refl.CraftMagicNumbers.class); + static Refl.Block reflBlock = ReflectionProxy.getProxy(Refl.Block.class); + static Refl.CraftItemStack craftItemStack = ReflectionProxy.getProxy(Refl.CraftItemStack.class); + + @Override + public Set getMaterialCollection(ItemMeta itemMeta, String key) { + Map unhandledTags = refl.getUnhandledTags(itemMeta); + if (!unhandledTags.containsKey(key)) return EnumSet.noneOf(Material.class); + EnumSet materialSet = EnumSet.noneOf(Material.class); + Object canDestroyList = unhandledTags.get(key); + + for (Object item : (List) nbtTagList.getListField(canDestroyList)) { + String blockString = nbtTagString.getString(item); + Object nmsBlock = reflBlock.getBlockByName(blockString); + int id = reflBlock.getId(nmsBlock); + Material material = Material.getMaterial(id); + materialSet.add(material); + } + + return materialSet; + } + + @Override + public void setMaterialCollection( + ItemMeta itemMeta, Collection materials, String collectionName) { + Map unhandledTags = refl.getUnhandledTags(itemMeta); + Object canDestroyList = + unhandledTags.containsKey(collectionName) + ? unhandledTags.get(collectionName) + : nbtTagList.build(); + for (Material material : materials) { + Object block = craftMagicNumbers.getBlock(material); + + if (block != null) { + String blockString = block.toString(); // Format: Block{what we want} + blockString = blockString.substring(6, blockString.length() - 1); + Object nbtString = nbtTagString.build(blockString); + nbtTagList.add(canDestroyList, nbtString); + } + } + if (!nbtTagList.isEmpty(canDestroyList)) unhandledTags.put(collectionName, canDestroyList); + } + + @Override + public boolean canMineBlock(MaterialData blockMaterial, ItemStack tool) { + if (!blockMaterial.getItemType().isBlock()) { + throw new IllegalArgumentException("Material '" + blockMaterial + "' is not a block"); + } + + Object nmsBlock = craftMagicNumbers.getBlock(blockMaterial.getItemType()); + Object nmsTool = tool == null ? null : craftMagicNumbers.getItem(tool.getType()); + + Object iBlockData = reflBlock.getBlockData(nmsBlock); + + boolean alwaysDestroyable = refl.isAlwaysDestroyable(reflBlock.getMaterial(nmsBlock)); + boolean toolCanDestroy = nmsTool != null && refl.canDestroySpecialBlock(nmsTool, iBlockData); + return nmsBlock != null && (alwaysDestroyable || toolCanDestroy); + } + + @Override + public AttributeMap buildAttributeMap(Player player) { + return new AttributeMapBukkit(player); + } + + @Override + public void applyAttributeModifiers( + SetMultimap attributeModifiers, ItemMeta meta) { + Object list = nbtTagList.build(); + if (!attributeModifiers.isEmpty()) { + for (Map.Entry entry : attributeModifiers.entries()) { + AttributeModifier modifier = entry.getValue(); + Object tag = nbtTagCompound.build(); + nbtTagCompound.setString(tag, "Name", modifier.getName()); + nbtTagCompound.setDouble(tag, "Amount", modifier.getAmount()); + nbtTagCompound.setInt(tag, "Operation", modifier.getOperation().ordinal()); + nbtTagCompound.setLong(tag, "UUIDMost", modifier.getUniqueId().getMostSignificantBits()); + nbtTagCompound.setLong(tag, "UUIDLeast", modifier.getUniqueId().getLeastSignificantBits()); + nbtTagCompound.setString(tag, "AttributeName", entry.getKey()); + nbtTagList.add(list, tag); + } + + Map unhandledTags = refl.getUnhandledTags(meta); + + unhandledTags.put("AttributeModifiers", list); + } + } + + @Override + public SetMultimap getAttributeModifiers(ItemMeta meta) { + Map unhandledTags = refl.getUnhandledTags(meta); + if (unhandledTags.containsKey("AttributeModifiers")) { + final SetMultimap attributeModifiers = HashMultimap.create(); + final Object modTags = unhandledTags.get("AttributeModifiers"); + for (int i = 0; i < nbtTagList.size(modTags); i++) { + final Object modTag = nbtTagList.get(modTags, i); + attributeModifiers.put( + nbtTagCompound.getString(modTag, "AttributeName"), + new AttributeModifier( + new UUID( + nbtTagCompound.getLong(modTag, "UUIDMost"), + nbtTagCompound.getLong(modTag, "UUIDLeast")), + nbtTagCompound.getString(modTag, "Name"), + nbtTagCompound.getDouble(modTag, "Amount"), + AttributeModifier.Operation.fromOpcode( + nbtTagCompound.getInt(modTag, "Operation")))); + } + return attributeModifiers; + } else { + return HashMultimap.create(); + } + } + + @Override + public ItemStack craftItemCopy(ItemStack item) { + return craftItemStack.asCraftCopy(item); + } + + @Override + public void postToMainThread(Plugin plugin, boolean priority, Runnable task) { + Server bukkitServer = plugin.getServer(); + Object nmsServer = refl.getNMSServer(refl.getCraftServerHandle(bukkitServer)); + refl.addCallableToMainThread(nmsServer, Executors.callable(task)); + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/nms/v1_9/NMSHacksProtocolLib.java b/util/src/main/java/tc/oc/pgm/util/nms/v1_9/NMSHacksProtocolLib.java new file mode 100644 index 0000000000..ba95bc4596 --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/nms/v1_9/NMSHacksProtocolLib.java @@ -0,0 +1,642 @@ +package tc.oc.pgm.util.nms.v1_9; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.ProtocolManager; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.reflect.StructureModifier; +import com.comphenix.protocol.wrappers.BlockPosition; +import com.comphenix.protocol.wrappers.EnumWrappers; +import com.comphenix.protocol.wrappers.PlayerInfoData; +import com.comphenix.protocol.wrappers.WrappedBlockData; +import com.comphenix.protocol.wrappers.WrappedChatComponent; +import com.comphenix.protocol.wrappers.WrappedDataWatcher; +import com.comphenix.protocol.wrappers.WrappedGameProfile; +import com.comphenix.protocol.wrappers.WrappedSignedProperty; +import com.google.common.collect.Sets; +import io.github.bananapuncher714.nbteditor.NBTEditor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Fireball; +import org.bukkit.entity.Firework; +import org.bukkit.entity.Item; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.bukkit.scoreboard.NameTagVisibility; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tc.oc.pgm.util.block.RayBlockIntersection; +import tc.oc.pgm.util.bukkit.ViaUtils; +import tc.oc.pgm.util.nms.EnumPlayerInfoAction; +import tc.oc.pgm.util.nms.NMSHacksNoOp; +import tc.oc.pgm.util.nms.entity.fake.FakeEntity; +import tc.oc.pgm.util.nms.entity.fake.armorstand.FakeArmorStandProtocolLib; +import tc.oc.pgm.util.nms.entity.fake.wither.FakeWitherSkullProtocolLib; +import tc.oc.pgm.util.skin.Skin; + +public abstract class NMSHacksProtocolLib extends NMSHacksNoOp { + + static ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); + + @Override + public void sendPacket(Player bukkitPlayer, Object packet) { + if (packet != null && bukkitPlayer != null) { + protocolManager.sendServerPacket(bukkitPlayer, (PacketContainer) packet); + } + } + + private List getViewingPlayers(Entity entity) { + // TODO: use radius defined in spigot.yml / sportpaper.yml + List players = new ArrayList<>(); + for (Entity nearbyEntity : + entity.getWorld().getNearbyEntities(entity.getLocation(), 64, 64, 64)) { + if (nearbyEntity instanceof Player && nearbyEntity.getEntityId() != entity.getEntityId()) { + players.add((Player) nearbyEntity); + } + } + return players; + } + + @Override + public void sendPacketToViewers(Entity entity, Object packet, boolean excludeSpectators) { + for (Player nearbyPlayer : getViewingPlayers(entity)) { + if (excludeSpectators) { + Entity spectatorTarget = nearbyPlayer.getSpectatorTarget(); + if (spectatorTarget != null && spectatorTarget.getUniqueId().equals(entity.getUniqueId())) + continue; + } + sendPacket(nearbyPlayer, packet); + } + } + + @Override + public void playDeathAnimation(Player player) { + float health = 0.0f; + PacketContainer metadataPacket = getMetadataPacket(player, health); + + sendPacketToViewers(player, metadataPacket, true); + + metadataPacket = getMetadataPacket(player, 1.0f); + sendPacket(player, metadataPacket); + + // Reimplement using Poses in 1.13+ + Location location = player.getLocation(); + PacketContainer useBedPacket = new PacketContainer(PacketType.Play.Server.USE_BED); + useBedPacket.getEntityModifier(player.getWorld()).write(0, player); + BlockPosition blockPosition = + new BlockPosition(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + useBedPacket.getBlockPositionModifier().write(0, blockPosition); + sendPacketToViewers(player, useBedPacket, true); + + Object teleport = teleportEntityPacket(player.getEntityId(), location); + sendPacketToViewers(player, teleport, true); + } + + @NotNull + private static PacketContainer getMetadataPacket(Player player, float health) { + PacketContainer metadataPacket = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA); + + metadataPacket.getIntegers().write(0, player.getEntityId()); + + WrappedDataWatcher wrappedDataWatcher = WrappedDataWatcher.getEntityWatcher(player).deepClone(); + + WrappedDataWatcher.WrappedDataWatcherObject watcherObject = + new WrappedDataWatcher.WrappedDataWatcherObject( + 6, WrappedDataWatcher.Registry.get(Float.class)); + wrappedDataWatcher.setObject(watcherObject, health); + + metadataPacket + .getWatchableCollectionModifier() + .write(0, wrappedDataWatcher.getWatchableObjects()); + return metadataPacket; + } + + @Override + public Object teleportEntityPacket(int entityId, Location location) { + PacketContainer entityTeleportPacket = + new PacketContainer(PacketType.Play.Server.ENTITY_TELEPORT); + entityTeleportPacket.getIntegers().write(0, entityId); + + entityTeleportPacket + .getDoubles() + .write(0, location.getX()) + .write(1, location.getY()) + .write(2, location.getZ()); + + entityTeleportPacket + .getBytes() + .write(0, (byte) (location.getYaw() * 256 / 360)) + .write(1, (byte) (location.getPitch() * 256 / 360)); + + entityTeleportPacket.getBooleans().write(0, true); + + return entityTeleportPacket; + } + + @Override + public Object entityMetadataPacket(int entityId, Entity entity, boolean complete) { + PacketContainer packetContainer = new PacketContainer(PacketType.Play.Server.ENTITY_METADATA); + + packetContainer.getIntegers().write(0, entityId); + + WrappedDataWatcher wrappedDataWatcher = WrappedDataWatcher.getEntityWatcher(entity).deepClone(); + packetContainer.getDataWatcherModifier().write(0, wrappedDataWatcher); + + return packetContainer; + } + + @Override + public void skipFireworksLaunch(Firework firework) { + NBTEditor.set(firework, "Life", 2); + NBTEditor.set(firework, "LifeTime", 2); + sendPacketToViewers( + firework, entityMetadataPacket(firework.getEntityId(), firework, false), false); + } + + static Sound itemPickupSound = Sound.valueOf("ENTITY_ITEM_PICKUP"); + + @Override + public void fakePlayerItemPickup(Player player, Item item) { + float pitch = (((float) (Math.random() - Math.random()) * 0.7F + 1.0F) * 2.0F); + item.getWorld().playSound(item.getLocation(), itemPickupSound, 0.2F, pitch); + + PacketContainer packet = new PacketContainer(PacketType.Play.Server.COLLECT); + packet.getIntegers().write(0, item.getEntityId()); + packet.getIntegers().write(1, player.getEntityId()); + + sendPacketToViewers(item, packet, false); + + item.remove(); + } + + @Override + public void freezeEntity(Entity entity) { + NBTEditor.set(entity, true, "NoAI"); + NBTEditor.set(entity, true, "NoGravity"); + } + + @Override + public void setFireballDirection(Fireball entity, Vector direction) { + List doubles = new ArrayList<>(); + doubles.add(direction.getX() * 0.1D); + doubles.add(direction.getY() * 0.1D); + doubles.add(direction.getZ() * 0.1D); + NBTEditor.set(entity, doubles, "power"); + } + + @Override + public void removeAndAddAllTabPlayers(Player viewer) { + List playerInfoDataList = new ArrayList<>(); + + for (Player player : Bukkit.getOnlinePlayers()) { + if (viewer.canSee(player) || player == viewer) { + playerInfoDataList.add( + new PlayerInfoData( + WrappedGameProfile.fromPlayer(player), + getPing(player), + EnumWrappers.NativeGameMode.fromBukkit(viewer.getGameMode()), + WrappedChatComponent.fromLegacyText(player.getPlayerListName()))); + } + } + + PacketContainer removePlayerPacket = new PacketContainer(PacketType.Play.Server.PLAYER_INFO); + removePlayerPacket.getPlayerInfoAction().write(0, EnumWrappers.PlayerInfoAction.REMOVE_PLAYER); + + removePlayerPacket.getPlayerInfoDataLists().write(0, playerInfoDataList); + sendPacket(viewer, removePlayerPacket); + + PacketContainer addPlayerPacket = new PacketContainer(PacketType.Play.Server.PLAYER_INFO); + addPlayerPacket.getPlayerInfoAction().write(0, EnumWrappers.PlayerInfoAction.ADD_PLAYER); + + addPlayerPacket.getPlayerInfoDataLists().write(0, playerInfoDataList); + sendPacket(viewer, addPlayerPacket); + } + + @Override + public void sendLegacyWearing(Player player, int slot, ItemStack item) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_EQUIPMENT); + + packet.getIntegers().write(0, player.getEntityId()); + + if (slot != 4) { + throw new UnsupportedOperationException( + "NMSHacks.entityEquipmentPacket needs to be refactored to support this!"); + } + packet.getItemSlots().write(0, EnumWrappers.ItemSlot.HEAD); + + packet.getItemModifier().write(0, item); + + for (Player viewingPlayer : getViewingPlayers(player)) { + if (ViaUtils.getProtocolVersion(viewingPlayer) <= ViaUtils.VERSION_1_7) { + sendPacket(viewingPlayer, packet); + } + } + } + + @Override + public void sendBlockChange(Location loc, Player player, @Nullable Material material) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.BLOCK_CHANGE); + + BlockPosition blockPosition = new BlockPosition(loc.toVector()); + + packet.getBlockPositionModifier().write(0, blockPosition); + + WrappedBlockData data; + + if (material == null) { + Block block = loc.getBlock(); + data = WrappedBlockData.createData(block.getType(), block.getData()); + } else { + data = WrappedBlockData.createData(material); + } + + packet.getBlockData().write(0, data); + + sendPacket(player, packet); + } + + @Override + public void clearArrowsInPlayer(Player player) { + WrappedDataWatcher entityWatcher = WrappedDataWatcher.getEntityWatcher(player); + entityWatcher.setObject(9, (int) 0, true); + } + + @Override + public void showBorderWarning(Player player, boolean show) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.WORLD_BORDER); + packet.getWorldBorderActions().write(0, EnumWrappers.WorldBorderAction.SET_WARNING_BLOCKS); + + packet.getIntegers().write(0, 29999984).write(1, 15).write(2, show ? Integer.MAX_VALUE : 0); + + packet.getDoubles().write(0, 0.0).write(1, 0.0).write(2, 6.0E7D).write(3, 6.0E7D); + packet.getLongs().write(0, 0L); + + sendPacket(player, packet); + } + + @Override + public Object entityEquipmentPacket(int entityId, int slot, ItemStack armor) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_EQUIPMENT); + + packet.getIntegers().write(0, entityId); + + if (slot != 4) { + throw new UnsupportedOperationException( + "NMSHacks.entityEquipmentPacket needs to be refactored to support this!"); + } else { + packet.getItemSlots().write(0, EnumWrappers.ItemSlot.HEAD); + } + + packet.getItemModifier().write(0, armor); + + return packet; + } + + @Override + public void entityAttach(Player player, int entityID, int vehicleID, boolean leash) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.MOUNT); + + packet.getIntegers().write(0, vehicleID); + + int[] ridingEntities = {entityID}; + + packet.getIntegerArrays().write(0, ridingEntities); + + sendPacket(player, packet); + } + + @Override + public FakeEntity fakeWitherSkull(World world) { + return new FakeWitherSkullProtocolLib(); + } + + @Override + public FakeEntity fakeArmorStand(World world, ItemStack head) { + return new FakeArmorStandProtocolLib(head); + } + + @Override + public Object spawnPlayerPacket(int entityId, UUID uuid, Location location, Player player) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.NAMED_ENTITY_SPAWN); + + packet.getIntegers().write(0, entityId); + + packet + .getDoubles() + .write(0, location.getX()) + .write(1, location.getY()) + .write(2, location.getZ()); + + packet.getUUIDs().write(0, uuid); + + packet + .getBytes() + .write(0, (byte) (int) (location.getYaw() * 256.0F / 360.0F)) + .write(1, (byte) (int) (location.getPitch() * 256.0F / 360.0F)); + + WrappedDataWatcher dataWatcher = + player == null + ? new WrappedDataWatcher() + : WrappedDataWatcher.getEntityWatcher(player).deepClone(); + + packet.getDataWatcherModifier().write(0, dataWatcher); + + return packet; + } + + @Override + public Object destroyEntitiesPacket(int... entityIds) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_DESTROY); + + packet.getIntegerArrays().write(0, entityIds); + + return packet; + } + + static EnumWrappers.PlayerInfoAction convertPlayerInfoAction( + EnumPlayerInfoAction enumPlayerInfoAction) { + switch (enumPlayerInfoAction) { + case ADD_PLAYER: + return EnumWrappers.PlayerInfoAction.ADD_PLAYER; + case UPDATE_GAME_MODE: + return EnumWrappers.PlayerInfoAction.UPDATE_GAME_MODE; + case UPDATE_LATENCY: + return EnumWrappers.PlayerInfoAction.UPDATE_LATENCY; + case UPDATE_DISPLAY_NAME: + return EnumWrappers.PlayerInfoAction.UPDATE_DISPLAY_NAME; + case REMOVE_PLAYER: + default: + return EnumWrappers.PlayerInfoAction.REMOVE_PLAYER; + } + } + + @Override + public Object createPlayerInfoPacket(EnumPlayerInfoAction action) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.PLAYER_INFO); + + packet.getPlayerInfoAction().write(0, convertPlayerInfoAction(action)); + + return packet; + } + + @Override + public void setPotionParticles(Player player, boolean enabled) { + WrappedDataWatcher dataWatcher = WrappedDataWatcher.getEntityWatcher(player); + + if (enabled) { + Collection activePotionEffects = player.getActivePotionEffects(); + for (PotionEffect potionEffect : activePotionEffects) { + if (!potionEffect.isAmbient()) { + dataWatcher.setObject(8, false, true); + dataWatcher.setObject(7, potionEffect.getType().getId(), true); + } + } + } + dataWatcher.setObject(7, (int) 0, true); + dataWatcher.setObject(8, true, true); + } + + @Override + public RayBlockIntersection getTargetedBLock(Player player) { + Block targetBlock = player.getTargetBlock(Sets.newHashSet(Material.AIR), 4); + + // this hit location will cause some particles to hide inside the block in adventure mode maps + // with blockdrops + Vector hitLocation = targetBlock.getLocation().toVector().add(new Vector(0.5, 0.5, 0.5)); + + // BlockFace is unused, default up + return new RayBlockIntersection(targetBlock, BlockFace.UP, hitLocation); + } + + @Override + public boolean playerInfoDataListNotEmpty(Object packet) { + PacketContainer packetContainer = (PacketContainer) packet; + + StructureModifier> playerInfoDataListsModifier = + packetContainer.getPlayerInfoDataLists(); + return !playerInfoDataListsModifier.read(0).isEmpty(); + } + + @Override + public Object playerListPacketData( + Object packetPlayOutPlayerInfo, + UUID uuid, + String name, + GameMode gamemode, + int ping, + @Nullable Skin skin, + @Nullable String renderedDisplayName) { + + WrappedGameProfile wrappedGameProfile = new WrappedGameProfile(uuid, name); + + if (skin != null) { + WrappedSignedProperty property = + WrappedSignedProperty.fromValues("textures", skin.getData(), skin.getSignature()); + wrappedGameProfile.getProperties().put("textures", property); + } + + EnumWrappers.NativeGameMode nativeGameMode = + gamemode == null + ? EnumWrappers.NativeGameMode.CREATIVE + : EnumWrappers.NativeGameMode.fromBukkit(gamemode); + + WrappedChatComponent wrappedChatComponent = + renderedDisplayName == null + ? WrappedChatComponent.fromText("") + : WrappedChatComponent.fromJson(renderedDisplayName); + + return new PlayerInfoData(wrappedGameProfile, ping, nativeGameMode, wrappedChatComponent); + } + + @Override + public void addPlayerInfoToPacket(Object packet, Object playerInfoData) { + PacketContainer packetContainer = (PacketContainer) packet; + + StructureModifier> playerInfoDataListsModifier = + packetContainer.getPlayerInfoDataLists(); + List playerInfoDataList = playerInfoDataListsModifier.read(0); + playerInfoDataList.add((PlayerInfoData) playerInfoData); + playerInfoDataListsModifier.write(0, playerInfoDataList); + } + + @Override + public Skin getPlayerSkin(Player player) { + for (WrappedSignedProperty wrappedSignedProperty : + WrappedGameProfile.fromPlayer(player).getProperties().get("textures")) { + // return the first texture + return new Skin(wrappedSignedProperty.getValue(), wrappedSignedProperty.getSignature()); + } + + return null; + } + + @Override + public void updateVelocity(Player player) { + Vector velocity = player.getVelocity(); + PacketContainer packet = new PacketContainer(PacketType.Play.Server.ENTITY_VELOCITY); + packet + .getIntegers() + .write(0, player.getEntityId()) + .write(1, (int) (velocity.getX() * 8000)) + .write(2, (int) (velocity.getY() * 8000)) + .write(3, (int) (velocity.getZ() * 8000)); + sendPacket(player, packet); + } + + @Override + public void sendSpawnEntityPacket( + Player player, int entityId, Location location, Vector velocity) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.SPAWN_ENTITY); + packet + .getIntegers() + .write(0, entityId) + .write(1, (int) (velocity.getX() * 8000)) + .write(2, (int) (velocity.getY() * 8000)) + .write(3, (int) (velocity.getZ() * 8000)) + .write(4, (int) (location.getPitch() * 256.0F / 360.0F)) + .write(5, (int) (location.getYaw() * 256.0F / 360.0F)) + .write(6, 66); + + packet.getUUIDs().write(0, UUID.randomUUID()); + + packet + .getDoubles() + .write(0, location.getX()) + .write(1, location.getY()) + .write(2, location.getZ()); + + sendPacket(player, packet); + } + + @Override + public void spawnFreezeEntity(Player player, int entityId, boolean legacy) { + if (legacy) { + Location location = player.getLocation().add(0, 0.286, 0); + if (location.getY() < -64) { + location.setY(-64); + player.teleport(location); + } + + sendSpawnEntityPacket(player, entityId, location); + } else { + Location location = player.getLocation().subtract(0, 1.1, 0); + Vector velocity = new Vector(); + + spawnFakeArmorStand(player, entityId, location, velocity); + } + } + + @Override + public void spawnFakeArmorStand(Player player, int entityId, Location location, Vector velocity) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.SPAWN_ENTITY_LIVING); + packet + .getIntegers() + .write(0, entityId) + .write(1, 30) // armor stand + .write(2, (int) (velocity.getX() * 8000)) + .write(3, (int) (velocity.getY() * 8000)) + .write(4, (int) (velocity.getZ() * 8000)); + packet + .getDoubles() + .write(0, location.getX()) + .write(1, location.getY()) + .write(2, location.getZ()); + + packet + .getBytes() + .write(0, (byte) (int) (location.getYaw() * 256.0F / 360.0F)) + .write(1, (byte) (int) (location.getPitch() * 256.0F / 360.0F)) + .write(2, (byte) (int) (location.getPitch() * 256.0F / 360.0F)); + + packet.getUUIDs().write(0, UUID.randomUUID()); + + WrappedDataWatcher dataWatcher = new WrappedDataWatcher(); + dataWatcher.setObject( + new WrappedDataWatcher.WrappedDataWatcherObject( + 0, WrappedDataWatcher.Registry.get(Byte.class)), + (byte) 0x20); + dataWatcher.setObject( + new WrappedDataWatcher.WrappedDataWatcherObject( + 4, WrappedDataWatcher.Registry.get(Boolean.class)), + true); + dataWatcher.setObject( + new WrappedDataWatcher.WrappedDataWatcherObject( + 10, WrappedDataWatcher.Registry.get(Byte.class)), + (byte) 0x8); + + packet.getDataWatcherModifier().write(0, dataWatcher); + + sendPacket(player, packet); + } + + @Override + public Object teamPacket( + int operation, + String name, + String displayName, + String prefix, + String suffix, + boolean friendlyFire, + boolean seeFriendlyInvisibles, + NameTagVisibility nameTagVisibility, + Collection players) { + PacketContainer packet = new PacketContainer(PacketType.Play.Server.SCOREBOARD_TEAM); + + String nameTagVisString = null; + if (nameTagVisibility != null) { + switch (nameTagVisibility) { + case ALWAYS: + nameTagVisString = "always"; + break; + case NEVER: + nameTagVisString = "never"; + break; + case HIDE_FOR_OTHER_TEAMS: + nameTagVisString = "hideForOtherTeams"; + break; + case HIDE_FOR_OWN_TEAM: + nameTagVisString = "hideForOwnTeam"; + break; + } + } + + packet + .getStrings() + .write(0, name) + .write(1, displayName) + .write(2, prefix) + .write(3, suffix) + .write(4, nameTagVisString); + + int flags = 0; + if (friendlyFire) { + flags |= 1; + } + if (seeFriendlyInvisibles) { + flags |= 2; + } + + packet + .getIntegers() + .write(0, -1) // color + .write(1, operation) + .write(2, flags); + + packet.getSpecificModifier(Collection.class).write(0, players); + + return packet; + } +} diff --git a/util/src/main/java/tc/oc/pgm/util/reflect/MinecraftReflectionUtils.java b/util/src/main/java/tc/oc/pgm/util/reflect/MinecraftReflectionUtils.java index 985c02d311..2fbfe3a3d8 100644 --- a/util/src/main/java/tc/oc/pgm/util/reflect/MinecraftReflectionUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/reflect/MinecraftReflectionUtils.java @@ -1,10 +1,28 @@ package tc.oc.pgm.util.reflect; +import io.github.bananapuncher714.nbteditor.NBTEditor; import org.bukkit.Bukkit; public interface MinecraftReflectionUtils { String VERSION = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; + NBTEditor.MinecraftVersion MINECRAFT_VERSION = NBTEditor.getMinecraftVersion(); + + double VERSION_NUMBER = parseVersionNumber(); + + static double parseVersionNumber() { + String[] split = VERSION.split("_"); + + double version = Double.parseDouble(split[1]); + ; + + if (split.length > 2) { + version += 0.1 * Double.parseDouble(split[2].substring(1)); + } + + return version; + } + static Class getBukkitClass(String classPath) { return ReflectionUtils.getClassFromName("org.bukkit." + classPath); } @@ -13,7 +31,19 @@ static Class getCraftBukkitClass(String classPath) { return ReflectionUtils.getClassFromName("org.bukkit.craftbukkit." + VERSION + "." + classPath); } - static Class getNMSClass(String classPath) { + static Class getNMSClassOriginal(String classPath, String newPath) { + if (MINECRAFT_VERSION.lessThanOrEqualTo(NBTEditor.MinecraftVersion.v1_16)) { + return ReflectionUtils.getClassFromName("net.minecraft.server." + VERSION + "." + classPath); + } else { + return ReflectionUtils.getClassFromName("net.minecraft." + newPath); + } + } + + static Class getNMSClassLegacy(String classPath) { return ReflectionUtils.getClassFromName("net.minecraft.server." + VERSION + "." + classPath); } + + static Class getNMSClassNew(String classPath) { + return ReflectionUtils.getClassFromName("net.minecraft." + classPath); + } } diff --git a/util/src/main/java/tc/oc/pgm/util/tablist/TablistResizer.java b/util/src/main/java/tc/oc/pgm/util/tablist/TablistResizer.java new file mode 100644 index 0000000000..50ea08cc6b --- /dev/null +++ b/util/src/main/java/tc/oc/pgm/util/tablist/TablistResizer.java @@ -0,0 +1,29 @@ +package tc.oc.pgm.util.tablist; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.events.ListenerPriority; +import com.comphenix.protocol.events.PacketAdapter; +import com.comphenix.protocol.events.PacketEvent; +import org.bukkit.plugin.Plugin; + +public class TablistResizer { + private static final int TAB_SIZE = 80; + + public static void registerAdapter(Plugin plugin) { + ProtocolLibrary.getProtocolManager().addPacketListener(new TablistResizePacketAdapter(plugin)); + } + + private static class TablistResizePacketAdapter extends PacketAdapter { + + public TablistResizePacketAdapter(Plugin plugin) { + super(plugin, ListenerPriority.LOWEST, PacketType.Play.Server.LOGIN); + } + + @Override + public void onPacketSending(PacketEvent event) { + if (event.getPacketType() == PacketType.Play.Server.LOGIN) + event.getPacket().getIntegers().write(2, TAB_SIZE); + } + } +} From c9c4b3e163abafa3d9622a92d6396125ec2e8a7b Mon Sep 17 00:00:00 2001 From: Pablo Herrera Date: Mon, 7 Aug 2023 06:50:05 +0200 Subject: [PATCH 23/23] Tweak flags & gamemodes, add gamemode to votebook (#1210) Signed-off-by: Pablo Herrera --- .../main/java/tc/oc/pgm/api/map/MapTag.java | 67 ++++++----- .../java/tc/oc/pgm/blitz/BlitzModule.java | 3 +- .../java/tc/oc/pgm/classes/ClassModule.java | 3 +- .../java/tc/oc/pgm/command/MapCommand.java | 20 +++- .../pgm/controlpoint/ControlPointModule.java | 12 +- .../main/java/tc/oc/pgm/core/CoreModule.java | 3 +- .../oc/pgm/destroyable/DestroyableModule.java | 3 +- .../java/tc/oc/pgm/ffa/FreeForAllModule.java | 3 +- .../main/java/tc/oc/pgm/flag/FlagModule.java | 3 +- .../java/tc/oc/pgm/map/MapContextImpl.java | 28 +---- .../main/java/tc/oc/pgm/map/MapInfoImpl.java | 105 ++++++++++++------ .../main/java/tc/oc/pgm/rage/RageModule.java | 3 +- .../vote/book/VotingBookCreatorImpl.java | 36 +++++- .../java/tc/oc/pgm/score/ScoreModule.java | 6 +- .../main/java/tc/oc/pgm/shops/ShopModule.java | 3 +- .../main/java/tc/oc/pgm/teams/TeamModule.java | 4 +- .../tc/oc/pgm/timelimit/TimeLimitModule.java | 2 +- .../main/java/tc/oc/pgm/tnt/TNTModule.java | 2 +- .../main/java/tc/oc/pgm/wool/WoolModule.java | 3 +- .../oc/pgm/worldborder/WorldBorderModule.java | 3 +- .../main/java/tc/oc/pgm/util/StreamUtils.java | 17 +++ 21 files changed, 203 insertions(+), 126 deletions(-) diff --git a/core/src/main/java/tc/oc/pgm/api/map/MapTag.java b/core/src/main/java/tc/oc/pgm/api/map/MapTag.java index 1bff7ce000..a99740e2ae 100644 --- a/core/src/main/java/tc/oc/pgm/api/map/MapTag.java +++ b/core/src/main/java/tc/oc/pgm/api/map/MapTag.java @@ -1,53 +1,51 @@ package tc.oc.pgm.api.map; -import static net.kyori.adventure.text.Component.empty; import static net.kyori.adventure.text.Component.text; import static tc.oc.pgm.util.Assert.assertNotNull; import static tc.oc.pgm.util.Assert.assertTrue; +import java.util.Collections; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.regex.Pattern; import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; /** A "#hashtag" that describes a {@link MapInfo} feature. */ public final class MapTag implements Comparable { + private static final SortedSet TAG_IDS = new TreeSet<>(); + private static final Pattern PATTERN = Pattern.compile("^[a-z0-9_-]+$"); private static final String SYMBOL = "#"; private final String id; private final Component name; - private final Component acronym; - private final boolean gamemode; + private final @Nullable Gamemode gamemode; private final boolean auxiliary; - public MapTag( - final String id, final String name, final boolean gamemode, final boolean auxiliary) { - this(id, id, name, gamemode, auxiliary); + public MapTag(String id, String name) { + this(id, name, null, true); + } + + public MapTag(String id, Gamemode gm, boolean auxiliary) { + this(id, gm.getFullName(), gm, auxiliary); } - public MapTag( - final String internalId, - final String id, - final String name, - final boolean gamemode, - final boolean auxiliary) { - assertTrue( - PATTERN.matcher(assertNotNull(id)).matches(), name + " must match " + PATTERN.pattern()); + private MapTag(String id, String name, @Nullable Gamemode gamemode, boolean auxiliary) { + assertNotNull(id); + assertTrue(PATTERN.matcher(id).matches(), id + " must match " + PATTERN.pattern()); + TAG_IDS.add(id); this.id = id; - if (gamemode) { - Gamemode gm = Gamemode.byId(internalId); - if (gm == null) { - throw new IllegalArgumentException("Gamemode id " + internalId + " not recognized"); - } - this.name = text(gm.getFullName()); - this.acronym = text(gm.getAcronym()); - } else { - this.name = text(name); - this.acronym = empty(); - } + this.name = text(name); this.gamemode = gamemode; this.auxiliary = auxiliary; } + public static Set getAllTagIds() { + return Collections.unmodifiableSortedSet(TAG_IDS); + } + /** * Get a short id for the tag. * @@ -66,26 +64,23 @@ public Component getName() { return this.name; } - /** - * Gets an acronym for the tag. - * - * @return An acronym. - */ - public Component getAcronym() { - return this.acronym; - } - /** * Get whether this tag represents a "gamemode." * - * @return If a gamemode. + * @return If the tag is for a gamemode. */ public boolean isGamemode() { + return this.gamemode != null; + } + + /** @return the gamemode if this tag represents one, null otherwise. */ + public @Nullable Gamemode getGamemode() { return this.gamemode; } /** - * Get whether this tag is an auxiliary feature. + * Get whether this tag is an auxiliary gamemode, that works as a 2nd level gamemode. Eg: blitz or + * rage are auxiliary due to wool "and blitz", or deathmatch "and rage". * * @return If an auxiliary feature. */ diff --git a/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java b/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java index ba489c86f9..594c4a844d 100644 --- a/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java +++ b/core/src/main/java/tc/oc/pgm/blitz/BlitzModule.java @@ -10,6 +10,7 @@ import org.jdom2.Document; import org.jdom2.Element; import tc.oc.pgm.api.filter.Filter; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -26,7 +27,7 @@ public class BlitzModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("blitz", "Blitz", true, true)); + ImmutableList.of(new MapTag("blitz", Gamemode.BLITZ, true)); private final BlitzConfig config; public BlitzModule(BlitzConfig config) { diff --git a/core/src/main/java/tc/oc/pgm/classes/ClassModule.java b/core/src/main/java/tc/oc/pgm/classes/ClassModule.java index fe7f4592d2..5fd7fec431 100644 --- a/core/src/main/java/tc/oc/pgm/classes/ClassModule.java +++ b/core/src/main/java/tc/oc/pgm/classes/ClassModule.java @@ -32,8 +32,7 @@ public class ClassModule implements MapModule { - private static final Collection TAGS = - ImmutableList.of(new MapTag("classes", "Classes", false, true)); + private static final Collection TAGS = ImmutableList.of(new MapTag("classes", "Classes")); final String family; final Map classes; final PlayerClass defaultClass; diff --git a/core/src/main/java/tc/oc/pgm/command/MapCommand.java b/core/src/main/java/tc/oc/pgm/command/MapCommand.java index 407da1404c..200aa07f24 100644 --- a/core/src/main/java/tc/oc/pgm/command/MapCommand.java +++ b/core/src/main/java/tc/oc/pgm/command/MapCommand.java @@ -15,6 +15,8 @@ import cloud.commandframework.annotations.Flag; import cloud.commandframework.annotations.specifier.Greedy; import cloud.commandframework.annotations.specifier.Range; +import cloud.commandframework.annotations.suggestions.Suggestions; +import cloud.commandframework.context.CommandContext; import com.google.common.collect.ImmutableList; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -36,6 +38,7 @@ import tc.oc.pgm.rotation.MapPoolManager; import tc.oc.pgm.rotation.pools.MapPool; import tc.oc.pgm.util.Audience; +import tc.oc.pgm.util.LiquidMetal; import tc.oc.pgm.util.PrettyPaginatedComponentResults; import tc.oc.pgm.util.StringUtils; import tc.oc.pgm.util.named.MapNameStyle; @@ -52,7 +55,8 @@ public void maps( CommandSender sender, MapLibrary library, @Argument(value = "page", defaultValue = "1") @Range(min = "1") int page, - @Flag(value = "tags", aliases = "t", repeatable = true) List tags, + @Flag(value = "tags", aliases = "t", repeatable = true, suggestions = "maptags") + List tags, @Flag(value = "author", aliases = "a") String author, @Flag(value = "name", aliases = "n") String name, @Flag(value = "phase", aliases = "p") Phase phase) { @@ -115,6 +119,20 @@ public Component format(MapInfo map, int index) { }.display(audience, ImmutableList.copyOf(maps), page); } + @Suggestions("maptags") + public List suggestMapTags(CommandContext sender, String input) { + int commaIdx = input.lastIndexOf(','); + + final String prefix = input.substring(0, commaIdx == -1 ? 0 : commaIdx + 1); + final String toComplete = + input.substring(commaIdx + 1).toLowerCase(Locale.ROOT).replace("!", ""); + + return MapTag.getAllTagIds().stream() + .filter(mt -> LiquidMetal.match(mt, toComplete)) + .flatMap(tag -> Stream.of(prefix + tag, prefix + "!" + tag)) + .collect(Collectors.toList()); + } + private static boolean matchesTags( MapInfo map, Collection posTags, Collection negTags) { int matches = 0; diff --git a/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointModule.java b/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointModule.java index b11b35170b..017abfe234 100644 --- a/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointModule.java +++ b/core/src/main/java/tc/oc/pgm/controlpoint/ControlPointModule.java @@ -11,6 +11,7 @@ import org.jdom2.Document; import org.jdom2.Element; import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -27,11 +28,9 @@ public class ControlPointModule implements MapModule { - private static final MapTag CP = - new MapTag("cp", "controlpoint", "Control the Point", true, false); - private static final MapTag KOTH = - new MapTag("koth", "controlpoint", "King of the Hill", true, false); - private static final MapTag PAYLOAD = new MapTag("payload", "Payload", true, false); + private static final MapTag CP = new MapTag("controlpoint", Gamemode.CONTROL_THE_POINT, false); + private static final MapTag KOTH = new MapTag("controlpoint", Gamemode.KING_OF_THE_HILL, false); + private static final MapTag PAYLOAD = new MapTag("payload", Gamemode.PAYLOAD, false); private final List definitions; private final Collection tags; @@ -94,7 +93,8 @@ public ControlPointModule parse(MapFactory factory, Logger logger, Document doc) for (Element kingEl : doc.getRootElement().getChildren("king")) { for (Element hillEl : XMLUtils.flattenElements(kingEl, "hills", "hill")) { - tags.add(KOTH); + // CP and KOTH are mutually exclusive, they're the same ID. + if (tags.isEmpty()) tags.add(KOTH); ControlPointDefinition definition = ControlPointParser.parseControlPoint(factory, hillEl, Type.HILL, serialNumber); factory.getFeatures().addFeature(kingEl, definition); diff --git a/core/src/main/java/tc/oc/pgm/core/CoreModule.java b/core/src/main/java/tc/oc/pgm/core/CoreModule.java index d078dbcfa3..c3516fd5f1 100644 --- a/core/src/main/java/tc/oc/pgm/core/CoreModule.java +++ b/core/src/main/java/tc/oc/pgm/core/CoreModule.java @@ -12,6 +12,7 @@ import org.jdom2.Attribute; import org.jdom2.Document; import org.jdom2.Element; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.map.MapTag; @@ -38,7 +39,7 @@ public class CoreModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("dtc", "core", "Destroy the Core", true, false)); + ImmutableList.of(new MapTag("core", Gamemode.DESTROY_THE_CORE, false)); protected final List coreFactories; public CoreModule(List coreFactories) { diff --git a/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java b/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java index 0910656cd2..1fc17c6032 100644 --- a/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java +++ b/core/src/main/java/tc/oc/pgm/destroyable/DestroyableModule.java @@ -9,6 +9,7 @@ import java.util.logging.Logger; import org.jdom2.Document; import org.jdom2.Element; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.map.MapTag; @@ -37,7 +38,7 @@ public class DestroyableModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("dtm", "monument", "Destroy the Monument", true, false)); + ImmutableList.of(new MapTag("monument", Gamemode.DESTROY_THE_MONUMENT, false)); protected final List destroyableFactories; public DestroyableModule(List destroyableFactories) { diff --git a/core/src/main/java/tc/oc/pgm/ffa/FreeForAllModule.java b/core/src/main/java/tc/oc/pgm/ffa/FreeForAllModule.java index 4f694c7677..10b67380e3 100644 --- a/core/src/main/java/tc/oc/pgm/ffa/FreeForAllModule.java +++ b/core/src/main/java/tc/oc/pgm/ffa/FreeForAllModule.java @@ -8,6 +8,7 @@ import org.jdom2.Document; import org.jdom2.Element; import tc.oc.pgm.api.PGM; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -24,7 +25,7 @@ public class FreeForAllModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("ffa", "Free for All", false, false)); + ImmutableList.of(new MapTag("ffa", Gamemode.FREE_FOR_ALL, true)); private final FreeForAllOptions options; public FreeForAllModule(FreeForAllOptions options) { diff --git a/core/src/main/java/tc/oc/pgm/flag/FlagModule.java b/core/src/main/java/tc/oc/pgm/flag/FlagModule.java index 931c1c7947..d1a2010988 100644 --- a/core/src/main/java/tc/oc/pgm/flag/FlagModule.java +++ b/core/src/main/java/tc/oc/pgm/flag/FlagModule.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.logging.Logger; import org.jdom2.Document; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -22,7 +23,7 @@ public class FlagModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("ctf", "flag", "Capture the Flag", true, false)); + ImmutableList.of(new MapTag("flag", Gamemode.CAPTURE_THE_FLAG, false)); private final ImmutableList posts; private final ImmutableList nets; private final ImmutableList flags; diff --git a/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java b/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java index 3a843490d0..b70086c145 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapContextImpl.java @@ -2,48 +2,24 @@ import static tc.oc.pgm.util.Assert.assertNotNull; -import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; -import java.lang.ref.SoftReference; import java.util.Collection; import java.util.List; import tc.oc.pgm.api.map.MapContext; import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.map.MapModule; -import tc.oc.pgm.api.map.MapTag; -import tc.oc.pgm.ffa.FreeForAllModule; -import tc.oc.pgm.teams.TeamFactory; -import tc.oc.pgm.teams.TeamModule; public class MapContextImpl implements MapContext { - private static final MapTag TERRAIN = new MapTag("terrain", "Terrain", false, true); private final MapInfo info; private final List modules; public MapContextImpl(MapInfoImpl info, Collection> modules) { - info.context = new SoftReference<>(this); this.info = info; this.modules = ImmutableList.copyOf(assertNotNull(modules)); - for (MapModule module : this.modules) { - info.tags.addAll(module.getTags()); - - if (module instanceof TeamModule) { - info.players.clear(); - info.players.addAll( - Collections2.transform(((TeamModule) module).getTeams(), TeamFactory::getMaxPlayers)); - } - - if (module instanceof FreeForAllModule) { - info.players.clear(); - info.players.add(((FreeForAllModule) module).getOptions().maxPlayers); - } - } - - if (info.getWorld().hasTerrain()) { - info.tags.add(TERRAIN); - } + // Update the map info with stuff derived from modules, like team sizes or tags. + info.setContext(this); } public MapInfo getInfo() { diff --git a/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java b/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java index 2fa4a0c66f..574c292deb 100644 --- a/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java +++ b/core/src/main/java/tc/oc/pgm/map/MapInfoImpl.java @@ -4,6 +4,9 @@ import static net.kyori.adventure.text.Component.translatable; import static tc.oc.pgm.util.Assert.assertNotNull; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.collect.Iterables; import java.lang.ref.SoftReference; import java.time.LocalDate; import java.util.*; @@ -20,12 +23,17 @@ import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapContext; import tc.oc.pgm.api.map.MapInfo; +import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapSource; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.Phase; import tc.oc.pgm.api.map.WorldInfo; +import tc.oc.pgm.ffa.FreeForAllModule; import tc.oc.pgm.map.contrib.PlayerContributor; import tc.oc.pgm.map.contrib.PseudonymContributor; +import tc.oc.pgm.teams.TeamFactory; +import tc.oc.pgm.teams.TeamModule; +import tc.oc.pgm.util.StreamUtils; import tc.oc.pgm.util.StringUtils; import tc.oc.pgm.util.Version; import tc.oc.pgm.util.named.MapNameStyle; @@ -36,6 +44,9 @@ import tc.oc.pgm.util.xml.XMLUtils; public class MapInfoImpl implements MapInfo { + // See #parseWorld, this class actually is responsible for terrain tag. + private static final MapTag TERRAIN = new MapTag("terrain", "Terrain"); + private final MapSource source; private final String id; @@ -56,11 +67,13 @@ public class MapInfoImpl implements MapInfo { private final WorldInfo world; private final boolean friendlyFire; - protected final Collection tags; - protected final Collection players; - protected final Collection gamemodes; + // May be set after loading the whole context + protected Collection gamemodes; - protected SoftReference context; + // Must be set after loading the whole context + protected Collection tags = ImmutableSortedSet.of(); + protected Collection players = ImmutableList.of(); + protected SoftReference context = null; public MapInfoImpl(MapSource source, Element root) throws InvalidXMLException { this.source = source; @@ -107,8 +120,6 @@ public MapInfoImpl(MapSource source, Element root) throws InvalidXMLException { "difficulty", Difficulty.NORMAL) .ordinal(); - this.tags = new TreeSet<>(); - this.players = new ArrayList<>(); this.world = parseWorld(root); this.gamemode = XMLUtils.parseFormattedText(root, "game"); this.gamemodes = parseGamemodes(root); @@ -187,7 +198,7 @@ public int getDifficulty() { @Override public Collection getTags() { - return tags; + return Collections.unmodifiableCollection(tags); } @Override @@ -268,44 +279,37 @@ public Component getStyledName(MapNameStyle style) { } private static @NotNull List parseRules(Element root) { - List rules = new ArrayList<>(); - for (Element parent : root.getChildren("rules")) { - for (Element rule : parent.getChildren("rule")) { - rules.add(rule.getTextNormalize()); - } - } - return rules; + return XMLUtils.flattenElements(root, "rules", "rule").stream() + .map(Element::getTextNormalize) + .collect(StreamUtils.toImmutableList()); } private static @NotNull List parseGamemodes(Element root) throws InvalidXMLException { - List gamemodes = new ArrayList<>(); + ImmutableList.Builder gamemodes = ImmutableList.builder(); for (Element gamemodeEl : root.getChildren("gamemode")) { Gamemode gm = Gamemode.byId(gamemodeEl.getText()); if (gm == null) throw new InvalidXMLException("Unknown gamemode", gamemodeEl); gamemodes.add(gm); } - return gamemodes; + return gamemodes.build(); } private static @NotNull List parseContributors(Element root, String tag) throws InvalidXMLException { List contributors = new ArrayList<>(); - for (Element parent : root.getChildren(tag + "s")) { - for (Element child : parent.getChildren(tag)) { - String name = XMLUtils.getNormalizedNullableText(child); - UUID uuid = XMLUtils.parseUuid(Node.fromAttr(child, "uuid")); - String contribution = XMLUtils.getNullableAttribute(child, "contribution", "contrib"); - - if (name == null && uuid == null) { - throw new InvalidXMLException("Contributor must have either a name or UUID", child); - } - - if (uuid == null) { - contributors.add(new PseudonymContributor(name, contribution)); - } else { - contributors.add(new PlayerContributor(uuid, contribution)); - } + for (Element child : XMLUtils.flattenElements(root, tag + "s", tag)) { + String name = XMLUtils.getNormalizedNullableText(child); + UUID uuid = XMLUtils.parseUuid(Node.fromAttr(child, "uuid")); + String contribution = XMLUtils.getNullableAttribute(child, "contribution", "contrib"); + + if (name == null && uuid == null) { + throw new InvalidXMLException("Contributor must have either a name or UUID", child); } + + contributors.add( + uuid == null + ? new PseudonymContributor(name, contribution) + : new PlayerContributor(uuid, contribution)); } return contributors; } @@ -314,4 +318,43 @@ public Component getStyledName(MapNameStyle style) { final Element world = root.getChild("terrain"); return world == null ? new WorldInfoImpl() : new WorldInfoImpl(world); } + + protected void setContext(MapContextImpl context) { + // The first time context is loaded, set properties which can't be + // parsed until after modules are parsed, like team sizes or tags. + if (this.context == null) { + ImmutableSortedSet.Builder tags = ImmutableSortedSet.naturalOrder(); + ImmutableList.Builder players = ImmutableList.builder(); + + for (MapModule module : context.getModules()) { + tags.addAll(module.getTags()); + + if (module instanceof TeamModule) + players.addAll( + Iterables.transform(((TeamModule) module).getTeams(), TeamFactory::getMaxPlayers)); + + if (module instanceof FreeForAllModule) + players.add(((FreeForAllModule) module).getOptions().maxPlayers); + } + + if (world.hasTerrain()) tags.add(TERRAIN); + + this.tags = tags.build(); + this.players = players.build(); + + // If the map defines no game-modes manually, derive them from map tags, sorted by auxiliary + // last. + if (this.gamemodes.isEmpty()) { + this.gamemodes = + this.tags.stream() + .filter(MapTag::isGamemode) + .sorted( + Comparator.comparing(MapTag::isAuxiliary) + .thenComparing(Comparator.naturalOrder())) + .map(MapTag::getGamemode) + .collect(StreamUtils.toImmutableList()); + } + } + this.context = new SoftReference<>(context); + } } diff --git a/core/src/main/java/tc/oc/pgm/rage/RageModule.java b/core/src/main/java/tc/oc/pgm/rage/RageModule.java index 290f49cc9d..adc4b25072 100644 --- a/core/src/main/java/tc/oc/pgm/rage/RageModule.java +++ b/core/src/main/java/tc/oc/pgm/rage/RageModule.java @@ -4,6 +4,7 @@ import java.util.Collection; import java.util.logging.Logger; import org.jdom2.Document; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.map.factory.MapFactory; @@ -13,7 +14,7 @@ public class RageModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("rage", "Rage", true, true)); + ImmutableList.of(new MapTag("rage", Gamemode.RAGE, true)); @Override public RageMatchModule createMatchModule(Match match) { diff --git a/core/src/main/java/tc/oc/pgm/rotation/vote/book/VotingBookCreatorImpl.java b/core/src/main/java/tc/oc/pgm/rotation/vote/book/VotingBookCreatorImpl.java index de1f61ba98..21424ba2b5 100644 --- a/core/src/main/java/tc/oc/pgm/rotation/vote/book/VotingBookCreatorImpl.java +++ b/core/src/main/java/tc/oc/pgm/rotation/vote/book/VotingBookCreatorImpl.java @@ -4,14 +4,18 @@ import static net.kyori.adventure.text.event.ClickEvent.runCommand; import static net.kyori.adventure.text.event.HoverEvent.showText; +import java.util.Collection; import java.util.stream.Collectors; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapInfo; import tc.oc.pgm.api.map.MapTag; import tc.oc.pgm.api.player.MatchPlayer; +import tc.oc.pgm.util.text.TextFormatter; public class VotingBookCreatorImpl implements VotingBookCreator { @@ -27,12 +31,34 @@ public Component getMapBookComponent(MatchPlayer viewer, MapInfo map, boolean vo voted ? NamedTextColor.DARK_GREEN : NamedTextColor.DARK_RED)); text.append(text(" ").decoration(TextDecoration.BOLD, !voted)); // Fix 1px symbol diff text.append(text(map.getName(), NamedTextColor.GOLD, TextDecoration.BOLD)); - text.hoverEvent( - showText( - text( - map.getTags().stream().map(MapTag::toString).collect(Collectors.joining(" ")), - NamedTextColor.YELLOW))); + text.hoverEvent(showText(getHover(viewer, map, voted))); text.clickEvent(runCommand("/votenext -o " + map.getName())); return text.build(); } + + public ComponentLike getHover(MatchPlayer viewer, MapInfo map, boolean voted) { + TextComponent.Builder text = text(); + + Collection gamemodes = map.getGamemodes(); + + if (map.getGamemode() != null) { + text.append(map.getGamemode().colorIfAbsent(NamedTextColor.AQUA)).appendNewline(); + } else if (!gamemodes.isEmpty()) { + boolean acronyms = gamemodes.size() > 1; + text.append( + TextFormatter.list( + gamemodes.stream() + .map(gm -> text(acronyms ? gm.getAcronym() : gm.getFullName())) + .collect(Collectors.toList()), + NamedTextColor.AQUA)) + .appendNewline(); + } + + text.append( + text( + map.getTags().stream().map(MapTag::toString).collect(Collectors.joining(" ")), + NamedTextColor.YELLOW)); + + return text; + } } diff --git a/core/src/main/java/tc/oc/pgm/score/ScoreModule.java b/core/src/main/java/tc/oc/pgm/score/ScoreModule.java index e2b8c874ee..12adeb29c7 100644 --- a/core/src/main/java/tc/oc/pgm/score/ScoreModule.java +++ b/core/src/main/java/tc/oc/pgm/score/ScoreModule.java @@ -15,6 +15,7 @@ import org.jdom2.Element; import org.jetbrains.annotations.NotNull; import tc.oc.pgm.api.filter.Filter; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.map.MapTag; @@ -36,9 +37,8 @@ import tc.oc.pgm.util.xml.XMLUtils; public class ScoreModule implements MapModule { - private static final MapTag SCORE_TAG = - new MapTag("tdm", "deathmatch", "Deathmatch", true, false); - private static final MapTag BOX_TAG = new MapTag("scorebox", "Scorebox", false, true); + private static final MapTag SCORE_TAG = new MapTag("deathmatch", Gamemode.DEATHMATCH, false); + private static final MapTag BOX_TAG = new MapTag("scorebox", "Scorebox"); public ScoreModule(@NotNull ScoreConfig config, @NotNull Set scoreBoxFactories) { assertNotNull(config, "score config"); diff --git a/core/src/main/java/tc/oc/pgm/shops/ShopModule.java b/core/src/main/java/tc/oc/pgm/shops/ShopModule.java index 7ca5ebec08..5eb26495d7 100644 --- a/core/src/main/java/tc/oc/pgm/shops/ShopModule.java +++ b/core/src/main/java/tc/oc/pgm/shops/ShopModule.java @@ -52,8 +52,7 @@ public class ShopModule implements MapModule { - private static final Collection TAGS = - ImmutableList.of(new MapTag("shops", "Shops", false, true)); + private static final Collection TAGS = ImmutableList.of(new MapTag("shops", "Shops")); private final ImmutableMap shops; private final ImmutableSet shopKeepers; diff --git a/core/src/main/java/tc/oc/pgm/teams/TeamModule.java b/core/src/main/java/tc/oc/pgm/teams/TeamModule.java index 2023fa4b3c..65982cf418 100644 --- a/core/src/main/java/tc/oc/pgm/teams/TeamModule.java +++ b/core/src/main/java/tc/oc/pgm/teams/TeamModule.java @@ -48,9 +48,7 @@ public Collection getTags() { ImmutableList.of( new MapTag( size + "team" + (size == 1 ? "" : "s"), - size + " Team" + (size == 1 ? "" : "s"), - false, - true))); + size + " Team" + (size == 1 ? "" : "s")))); } @Override diff --git a/core/src/main/java/tc/oc/pgm/timelimit/TimeLimitModule.java b/core/src/main/java/tc/oc/pgm/timelimit/TimeLimitModule.java index 0edc4cb918..3dc15e2544 100644 --- a/core/src/main/java/tc/oc/pgm/timelimit/TimeLimitModule.java +++ b/core/src/main/java/tc/oc/pgm/timelimit/TimeLimitModule.java @@ -24,7 +24,7 @@ public class TimeLimitModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("timelimit", "Timelimit", false, true)); + ImmutableList.of(new MapTag("timelimit", "Timelimit")); private final @Nullable TimeLimit timeLimit; public TimeLimitModule(@Nullable TimeLimit limit) { diff --git a/core/src/main/java/tc/oc/pgm/tnt/TNTModule.java b/core/src/main/java/tc/oc/pgm/tnt/TNTModule.java index a5d95165e6..7bf5e2c256 100644 --- a/core/src/main/java/tc/oc/pgm/tnt/TNTModule.java +++ b/core/src/main/java/tc/oc/pgm/tnt/TNTModule.java @@ -25,7 +25,7 @@ public class TNTModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("autotnt", "Instant TNT", false, true)); + ImmutableList.of(new MapTag("autotnt", "Instant TNT")); public static final int DEFAULT_DISPENSER_NUKE_LIMIT = 16; public static final float DEFAULT_DISPENSER_NUKE_MULTIPLIER = 0.25f; diff --git a/core/src/main/java/tc/oc/pgm/wool/WoolModule.java b/core/src/main/java/tc/oc/pgm/wool/WoolModule.java index 832a2976c3..df21c445bf 100644 --- a/core/src/main/java/tc/oc/pgm/wool/WoolModule.java +++ b/core/src/main/java/tc/oc/pgm/wool/WoolModule.java @@ -10,6 +10,7 @@ import org.bukkit.util.Vector; import org.jdom2.Document; import org.jdom2.Element; +import tc.oc.pgm.api.map.Gamemode; import tc.oc.pgm.api.map.MapModule; import tc.oc.pgm.api.map.MapProtos; import tc.oc.pgm.api.map.MapTag; @@ -34,7 +35,7 @@ public class WoolModule implements MapModule { private static final Collection TAGS = - ImmutableList.of(new MapTag("ctw", "wool", "Capture the Wool", true, false)); + ImmutableList.of(new MapTag("wool", Gamemode.CAPTURE_THE_WOOL, false)); protected final Multimap woolFactories; diff --git a/core/src/main/java/tc/oc/pgm/worldborder/WorldBorderModule.java b/core/src/main/java/tc/oc/pgm/worldborder/WorldBorderModule.java index 388997e5ca..900e55363e 100644 --- a/core/src/main/java/tc/oc/pgm/worldborder/WorldBorderModule.java +++ b/core/src/main/java/tc/oc/pgm/worldborder/WorldBorderModule.java @@ -21,8 +21,7 @@ import tc.oc.pgm.util.xml.XMLUtils; public class WorldBorderModule implements MapModule { - private final Collection TAGS = - ImmutableList.of(new MapTag("border", "World Border", false, true)); + private final Collection TAGS = ImmutableList.of(new MapTag("border", "World Border")); private final List borders; public WorldBorderModule(List borders) { diff --git a/util/src/main/java/tc/oc/pgm/util/StreamUtils.java b/util/src/main/java/tc/oc/pgm/util/StreamUtils.java index dd5f15ec35..1424f930da 100644 --- a/util/src/main/java/tc/oc/pgm/util/StreamUtils.java +++ b/util/src/main/java/tc/oc/pgm/util/StreamUtils.java @@ -1,13 +1,25 @@ package tc.oc.pgm.util; +import com.google.common.collect.ImmutableList; import java.util.Iterator; import java.util.Spliterator; import java.util.Spliterators; +import java.util.stream.Collector; import java.util.stream.Stream; import java.util.stream.StreamSupport; public class StreamUtils { + private static final Collector> TO_IMMUTABLE_LIST = + Collector.of( + ImmutableList::builder, + ImmutableList.Builder::add, + (ImmutableList.Builder a, ImmutableList.Builder b) -> { + a.addAll(b.build()); + return a; + }, + ImmutableList.Builder::build); + public static Stream of(Iterable iterable) { return of(iterable.iterator()); } @@ -16,4 +28,9 @@ public static Stream of(Iterator iterator) { return StreamSupport.stream( Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED), false); } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static Collector> toImmutableList() { + return (Collector) TO_IMMUTABLE_LIST; + } }