From 39a2aee5a060cf63b90d56bbae103e942ef71b57 Mon Sep 17 00:00:00 2001 From: "Joseph T. McQuigg" Date: Mon, 24 Mar 2025 22:21:19 -0400 Subject: [PATCH] NeoForge Port --- build-logic/build.gradle.kts | 2 +- build-logic/settings.gradle.kts | 3 + fabric/build.gradle.kts | 6 +- .../resources/resources/tab.accesswidener | 2 +- jar/build.gradle.kts | 2 +- neoforge/build.gradle.kts | 33 +++ neoforge/gradle.properties | 1 + .../platforms/neoforge/NeoForgeBossBar.java | 64 ++++++ .../neoforge/NeoForgeEventListener.java | 37 ++++ .../neoforge/NeoForgePipelineInjector.java | 25 +++ .../platforms/neoforge/NeoForgePlatform.java | 193 ++++++++++++++++++ .../neoforge/NeoForgeScoreboard.java | 144 +++++++++++++ .../tab/platforms/neoforge/NeoForgeTAB.java | 34 +++ .../neoforge/NeoForgeTabCommand.java | 96 +++++++++ .../platforms/neoforge/NeoForgeTabList.java | 187 +++++++++++++++++ .../platforms/neoforge/NeoForgeTabPlayer.java | 83 ++++++++ .../resources/META-INF/accesstransformer.cfg | 4 + .../resources/META-INF/neoforge.mods.toml | 25 +++ settings.gradle.kts | 2 + 19 files changed, 936 insertions(+), 7 deletions(-) create mode 100644 neoforge/build.gradle.kts create mode 100644 neoforge/gradle.properties create mode 100644 neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeBossBar.java create mode 100644 neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeEventListener.java create mode 100644 neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgePipelineInjector.java create mode 100644 neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgePlatform.java create mode 100644 neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeScoreboard.java create mode 100644 neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTAB.java create mode 100644 neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabCommand.java create mode 100644 neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabList.java create mode 100644 neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabPlayer.java create mode 100644 neoforge/src/main/resources/META-INF/accesstransformer.cfg create mode 100644 neoforge/src/main/resources/META-INF/neoforge.mods.toml diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 0eecc1ad1..6046f585f 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -5,5 +5,5 @@ plugins { dependencies { implementation("com.gradleup.shadow:shadow-gradle-plugin:8.3.6") implementation("io.freefair.gradle:lombok-plugin:8.13") - implementation("fabric-loom:fabric-loom.gradle.plugin:1.10-SNAPSHOT") + implementation("dev.architectury.loom:dev.architectury.loom.gradle.plugin:1.9-SNAPSHOT") } diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index e66c93e2e..af381b29b 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -3,6 +3,9 @@ rootProject.name = "build-logic" dependencyResolutionManagement { repositories { maven("https://maven.fabricmc.net/") + maven("https://maven.architectury.dev/") + maven("https://maven.minecraftforge.net/") + maven("https://maven.neoforged.net/releases/") gradlePluginPortal() mavenCentral() } diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts index 5e04e3d5f..66a4e7e0b 100644 --- a/fabric/build.gradle.kts +++ b/fabric/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("fabric-loom") + id("dev.architectury.loom") } repositories { @@ -25,9 +25,7 @@ dependencies { modImplementation(fabricApi.module("fabric-command-api-v2", version)) } -loom { - accessWidenerPath.set(file("src/main/resources/resources/tab.accesswidener")) -} +loom.accessWidenerPath.set(file("src/main/resources/resources/tab.accesswidener")) tasks { compileJava { diff --git a/fabric/src/main/resources/resources/tab.accesswidener b/fabric/src/main/resources/resources/tab.accesswidener index a1a596990..652c56140 100644 --- a/fabric/src/main/resources/resources/tab.accesswidener +++ b/fabric/src/main/resources/resources/tab.accesswidener @@ -1,4 +1,4 @@ -accessWidener v1 named +accessWidener v2 named # Scoreboard packets accessible field net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket method I diff --git a/jar/build.gradle.kts b/jar/build.gradle.kts index b0ef1b862..f34e361b1 100644 --- a/jar/build.gradle.kts +++ b/jar/build.gradle.kts @@ -30,7 +30,7 @@ tasks { } registerPlatform(rootProject.projects.fabric.dependencyProject, rootProject.projects.fabric.dependencyProject.tasks.named("remapJar").get()) - + registerPlatform(rootProject.projects.neoforge.dependencyProject, rootProject.projects.neoforge.dependencyProject.tasks.named("remapJar").get()) } build { dependsOn(shadowJar) diff --git a/neoforge/build.gradle.kts b/neoforge/build.gradle.kts new file mode 100644 index 000000000..53bafc660 --- /dev/null +++ b/neoforge/build.gradle.kts @@ -0,0 +1,33 @@ +plugins { + id("dev.architectury.loom") +} + +repositories { + // Gradle doesn't support combining settings and project repositories, so we have to re-declare all the settings repos we need + maven("https://jitpack.io") // YamlAssist + maven("https://repo.opencollab.dev/maven-snapshots/") + maven("https://repo.viaversion.com/") + maven("https://oss.sonatype.org/content/repositories/snapshots") + maven("https://maven.nucleoid.xyz/") + maven("https://maven.neoforged.net/releases") + maven("https://maven.parchmentmc.org") +} + +@Suppress("UnstableApiUsage") +dependencies { + minecraft("com.mojang:minecraft:1.21.4") + mappings(loom.layered{ + officialMojangMappings() + parchment("org.parchmentmc.data:parchment-1.21.4:2025.03.23@zip") + }) + neoForge("net.neoforged:neoforge:21.4.123") + api(projects.shared) +} + +loom.neoForge.accessTransformer(file("src/main/resources/META-INF/accesstransformer.cfg")) + +tasks { + compileJava { + options.release.set(21) + } +} diff --git a/neoforge/gradle.properties b/neoforge/gradle.properties new file mode 100644 index 000000000..2914393db --- /dev/null +++ b/neoforge/gradle.properties @@ -0,0 +1 @@ +loom.platform=neoforge \ No newline at end of file diff --git a/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeBossBar.java b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeBossBar.java new file mode 100644 index 000000000..b24851870 --- /dev/null +++ b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeBossBar.java @@ -0,0 +1,64 @@ +package me.neznamy.tab.platforms.neoforge; + +import lombok.RequiredArgsConstructor; +import me.neznamy.tab.api.bossbar.BarColor; +import me.neznamy.tab.api.bossbar.BarStyle; +import me.neznamy.chat.component.TabComponent; +import me.neznamy.tab.shared.platform.decorators.SafeBossBar; +import net.minecraft.server.level.ServerBossEvent; +import net.minecraft.world.BossEvent.BossBarColor; +import net.minecraft.world.BossEvent.BossBarOverlay; +import org.jetbrains.annotations.NotNull; + +/** + * BossBar implementation for NeoForge using packets. + */ +@RequiredArgsConstructor +public class NeoForgeBossBar extends SafeBossBar { + + /** Player this BossBar belongs to */ + @NotNull + private final NeoForgeTabPlayer player; + + @Override + @NotNull + public ServerBossEvent constructBossBar(@NotNull TabComponent title, float progress, @NotNull BarColor color, @NotNull BarStyle style) { + ServerBossEvent bar = new ServerBossEvent( + title.convert(), + BossBarColor.valueOf(color.name()), + BossBarOverlay.valueOf(style.name()) + ); + bar.setProgress(progress); + return bar; + } + + @Override + public void create(@NotNull BossBarInfo bar) { + bar.getBossBar().addPlayer(player.getPlayer()); + } + + @Override + public void updateTitle(@NotNull BossBarInfo bar) { + bar.getBossBar().setName(bar.getTitle().convert()); + } + + @Override + public void updateProgress(@NotNull BossBarInfo bar) { + bar.getBossBar().setProgress(bar.getProgress()); + } + + @Override + public void updateStyle(@NotNull BossBarInfo bar) { + bar.getBossBar().setOverlay(BossBarOverlay.valueOf(bar.getStyle().name())); + } + + @Override + public void updateColor(@NotNull BossBarInfo bar) { + bar.getBossBar().setColor(BossBarColor.valueOf(bar.getColor().name())); + } + + @Override + public void remove(@NotNull BossBarInfo bar) { + bar.getBossBar().removePlayer(player.getPlayer()); + } +} diff --git a/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeEventListener.java b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeEventListener.java new file mode 100644 index 000000000..7de1753d0 --- /dev/null +++ b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeEventListener.java @@ -0,0 +1,37 @@ +package me.neznamy.tab.platforms.neoforge; + +import me.neznamy.tab.shared.TAB; +import me.neznamy.tab.shared.platform.EventListener; +import me.neznamy.tab.shared.platform.TabPlayer; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import org.jetbrains.annotations.NotNull; + +/** + * Event listener for NeoForge. + */ +public class NeoForgeEventListener implements EventListener { + + /** + * Registers all event listeners. + */ + public void register() { + IEventBus eventBus = NeoForge.EVENT_BUS; + eventBus.addListener((PlayerEvent.PlayerLoggedInEvent event) -> join((ServerPlayer) event.getEntity())); + eventBus.addListener((PlayerEvent.PlayerLoggedOutEvent event) -> quit(event.getEntity().getUUID())); + eventBus.addListener((PlayerEvent.PlayerRespawnEvent event) -> { + ServerPlayer player = (ServerPlayer) event.getEntity(); + replacePlayer(player.getUUID(), player); + worldChange(player.getUUID(), NeoForgeTAB.getLevelName(player.level())); + }); + eventBus.addListener((PlayerEvent.PlayerChangedDimensionEvent event) -> worldChange(event.getEntity().getUUID(), NeoForgeTAB.getLevelName(event.getEntity().level()))); + } + + @Override + @NotNull + public TabPlayer createPlayer(@NotNull ServerPlayer player) { + return new NeoForgeTabPlayer((NeoForgePlatform) TAB.getInstance().getPlatform(), player); + } +} diff --git a/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgePipelineInjector.java b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgePipelineInjector.java new file mode 100644 index 000000000..6cd8751c1 --- /dev/null +++ b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgePipelineInjector.java @@ -0,0 +1,25 @@ +package me.neznamy.tab.platforms.neoforge; + +import io.netty.channel.Channel; +import me.neznamy.tab.shared.features.injection.NettyPipelineInjector; +import me.neznamy.tab.shared.platform.TabPlayer; +import org.jetbrains.annotations.NotNull; + +/** + * Pipeline injector for Fabric. + */ +public class NeoForgePipelineInjector extends NettyPipelineInjector { + + /** + * Constructs new instance. + */ + public NeoForgePipelineInjector() { + super("packet_handler"); + } + + @Override + @NotNull + protected Channel getChannel(@NotNull TabPlayer player) { + return ((NeoForgeTabPlayer)player).getPlayer().connection.getConnection().channel(); + } +} diff --git a/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgePlatform.java b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgePlatform.java new file mode 100644 index 000000000..d513609f9 --- /dev/null +++ b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgePlatform.java @@ -0,0 +1,193 @@ +package me.neznamy.tab.platforms.neoforge; + +import com.mojang.logging.LogUtils; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import me.neznamy.chat.component.KeybindComponent; +import me.neznamy.chat.component.TabComponent; +import me.neznamy.chat.component.TextComponent; +import me.neznamy.chat.component.TranslatableComponent; +import me.neznamy.tab.shared.TAB; +import me.neznamy.tab.shared.TabConstants; +import me.neznamy.tab.shared.backend.BackendPlatform; +import me.neznamy.tab.shared.features.PerWorldPlayerListConfiguration; +import me.neznamy.tab.shared.features.injection.PipelineInjector; +import me.neznamy.tab.shared.features.types.TabFeature; +import me.neznamy.tab.shared.placeholders.expansion.EmptyTabExpansion; +import me.neznamy.tab.shared.placeholders.expansion.TabExpansion; +import me.neznamy.tab.shared.platform.BossBar; +import me.neznamy.tab.shared.platform.Scoreboard; +import me.neznamy.tab.shared.platform.TabList; +import me.neznamy.tab.shared.platform.TabPlayer; +import net.minecraft.SharedConstants; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; +import net.minecraft.network.chat.TextColor; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.loading.FMLPaths; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; + +/** + * Platform implementation for NeoForge + */ +@RequiredArgsConstructor +@Getter +public class NeoForgePlatform implements BackendPlatform { + + /** Minecraft server reference */ + private final MinecraftServer server; + + @Override + public void registerUnknownPlaceholder(@NotNull String identifier) { + registerDummyPlaceholder(identifier); + } + + @Override + public void loadPlayers() { + for (ServerPlayer player : getOnlinePlayers()) { + TAB.getInstance().addPlayer(new NeoForgeTabPlayer(this, player)); + } + } + + private Collection getOnlinePlayers() { + // It's nullable on startup + return server.getPlayerList() == null ? Collections.emptyList() : server.getPlayerList().getPlayers(); + } + + @Override + @NotNull + public PipelineInjector createPipelineInjector() { + return new NeoForgePipelineInjector(); + } + + @Override + @NotNull + public TabExpansion createTabExpansion() { + return new EmptyTabExpansion(); + } + + @Override + @Nullable + public TabFeature getPerWorldPlayerList(@NotNull PerWorldPlayerListConfiguration configuration) { + return null; + } + + @Override + public void logInfo(@NotNull TabComponent message) { + LogUtils.getLogger().info("[TAB] " + message.toRawText()); + } + + @Override + public void logWarn(@NotNull TabComponent message) { + LogUtils.getLogger().warn("[TAB] " + message.toRawText()); + } + + @Override + @NotNull + public String getServerVersionInfo() { + return "[Fabric] " + SharedConstants.getCurrentVersion().getName(); + } + + @Override + public void registerListener() { + new NeoForgeEventListener().register(); + } + + @Override + public void registerCommand() { + // Event listener must be registered in main class + } + + @Override + public void startMetrics() { + // Not available + } + + @Override + @NotNull + public File getDataFolder() { + return FMLPaths.CONFIGDIR.get().resolve(TabConstants.PLUGIN_ID).toFile(); + } + + @Override + @NotNull + public Component convertComponent(@NotNull TabComponent component) { + // Component type + MutableComponent nmsComponent; + if (component instanceof TextComponent) { + nmsComponent = Component.literal(((TextComponent) component).getText()); + } else if (component instanceof TranslatableComponent) { + nmsComponent = Component.translatable(((TranslatableComponent) component).getKey()); + } else if (component instanceof KeybindComponent) { + nmsComponent = Component.keybind(((KeybindComponent) component).getKeybind()); + } else { + throw new IllegalStateException("Unexpected component type: " + component.getClass().getName()); + } + + // Component style + nmsComponent.setStyle(new Style( + component.getModifier().getColor() == null ? null : TextColor.fromRgb(component.getModifier().getColor().getRgb()), + component.getModifier().getShadowColor(), + component.getModifier().getBold(), + component.getModifier().getItalic(), + component.getModifier().getUnderlined(), + component.getModifier().getStrikethrough(), + component.getModifier().getObfuscated(), + null, + null, + null, + component.getModifier().getFont() == null ? null : ResourceLocation.tryParse(component.getModifier().getFont()) + )); + + // Extra + for (TabComponent extra : component.getExtra()) { + nmsComponent.getSiblings().add(convertComponent(extra)); + } + + return nmsComponent; + } + + @Override + @NotNull + public Scoreboard createScoreboard(@NotNull TabPlayer player) { + return new NeoForgeScoreboard((NeoForgeTabPlayer) player); + } + + @Override + @NotNull + public BossBar createBossBar(@NotNull TabPlayer player) { + return new NeoForgeBossBar((NeoForgeTabPlayer) player); + } + + @Override + @NotNull + public TabList createTabList(@NotNull TabPlayer player) { + return new NeoForgeTabList((NeoForgeTabPlayer) player); + } + + @Override + public boolean supportsScoreboards() { + return true; + } + + @Override + public double getTPS() { + double mspt = getMSPT(); + if (mspt < 50) return 20; + return Math.round(1000 / mspt); + } + + @Override + public double getMSPT() { + return (float) server.getAverageTickTimeNanos() / 1000000; + } +} diff --git a/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeScoreboard.java b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeScoreboard.java new file mode 100644 index 000000000..5ce8ad6dd --- /dev/null +++ b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeScoreboard.java @@ -0,0 +1,144 @@ +package me.neznamy.tab.platforms.neoforge; + +import lombok.NonNull; +import me.neznamy.tab.shared.TAB; +import me.neznamy.tab.shared.platform.decorators.SafeScoreboard; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.numbers.FixedFormat; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.*; +import net.minecraft.world.scores.PlayerTeam; +import net.minecraft.world.scores.Scoreboard; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; +import net.minecraft.world.scores.criteria.ObjectiveCriteria.RenderType; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + * Scoreboard implementation for NeoForge using packets. + */ +public class NeoForgeScoreboard extends SafeScoreboard { + + private static final ChatFormatting[] formats = ChatFormatting.values(); + private static final net.minecraft.world.scores.Team.CollisionRule[] collisions = net.minecraft.world.scores.Team.CollisionRule.values(); + private static final net.minecraft.world.scores.Team.Visibility[] visibilities = net.minecraft.world.scores.Team.Visibility.values(); + private static final Scoreboard dummyScoreboard = new Scoreboard(); + + /** + * Constructs new instance with given player. + * + * @param player + * Player this scoreboard will belong to + */ + public NeoForgeScoreboard(NeoForgeTabPlayer player) { + super(player); + } + + @Override + public void registerObjective(@NonNull Objective objective) { + net.minecraft.world.scores.Objective obj = new net.minecraft.world.scores.Objective( + dummyScoreboard, + objective.getName(), + ObjectiveCriteria.DUMMY, + objective.getTitle().convert(), + RenderType.values()[objective.getHealthDisplay().ordinal()], + false, + objective.getNumberFormat() == null ? null : objective.getNumberFormat().toFixedFormat(FixedFormat::new) + ); + objective.setPlatformObjective(obj); + sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.REGISTER)); + sendPacket(new ClientboundSetDisplayObjectivePacket(net.minecraft.world.scores.DisplaySlot.values()[objective.getDisplaySlot().ordinal()], obj)); + } + + @Override + public void unregisterObjective(@NonNull Objective objective) { + sendPacket(new ClientboundSetObjectivePacket((net.minecraft.world.scores.Objective) objective.getPlatformObjective(), ObjectiveAction.UNREGISTER)); + } + + @Override + public void updateObjective(@NonNull Objective objective) { + net.minecraft.world.scores.Objective obj = (net.minecraft.world.scores.Objective) objective.getPlatformObjective(); + obj.setDisplayName(objective.getTitle().convert()); + obj.setRenderType(RenderType.values()[objective.getHealthDisplay().ordinal()]); + sendPacket(new ClientboundSetObjectivePacket(obj, ObjectiveAction.UPDATE)); + } + + @Override + public void setScore(@NonNull Score score) { + sendPacket(new ClientboundSetScorePacket( + score.getHolder(), + score.getObjective().getName(), + score.getValue(), + Optional.ofNullable(score.getDisplayName() == null ? null : score.getDisplayName().convert()), + Optional.ofNullable(score.getNumberFormat() == null ? null : score.getNumberFormat().toFixedFormat(FixedFormat::new)) + )); + } + + @Override + public void removeScore(@NonNull Score score) { + sendPacket(new ClientboundResetScorePacket(score.getHolder(), score.getObjective().getName())); + } + + @Override + @NotNull + public Object createTeam(@NonNull String name) { + return new PlayerTeam(dummyScoreboard, name); + } + + @Override + public void registerTeam(@NonNull Team team) { + updateTeamProperties(team); + PlayerTeam t = (PlayerTeam) team.getPlatformTeam(); + t.getPlayers().addAll(team.getPlayers()); + sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(t, true)); + } + + @Override + public void unregisterTeam(@NonNull Team team) { + sendPacket(ClientboundSetPlayerTeamPacket.createRemovePacket((PlayerTeam) team.getPlatformTeam())); + } + + @Override + public void updateTeam(@NonNull Team team) { + updateTeamProperties(team); + sendPacket(ClientboundSetPlayerTeamPacket.createAddOrModifyPacket((PlayerTeam) team.getPlatformTeam(), false)); + } + + private void updateTeamProperties(@NonNull Team team) { + PlayerTeam t = (PlayerTeam) team.getPlatformTeam(); + t.setAllowFriendlyFire((team.getOptions() & 0x01) != 0); + t.setSeeFriendlyInvisibles((team.getOptions() & 0x02) != 0); + t.setColor(formats[team.getColor().getLegacyColor().ordinal()]); + t.setCollisionRule(collisions[team.getCollision().ordinal()]); + t.setNameTagVisibility(visibilities[team.getVisibility().ordinal()]); + t.setPlayerPrefix(team.getPrefix().convert()); + t.setPlayerSuffix(team.getSuffix().convert()); + } + + @Override + public void onPacketSend(@NonNull Object packet) { + if (isAntiOverrideScoreboard()) { + if (packet instanceof ClientboundSetDisplayObjectivePacket display) { + TAB.getInstance().getFeatureManager().onDisplayObjective(player, display.getSlot().ordinal(), display.getObjectiveName()); + } + if (packet instanceof ClientboundSetObjectivePacket objective) { + TAB.getInstance().getFeatureManager().onObjective(player, objective.getMethod(), objective.getObjectiveName()); + } + } + if (isAntiOverrideTeams() && packet instanceof ClientboundSetPlayerTeamPacket team) { + if (team.method == TeamAction.UPDATE) return; + team.players = onTeamPacket(team.method, team.getName(), team.getPlayers()); + } + } + + /** + * Sends the packet to the player. + * + * @param packet + * Packet to send + */ + private void sendPacket(@NotNull Packet packet) { + player.getPlayer().connection.send(packet); + } +} diff --git a/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTAB.java b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTAB.java new file mode 100644 index 000000000..716125216 --- /dev/null +++ b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTAB.java @@ -0,0 +1,34 @@ +package me.neznamy.tab.platforms.neoforge; + +import me.neznamy.tab.shared.TAB; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ServerLevelData; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import net.neoforged.neoforge.event.server.ServerStartingEvent; +import net.neoforged.neoforge.event.server.ServerStoppingEvent; +import org.jetbrains.annotations.NotNull; + +@Mod(value = "tab", dist = Dist.DEDICATED_SERVER) +public class NeoForgeTAB { + + public NeoForgeTAB(final IEventBus eventBus) { + IEventBus EVENT_BUS = NeoForge.EVENT_BUS; + EVENT_BUS.addListener((RegisterCommandsEvent event) -> new NeoForgeTabCommand().onRegisterCommands(event.getDispatcher())); + EVENT_BUS.addListener((ServerStartingEvent event) -> TAB.create(new NeoForgePlatform(event.getServer()))); + EVENT_BUS.addListener((ServerStoppingEvent event) -> TAB.getInstance().unload()); + } + + @NotNull + public static String getLevelName(@NotNull Level level) { + String path = level.dimension().location().getPath(); + return ((ServerLevelData)level.getLevelData()).getLevelName() + switch (path) { + case "overworld" -> ""; // No suffix for overworld + case "the_nether" -> "_nether"; + default -> "_" + path; // End + default behavior for other dimensions created by mods + }; + } +} diff --git a/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabCommand.java b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabCommand.java new file mode 100644 index 000000000..e71e3e185 --- /dev/null +++ b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabCommand.java @@ -0,0 +1,96 @@ +package me.neznamy.tab.platforms.neoforge; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import me.neznamy.tab.shared.TAB; +import me.neznamy.tab.shared.TabConstants; +import me.neznamy.chat.component.TabComponent; +import me.neznamy.tab.shared.platform.TabPlayer; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +/** + * Command handler for plugin's command for Fabric. + */ +public class NeoForgeTabCommand { + + /** + * Handles command register request from server and registers own command. + * + * @param dispatcher + * Dispatcher to register command to + */ + public void onRegisterCommands(@NotNull CommandDispatcher dispatcher) { + LiteralCommandNode command = Commands.literal("tab") // TODO extract it from Platform somehow + .executes(context -> executeCommand(context.getSource(), new String[0])) + .build(); + ArgumentCommandNode args = Commands.argument("args", StringArgumentType.greedyString()) + .suggests((context, builder) -> getSuggestions(context.getSource(), getArguments(context), builder)) + .executes(context -> executeCommand(context.getSource(), getArguments(context))) + .build(); + command.addChild(args); + dispatcher.getRoot().addChild(command); + } + + @NotNull + private String[] getArguments(@NotNull CommandContext context) { + String input = context.getInput(); + int firstSpace = input.indexOf(' '); + if (firstSpace == -1) return new String[0]; + String rawArgs = input.substring(firstSpace + 1); + String[] args = rawArgs.split(" "); + if (rawArgs.endsWith(" ")) { + args = Arrays.copyOf(args, args.length+1); + args[args.length-1] = ""; + } + return args; + } + + @SuppressWarnings("SameReturnValue") // Unused by plugin + private int executeCommand(@NotNull CommandSourceStack source, @NotNull String[] args) { + if (TAB.getInstance().isPluginDisabled()) { + boolean hasReloadPermission = true; //PermissionsAPIHook.hasPermission(source, TabConstants.Permission.COMMAND_RELOAD); + boolean hasAdminPermission = true; //PermissionsAPIHook.hasPermission(source, TabConstants.Permission.COMMAND_ALL); + for (String message : TAB.getInstance().getDisabledCommand().execute(args, hasReloadPermission, hasAdminPermission)) { + source.sendSystemMessage(TabComponent.fromColoredText(message).convert()); + } + } else { + if (source.getEntity() == null) { + TAB.getInstance().getCommand().execute(null, args); + } else { + TabPlayer player = TAB.getInstance().getPlayer(source.getEntity().getUUID()); + if (player != null) TAB.getInstance().getCommand().execute(player, args); + } + } + return 0; + } + + @NotNull + private CompletableFuture getSuggestions(@NotNull CommandSourceStack source, @NotNull String[] args, + @NotNull SuggestionsBuilder builder) { + TabPlayer player = null; + if (source.getEntity() != null) { + player = TAB.getInstance().getPlayer(source.getEntity().getUUID()); + if (player == null) return Suggestions.empty(); + } + + SuggestionsBuilder newBuilder = builder; + int lastSpace = newBuilder.getRemaining().lastIndexOf(' '); + if (lastSpace != -1) { + newBuilder = newBuilder.createOffset(lastSpace + 1 + newBuilder.getStart()); + } + for (String suggestion : TAB.getInstance().getCommand().complete(player, args)) { + newBuilder.suggest(suggestion); + } + return newBuilder.buildFuture(); + } +} diff --git a/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabList.java b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabList.java new file mode 100644 index 000000000..c92d1dc39 --- /dev/null +++ b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabList.java @@ -0,0 +1,187 @@ +package me.neznamy.tab.platforms.neoforge; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import lombok.NonNull; +import me.neznamy.chat.component.TabComponent; +import me.neznamy.tab.shared.TAB; +import me.neznamy.tab.shared.platform.TabList; +import me.neznamy.tab.shared.platform.decorators.TrackedTabList; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundTabListPacket; +import net.minecraft.world.level.GameType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * TabList implementation for Fabric using packets. + */ +public class NeoForgeTabList extends TrackedTabList { + + private static final EnumSet addPlayer = EnumSet.allOf(ClientboundPlayerInfoUpdatePacket.Action.class); + private static final EnumSet updateDisplayName = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME); + private static final EnumSet updateLatency = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY); + private static final EnumSet updateGameMode = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE); + private static final EnumSet updateListed = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED); + private static final EnumSet updateListOrder = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER); + private static final EnumSet updateHat = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT); + + /** + * Constructs new instance. + * + * @param player + * Player this tablist will belong to + */ + public NeoForgeTabList(@NotNull NeoForgeTabPlayer player) { + super(player); + } + + @Override + public void removeEntry(@NonNull UUID entry) { + sendPacket(new ClientboundPlayerInfoRemovePacket(Collections.singletonList(entry))); + } + + @Override + public void updateDisplayName0(@NonNull UUID entry, @Nullable TabComponent displayName) { + sendPacket(updateDisplayName, entry, "", null, false, 0, 0, displayName, 0, false); + } + + @Override + public void updateLatency(@NonNull UUID entry, int latency) { + sendPacket(updateLatency, entry, "", null, false, latency, 0, null, 0, false); + } + + @Override + public void updateGameMode(@NonNull UUID entry, int gameMode) { + sendPacket(updateGameMode, entry, "", null, false, 0, gameMode, null, 0, false); + } + + @Override + public void updateListed(@NonNull UUID entry, boolean listed) { + sendPacket(updateListed, entry, "", null, listed, 0, 0, null, 0, false); + } + + @Override + public void updateListOrder(@NonNull UUID entry, int listOrder) { + sendPacket(updateListOrder, entry, "", null, false, 0, 0, null, listOrder, false); + } + + @Override + public void updateHat(@NonNull UUID entry, boolean showHat) { + sendPacket(updateHat, entry, "", null, false, 0, 0, null, 0, showHat); + } + + @Override + public void addEntry0(@NonNull Entry entry) { + sendPacket(addPlayer, entry.getUniqueId(), entry.getName(), entry.getSkin(), entry.isListed(), entry.getLatency(), + entry.getGameMode(), entry.getDisplayName(), entry.getListOrder(), entry.isShowHat()); + } + + @Override + public void setPlayerListHeaderFooter(@NonNull TabComponent header, @NonNull TabComponent footer) { + sendPacket(new ClientboundTabListPacket(header.convert(), footer.convert())); + } + + @Override + public boolean containsEntry(@NonNull UUID entry) { + return true; // TODO? + } + + @Override + @Nullable + public Skin getSkin() { + Collection properties = player.getPlayer().getGameProfile().getProperties().get(TEXTURES_PROPERTY); + if (properties.isEmpty()) return null; // Offline mode + Property property = properties.iterator().next(); + return new Skin(property.value(), property.signature()); + } + + @Override + public void onPacketSend(@NonNull Object packet) { + if (packet instanceof ClientboundPlayerInfoUpdatePacket info) { + EnumSet actions = info.actions(); + List updatedList = new ArrayList<>(); + boolean rewritePacket = false; + for (ClientboundPlayerInfoUpdatePacket.Entry nmsData : info.entries()) { + boolean rewriteEntry = false; + Component displayName = nmsData.displayName(); + int latency = nmsData.latency(); + if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME)) { + TabComponent expectedDisplayName = getExpectedDisplayNames().get(nmsData.profileId()); + if (expectedDisplayName != null && expectedDisplayName.convert() != displayName) { + displayName = expectedDisplayName.convert(); + rewriteEntry = rewritePacket = true; + } + } + if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY)) { + int newLatency = TAB.getInstance().getFeatureManager().onLatencyChange(player, nmsData.profileId(), latency); + if (newLatency != latency) { + latency = newLatency; + rewriteEntry = rewritePacket = true; + } + } + if (actions.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER)) { + TAB.getInstance().getFeatureManager().onEntryAdd(player, nmsData.profileId(), nmsData.profile().getName()); + } + updatedList.add(rewriteEntry ? new ClientboundPlayerInfoUpdatePacket.Entry( + nmsData.profileId(), nmsData.profile(), nmsData.listed(), latency, nmsData.gameMode(), displayName, + nmsData.showHat(), nmsData.listOrder(), nmsData.chatSession() + ) : nmsData); + } + if (rewritePacket) info.entries = updatedList; + } + } + + private void sendPacket(@NonNull EnumSet action, @NonNull UUID id, @NonNull String name, @Nullable Skin skin, + boolean listed, int latency, int gameMode, @Nullable TabComponent displayName, int listOrder, boolean showHat) { + ClientboundPlayerInfoUpdatePacket packet = new ClientboundPlayerInfoUpdatePacket(action, Collections.emptyList()); + packet.entries = Collections.singletonList(new ClientboundPlayerInfoUpdatePacket.Entry( + id, + action.contains(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER) ? createProfile(id, name, skin) : null, + listed, + latency, + GameType.byId(gameMode), + displayName == null ? null : displayName.convert(), + showHat, + listOrder, + null + )); + sendPacket(packet); + } + + /** + * Creates GameProfile from given parameters. + * + * @param id + * Profile ID + * @param name + * Profile name + * @param skin + * Player skin + * @return GameProfile from given parameters + */ + @NotNull + private GameProfile createProfile(@NonNull UUID id, @NonNull String name, @Nullable Skin skin) { + GameProfile profile = new GameProfile(id, name); + if (skin != null) { + profile.getProperties().put(TabList.TEXTURES_PROPERTY, + new Property(TabList.TEXTURES_PROPERTY, skin.getValue(), skin.getSignature())); + } + return profile; + } + + /** + * Sends the packet to the player. + * + * @param packet + * Packet to send + */ + private void sendPacket(@NotNull Packet packet) { + player.getPlayer().connection.send(packet); + } +} diff --git a/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabPlayer.java b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabPlayer.java new file mode 100644 index 000000000..af56fd2a1 --- /dev/null +++ b/neoforge/src/main/java/me/neznamy/tab/platforms/neoforge/NeoForgeTabPlayer.java @@ -0,0 +1,83 @@ +package me.neznamy.tab.platforms.neoforge; + +import me.neznamy.chat.component.TabComponent; +import me.neznamy.tab.shared.backend.BackendTabPlayer; +import net.minecraft.SharedConstants; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; + +/** + * TabPlayer implementation for NeoForge. + */ +public class NeoForgeTabPlayer extends BackendTabPlayer { + + /** + * Constructs new instance with given parameters. + * + * @param platform + * Server platform + * @param player + * Platform's player object + */ + public NeoForgeTabPlayer(@NotNull NeoForgePlatform platform, @NotNull ServerPlayer player) { + super(platform, player, player.getUUID(), player.getGameProfile().getName(), + NeoForgeTAB.getLevelName(player.level()), SharedConstants.getProtocolVersion()); + } + + @Override + public boolean hasPermission(@NotNull String permission) { + return true; // PermissionsAPIHook.hasPermission(getPlayer(), permission); + } + + @Override + public int getPing() { + return getPlayer().connection.latency(); + } + + @Override + public void sendMessage(@NotNull TabComponent message) { + getPlayer().sendSystemMessage(message.convert()); + } + + @Override + public boolean hasInvisibilityPotion() { + return false; + } + + @Override + public boolean isDisguised() { + return false; + } + + @Override + @NotNull + public ServerPlayer getPlayer() { + return (ServerPlayer) player; + } + + @Override + public NeoForgePlatform getPlatform() { + return (NeoForgePlatform) platform; + } + + @Override + public boolean isVanished0() { + return false; + } + + @Override + public int getGamemode() { + return getPlayer().gameMode.getGameModeForPlayer().getId(); + } + + @Override + public double getHealth() { + return getPlayer().getHealth(); + } + + @Override + @NotNull + public String getDisplayName() { + return getPlayer().getGameProfile().getName(); // Will make it work properly if someone asks + } +} diff --git a/neoforge/src/main/resources/META-INF/accesstransformer.cfg b/neoforge/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 000000000..d3f7ecb2e --- /dev/null +++ b/neoforge/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,4 @@ +public net.minecraft.network.chat.Style (Lnet/minecraft/network/chat/TextColor;Ljava/lang/Integer;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/Boolean;Lnet/minecraft/network/chat/ClickEvent;Lnet/minecraft/network/chat/HoverEvent;Ljava/lang/String;Lnet/minecraft/resources/ResourceLocation;)V # constructor +public-f net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket entries # entries +public-f net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket method # method +public-f net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket players # players \ No newline at end of file diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 000000000..1b5f5b4ca --- /dev/null +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,25 @@ +modLoader = "javafml" +loaderVersion = "[4,)" +issueTrackerURL = "" +license = "All Rights Reserved" + +[[mods]] +modId = "tab" +version = "@version@" +displayName = "@name@" +authors = "@author@" +description = '''@description@''' + +[[dependencies.tab]] +modId = "neoforge" +type = "required" +versionRange = "[21.4.121,)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.tab]] +modId = "minecraft" +type = "required" +versionRange = "[1.21.4,)" +ordering = "NONE" +side = "BOTH" diff --git a/settings.gradle.kts b/settings.gradle.kts index 46add9a80..d9bd984f1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,6 +20,7 @@ pluginManagement { includeBuild("build-logic") repositories { maven("https://repo.spongepowered.org/repository/maven-public/") + maven("https://maven.architectury.dev/") gradlePluginPortal() mavenCentral() } @@ -38,4 +39,5 @@ include(":bungeecord") include(":component") include(":sponge") include(":fabric") +include(":neoforge") include(":jar")