From 6be6ce5bf3b535badeb08e57c285c97c1cdda580 Mon Sep 17 00:00:00 2001
From: Cloudfl4re
Date: Sun, 2 Aug 2026 23:57:13 +0800
Subject: [PATCH 01/23] =?UTF-8?q?=E7=A5=9E=E7=A7=98=E5=B0=8F=E4=B8=9C?=
=?UTF-8?q?=E8=A5=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 4f4ea79..7246ba0 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+ [](https://www.codefactor.io/repository/github/mcxqk/videoplayer)
From 025b43884e233168ef76b75ce81f59c4b4db076d Mon Sep 17 00:00:00 2001
From: Cloudfl4re
Date: Sun, 2 Aug 2026 23:58:17 +0800
Subject: [PATCH 02/23] =?UTF-8?q?=E7=A5=9E=E7=A7=98=E5=B0=8F=E4=B8=9C?=
=?UTF-8?q?=E8=A5=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
有点笨(
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 7246ba0..9fa6540 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
- [](https://www.codefactor.io/repository/github/mcxqk/videoplayer)
+
From 665e80875c782500c840889d9bbf74f38f884c6b Mon Sep 17 00:00:00 2001
From: cloudfl4re
Date: Mon, 3 Aug 2026 11:08:05 +0800
Subject: [PATCH 03/23] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=202.0.2=20=E7=89=88?=
=?UTF-8?q?=E6=9C=AC=E5=8F=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index b7f3aeb..f870ac5 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,7 +10,7 @@ loader_version=0.17.3
loom_version=1.17.12
# Mod Properties
-mod_version=2.0.1
+mod_version=2.0.2
maven_group=com.github.squi2rel.vp
archives_base_name=VideoPlayer
From c05fe684b6bf843e6d1478ae321b8afb9351c409 Mon Sep 17 00:00:00 2001
From: cloudfl4re
Date: Mon, 3 Aug 2026 17:27:22 +0800
Subject: [PATCH 04/23] =?UTF-8?q?=E5=85=BC=E5=AE=B9=202.0.1=20=E5=AE=A2?=
=?UTF-8?q?=E6=88=B7=E7=AB=AF=E8=A7=86=E9=A2=91=E5=88=97=E8=A1=A8=E5=8D=8F?=
=?UTF-8?q?=E8=AE=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../com/github/squi2rel/vp/DataHolder.java | 49 ++++++++++++++--
.../squi2rel/vp/VideoPlayerPaperPlugin.java | 2 +
.../vp/network/ServerPacketHandler.java | 30 +++++++---
.../squi2rel/vp/video/ScreenBroadcaster.java | 17 ++++++
.../vp/VideoHandshakeRejectionTest.java | 16 +++++
.../ServerPacketHandlerEarlyReturnTest.java | 35 +++++++++++
.../squi2rel/vp/ClientPacketHandler.java | 22 +++++--
.../github/squi2rel/vp/VideoPlayerClient.java | 2 +-
.../com/github/squi2rel/vp/DataHolder.java | 39 +++++++++++--
.../vp/network/ServerPacketHandler.java | 25 +++++---
.../squi2rel/vp/network/VideoPackets.java | 58 ++++++++++++++++++-
.../squi2rel/vp/network/VideoProtocol.java | 44 +++++++++++++-
.../squi2rel/vp/video/PlaybackController.java | 2 +-
.../squi2rel/vp/video/ScreenBroadcaster.java | 21 +++++++
.../github/squi2rel/vp/video/VideoScreen.java | 26 +++++++++
.../assets/videoplayer/lang/en_us.json | 2 +-
.../assets/videoplayer/lang/zh_cn.json | 2 +-
.../network/IdlePlayMutationPacketTest.java | 35 +++++++++++
.../vp/network/VideoProtocolTest.java | 38 ++++++++----
19 files changed, 418 insertions(+), 47 deletions(-)
diff --git a/paper-plugin/src/main/java/com/github/squi2rel/vp/DataHolder.java b/paper-plugin/src/main/java/com/github/squi2rel/vp/DataHolder.java
index 7ad7174..9f8a83c 100644
--- a/paper-plugin/src/main/java/com/github/squi2rel/vp/DataHolder.java
+++ b/paper-plugin/src/main/java/com/github/squi2rel/vp/DataHolder.java
@@ -753,7 +753,8 @@ public static boolean sendResetHandshake(Player player) {
handshakes.put(uuid, VideoHandshakeState.RESET_SENT);
handshakeNonces.put(uuid, nonce);
allPlayers.remove(uuid);
- reset = VideoPackets.resetClient(handshakeTokenLocked(uuid), config, nonce);
+ String token = handshakeTokens.computeIfAbsent(uuid, ignored -> VideoProtocol.legacyToken());
+ reset = VideoPackets.resetClient(token, config, nonce);
}
sendTo(player, reset);
return true;
@@ -1132,11 +1133,13 @@ public static VideoHandshakeState acceptHandshake(UUID uuid) {
}
}
- public static void recordHandshakeToken(UUID uuid, String remoteToken) {
- if (uuid == null) return;
+ public static boolean recordHandshakeToken(UUID uuid, String remoteToken) {
+ if (uuid == null) return false;
synchronized (LOCK) {
- handshakeTokens.put(uuid, VideoProtocol.responseToken(VideoPlayerMain.version, remoteToken));
+ String token = VideoProtocol.responseToken(VideoPlayerMain.version, remoteToken);
+ String previous = handshakeTokens.put(uuid, token);
cancelReloadHandshakeLocked(uuid);
+ return previous == null || !previous.equals(token);
}
}
@@ -1213,6 +1216,40 @@ public static boolean protocolActive(UUID uuid) {
}
}
+ public static boolean supportsClientPlaybackReporting(UUID uuid) {
+ synchronized (LOCK) {
+ return handshakes.get(uuid) == VideoHandshakeState.ACTIVE
+ && VideoProtocol.supportsClientPlaybackReporting(handshakeTokenLocked(uuid));
+ }
+ }
+
+ public static boolean supportsIdlePlayMutations(UUID uuid) {
+ synchronized (LOCK) {
+ return handshakes.get(uuid) == VideoHandshakeState.ACTIVE
+ && VideoProtocol.supportsIdlePlayMutations(handshakeTokenLocked(uuid));
+ }
+ }
+
+ public static void refreshPlayerProtocol(Player player) {
+ if (player == null) return;
+ synchronized (LOCK) {
+ UUID uuid = player.getUniqueId();
+ if (handshakes.get(uuid) != VideoHandshakeState.ACTIVE) return;
+ boolean mutations = VideoProtocol.supportsIdlePlayMutations(handshakeTokenLocked(uuid));
+ for (HashMap world : areas.values()) {
+ for (VideoArea area : world.values()) {
+ if (!area.containsPlayer(uuid)) continue;
+ for (VideoScreen screen : area.screens) {
+ screen.addPlayer(uuid);
+ if (!screen.idlePlayEntries.isEmpty() || screen.idlePlayRandom) {
+ sendToCurrentThread(player, VideoPackets.idlePlay(screen, mutations));
+ }
+ }
+ }
+ }
+ }
+ }
+
public static VideoScreen findScreen(ScreenKey key) {
if (key == null) return null;
synchronized (LOCK) {
@@ -1257,7 +1294,9 @@ private static void sendAreaSnapshot(Player player, VideoArea area) {
sendToCurrentThread(player, VideoPackets.setMetadata(screen, entry.getKey(), entry.getValue()));
}
if (!screen.idlePlayEntries.isEmpty() || screen.idlePlayRandom) {
- sendToCurrentThread(player, VideoPackets.idlePlay(screen));
+ sendToCurrentThread(player, VideoPackets.idlePlay(
+ screen, supportsIdlePlayMutations(player.getUniqueId())
+ ));
}
}
boolean loadedPlayback = false;
diff --git a/paper-plugin/src/main/java/com/github/squi2rel/vp/VideoPlayerPaperPlugin.java b/paper-plugin/src/main/java/com/github/squi2rel/vp/VideoPlayerPaperPlugin.java
index fffd702..44b70c2 100644
--- a/paper-plugin/src/main/java/com/github/squi2rel/vp/VideoPlayerPaperPlugin.java
+++ b/paper-plugin/src/main/java/com/github/squi2rel/vp/VideoPlayerPaperPlugin.java
@@ -14,6 +14,7 @@
import org.bukkit.command.PluginIdentifiableCommand;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
+import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
@@ -103,6 +104,7 @@ public void onDisable() {
nativeRuntime.stop();
nativeRuntime = null;
}
+ HandlerList.unregisterAll((Listener) this);
VideoPermissions.reset();
getServer().getMessenger().unregisterIncomingPluginChannel(this, CHANNEL, this);
getServer().getMessenger().unregisterOutgoingPluginChannel(this, CHANNEL);
diff --git a/paper-plugin/src/main/java/com/github/squi2rel/vp/network/ServerPacketHandler.java b/paper-plugin/src/main/java/com/github/squi2rel/vp/network/ServerPacketHandler.java
index 8d5406d..bb1cde1 100644
--- a/paper-plugin/src/main/java/com/github/squi2rel/vp/network/ServerPacketHandler.java
+++ b/paper-plugin/src/main/java/com/github/squi2rel/vp/network/ServerPacketHandler.java
@@ -75,13 +75,13 @@ private static void handleLocked(Player player, ByteBuf buf, long receivedAt) {
sendTo(player, VideoPackets.protocolReject(VideoPlayerMain.version));
reject(player, VpTranslation.of(
"error.videoplayer.protocol_mismatch",
- "VideoPlayer client and server must use the same release version. Client: %s, server: %s",
+ "VideoPlayer client version %s is not compatible with server %s",
VideoProtocol.displayVersion(remoteToken), VideoPlayerMain.version
));
}
return;
}
- DataHolder.recordHandshakeToken(player.getUniqueId(), remoteToken);
+ boolean protocolChanged = DataHolder.recordHandshakeToken(player.getUniqueId(), remoteToken);
String responseToken = DataHolder.handshakeToken(player.getUniqueId());
VideoHandshakeState previous = DataHolder.handshakeState(player.getUniqueId());
if (previous == VideoHandshakeState.NEEDS_RESET) {
@@ -93,8 +93,14 @@ private static void handleLocked(Player player, ByteBuf buf, long receivedAt) {
if (nonce == 0L) nonce = DataHolder.issueHandshakeNonce(player.getUniqueId());
sendTo(player, VideoPackets.resetClient(responseToken, DataHolder.config, nonce));
} else if (previous == VideoHandshakeState.ACTIVE) {
- sendTo(player, VideoPackets.config(responseToken, DataHolder.config));
+ byte[] config = VideoPackets.config(responseToken, DataHolder.config);
+ if (protocolChanged) {
+ DataHolder.sendToCurrentThread(player, config);
+ } else {
+ sendTo(player, config);
+ }
sendGlobalPermissions(player);
+ if (protocolChanged) DataHolder.refreshPlayerProtocol(player);
Throwable backendError = PaperNativeRuntime.currentError();
if (backendError != null) {
message(player, nativeBackendUnavailableMessage(backendError));
@@ -104,7 +110,9 @@ private static void handleLocked(Player player, ByteBuf buf, long receivedAt) {
case HANDSHAKE_ACK -> {
long nonce = buf.readLong();
if (DataHolder.acceptHandshakeAck(player.getUniqueId(), nonce)) {
- sendTo(player, VideoPackets.config(DataHolder.handshakeToken(player.getUniqueId()), DataHolder.config));
+ DataHolder.sendToCurrentThread(player, VideoPackets.config(
+ DataHolder.handshakeToken(player.getUniqueId()), DataHolder.config
+ ));
sendGlobalPermissions(player);
Throwable backendError = PaperNativeRuntime.currentError();
if (backendError != null) {
@@ -444,7 +452,9 @@ url, new PlayerProviderSource(player, bilibiliLimit, youtubeLimit)
case IDLE_PLAY -> handleRequest(player, buf, reply -> {
String areaName = VideoPackets.readName(buf);
String screenName = VideoPackets.readName(buf);
- IdlePlayMutation mutation = VideoPackets.readIdlePlayMutation(buf);
+ boolean mutations = DataHolder.supportsIdlePlayMutations(player.getUniqueId());
+ IdlePlayMutation mutation = mutations ? VideoPackets.readIdlePlayMutation(buf) : null;
+ VideoPackets.LegacyIdlePlayConfig legacy = mutations ? null : VideoPackets.readLegacyIdlePlayConfig(buf);
reply.decoded();
VideoArea area = getArea(player, areaName);
if (area == null) {
@@ -457,21 +467,25 @@ url, new PlayerProviderSource(player, bilibiliLimit, youtubeLimit)
return;
}
if (!requirePermission(player, reply, VideoPermissionAction.SET_IDLE_PLAY, VideoPermissionContext.screen(screen))) return;
- if (mutation.action() == IdlePlayAction.ADD) {
+ if (mutation != null && mutation.action() == IdlePlayAction.ADD) {
mutation = IdlePlayMutation.add(VideoUrlNormalizer.normalizeSubmittedUrl(mutation.url()), mutation.priority());
if (!VideoScreen.validIdlePlayUrl(mutation.url())) {
reply.error(VpTranslation.of("error.videoplayer.idle_play_url_invalid", "IdlePlay URL is invalid"));
return;
}
}
- if (!screen.applyIdlePlayMutation(mutation, player.getUniqueId(), player.getName())) {
+ if (legacy != null) {
+ screen.replaceLegacyIdlePlayConfig(
+ legacy.urls(), legacy.random(), player.getUniqueId(), player.getName()
+ );
+ } else if (!screen.applyIdlePlayMutation(mutation, player.getUniqueId(), player.getName())) {
reply.error(VpTranslation.of("error.videoplayer.idle_play_mutation_failed", "Unable to update IdlePlay entry"));
return;
}
DataHolder.queueWorldSave(area.dim);
screen.idlePlayConfigChanged();
if (area.hasPlayer()) {
- sendToPlayers(area.playerSnapshot(), VideoPackets.idlePlay(screen));
+ screen.syncIdlePlay();
}
message(player, VpTranslation.of("message.videoplayer.idle_play_updated", "Updated IdlePlay list for screen %s", screen.name));
reply.ok();
diff --git a/paper-plugin/src/main/java/com/github/squi2rel/vp/video/ScreenBroadcaster.java b/paper-plugin/src/main/java/com/github/squi2rel/vp/video/ScreenBroadcaster.java
index e710b67..c943a1d 100644
--- a/paper-plugin/src/main/java/com/github/squi2rel/vp/video/ScreenBroadcaster.java
+++ b/paper-plugin/src/main/java/com/github/squi2rel/vp/video/ScreenBroadcaster.java
@@ -29,6 +29,23 @@ public void syncPlaylist() {
send(VideoPackets.updatePlaylist(List.of(screen)));
}
+ public void syncIdlePlay() {
+ byte[] current = null;
+ byte[] legacy = null;
+ for (UUID uuid : screen.area.playerSnapshot()) {
+ boolean mutations = DataHolder.supportsIdlePlayMutations(uuid);
+ byte[] data;
+ if (mutations) {
+ if (current == null) current = VideoPackets.idlePlay(screen, true);
+ data = current;
+ } else {
+ if (legacy == null) legacy = VideoPackets.idlePlay(screen, false);
+ data = legacy;
+ }
+ DataHolder.sendTo(uuid, data);
+ }
+ }
+
public void playbackNotice(VpTranslation message, boolean error) {
for (UUID uuid : screen.area.playerSnapshot()) {
DataHolder.message(uuid, message);
diff --git a/paper-plugin/src/test/java/com/github/squi2rel/vp/VideoHandshakeRejectionTest.java b/paper-plugin/src/test/java/com/github/squi2rel/vp/VideoHandshakeRejectionTest.java
index bfd2da9..e487efa 100644
--- a/paper-plugin/src/test/java/com/github/squi2rel/vp/VideoHandshakeRejectionTest.java
+++ b/paper-plugin/src/test/java/com/github/squi2rel/vp/VideoHandshakeRejectionTest.java
@@ -21,4 +21,20 @@ void reportsOnlyTheFirstRejectionForAPlayer() {
DataHolder.playerLeave(uuid);
}
}
+
+ @Test
+ void reportsOnlyActualHandshakeTokenChanges() {
+ UUID uuid = UUID.randomUUID();
+ String previousVersion = VideoPlayerMain.version;
+ try {
+ VideoPlayerMain.version = "2.0.2";
+ assertTrue(DataHolder.recordHandshakeToken(uuid, "2.0.1|vp2"));
+ assertFalse(DataHolder.recordHandshakeToken(uuid, "2.0.1|vp2"));
+ assertTrue(DataHolder.recordHandshakeToken(uuid, "2.0.2|vp5"));
+ assertFalse(DataHolder.recordHandshakeToken(uuid, "2.0.2|vp5"));
+ } finally {
+ VideoPlayerMain.version = previousVersion;
+ DataHolder.playerLeave(uuid);
+ }
+ }
}
diff --git a/paper-plugin/src/test/java/com/github/squi2rel/vp/network/ServerPacketHandlerEarlyReturnTest.java b/paper-plugin/src/test/java/com/github/squi2rel/vp/network/ServerPacketHandlerEarlyReturnTest.java
index fe23921..8a19c8b 100644
--- a/paper-plugin/src/test/java/com/github/squi2rel/vp/network/ServerPacketHandlerEarlyReturnTest.java
+++ b/paper-plugin/src/test/java/com/github/squi2rel/vp/network/ServerPacketHandlerEarlyReturnTest.java
@@ -1,6 +1,7 @@
package com.github.squi2rel.vp.network;
import com.github.squi2rel.vp.DataHolder;
+import com.github.squi2rel.vp.VideoPlayerMain;
import com.github.squi2rel.vp.permission.AreaPermissionDecision;
import com.github.squi2rel.vp.permission.VideoPermissionContext;
import com.github.squi2rel.vp.permission.VideoPermissions;
@@ -288,6 +289,40 @@ void deniedIdlePlayConsumesConfigWithoutMutatingScreen() {
}
}
+ @Test
+ void legacyIdlePlaySnapshotIsConsumedWithoutDisconnectingAndPreservesExistingEntries() {
+ IdlePlayEntry existing = IdlePlayEntry.create(
+ "https://example.invalid/existing", UUID.randomUUID(), "original", 75
+ );
+ screen.setIdlePlayEntries(List.of(existing), false);
+ String previousVersion = VideoPlayerMain.version;
+ VideoPlayerMain.version = "2.0.2";
+ DataHolder.recordHandshakeToken(playerId, "2.0.1|vp2");
+ ByteBuf buf = controlled(VideoPacketType.IDLE_PLAY, screen.name);
+ try {
+ VideoPackets.writeLegacyIdlePlayConfig(buf, List.of(
+ existing.url(), "https://example.invalid/new"
+ ), true);
+
+ try (MockedStatic dataHolder = mockStatic(DataHolder.class, CALLS_REAL_METHODS)) {
+ ServerPacketHandler.handle(player, buf);
+
+ dataHolder.verify(() -> DataHolder.disconnect(eq(player), anyString()), never());
+ }
+
+ assertFalse(buf.isReadable());
+ assertTrue(screen.idlePlayRandom);
+ assertEquals(2, screen.idlePlayEntries.size());
+ assertEquals(existing, screen.idlePlayEntries.getFirst());
+ assertEquals(playerId, screen.idlePlayEntries.getLast().addedBy());
+ assertEquals("tester", screen.idlePlayEntries.getLast().addedByName());
+ assertEquals(IdlePlayEntry.MIN_PRIORITY, screen.idlePlayEntries.getLast().priority());
+ } finally {
+ buf.release();
+ VideoPlayerMain.version = previousVersion;
+ }
+ }
+
@Test
void successfulIdlePlayUsesTheServerPlayerIdentityAndPreservesItOnPriorityChange() {
ByteBuf add = controlled(VideoPacketType.IDLE_PLAY, screen.name);
diff --git a/src/client/java/com/github/squi2rel/vp/ClientPacketHandler.java b/src/client/java/com/github/squi2rel/vp/ClientPacketHandler.java
index 66c5cbb..c961adf 100644
--- a/src/client/java/com/github/squi2rel/vp/ClientPacketHandler.java
+++ b/src/client/java/com/github/squi2rel/vp/ClientPacketHandler.java
@@ -61,6 +61,7 @@ public class ClientPacketHandler {
private static final Map pendingRequests = new HashMap<>();
private static final Map pendingReporterGrants = new HashMap<>();
private static final Map playbackDiagnostics = new HashMap<>();
+ private static String serverProtocolToken = "";
public static void handle(ByteBuf buf) {
handle(buf, System.currentTimeMillis());
@@ -132,12 +133,21 @@ public static void handle(ByteBuf buf, long receivedAt) {
String areaName = VideoPackets.readName(buf);
String screenName = VideoPackets.readName(buf);
ClientVideoScreen screen = screenOrNull(areaName, screenName);
- if (screen == null) {
- VideoScreen discard = new VideoScreen(null, screenName, List.of(), "");
- VideoPackets.readIdlePlayConfig(buf, discard);
- return;
+ if (VideoProtocol.supportsIdlePlayMutations(serverProtocolToken)) {
+ if (screen == null) {
+ VideoScreen discard = new VideoScreen(null, screenName, List.of(), "");
+ VideoPackets.readIdlePlayConfig(buf, discard);
+ return;
+ }
+ VideoPackets.readIdlePlayConfig(buf, screen);
+ } else {
+ VideoPackets.LegacyIdlePlayConfig legacy = VideoPackets.readLegacyIdlePlayConfig(buf);
+ if (screen != null) {
+ screen.replaceLegacyIdlePlayConfig(
+ legacy.urls(), legacy.random(), IdlePlayEntry.UNKNOWN_UUID, ""
+ );
+ }
}
- VideoPackets.readIdlePlayConfig(buf, screen);
}
case SET_UV -> {
String areaName = VideoPackets.readName(buf);
@@ -282,6 +292,7 @@ private static void handleConfig(ByteBuf buf, boolean reset) {
}
String remoteVersion = VideoProtocol.displayVersion(remoteToken);
VideoPlayerClient.acceptProtocol();
+ serverProtocolToken = remoteToken;
VideoPlayerClient.handshakeResponse(remoteVersion);
VideoPlayerClient.remoteControlName = ByteBufUtils.readString(buf, 256);
VideoPlayerClient.remoteControlId = buf.readFloat();
@@ -664,6 +675,7 @@ private static void removeDiagnosticsArea(String areaName) {
public static void resetPendingRequests() {
pendingReporterGrants.clear();
playbackDiagnostics.clear();
+ serverProtocolToken = "";
if (pendingRequests.isEmpty()) return;
ArrayList> pending = new ArrayList<>(pendingRequests.entrySet());
pendingRequests.clear();
diff --git a/src/client/java/com/github/squi2rel/vp/VideoPlayerClient.java b/src/client/java/com/github/squi2rel/vp/VideoPlayerClient.java
index d85eabc..de51d63 100644
--- a/src/client/java/com/github/squi2rel/vp/VideoPlayerClient.java
+++ b/src/client/java/com/github/squi2rel/vp/VideoPlayerClient.java
@@ -1004,7 +1004,7 @@ public static void rejectProtocol(String remoteVersion) {
protocolMismatchShown = true;
client.player.sendMessage(VpTexts.tr(
"message.videoplayer.version_mismatch",
- "VideoPlayer client and server must use the same release version. Local: %s, server: %s",
+ "VideoPlayer client version %s is not compatible with server %s",
VideoPlayerMain.version, remoteVersion == null || remoteVersion.isBlank() ? "unknown" : remoteVersion
).formatted(Formatting.RED), false);
}
diff --git a/src/main/java/com/github/squi2rel/vp/DataHolder.java b/src/main/java/com/github/squi2rel/vp/DataHolder.java
index c1e76d4..64e8d98 100644
--- a/src/main/java/com/github/squi2rel/vp/DataHolder.java
+++ b/src/main/java/com/github/squi2rel/vp/DataHolder.java
@@ -538,7 +538,9 @@ private static void sendAreaSnapshot(ServerPlayerEntity player, VideoArea area)
ServerPacketHandler.sendTo(player, VideoPackets.setMetadata(screen, entry.getKey(), entry.getValue()));
}
if (!screen.idlePlayEntries.isEmpty() || screen.idlePlayRandom) {
- ServerPacketHandler.sendTo(player, VideoPackets.idlePlay(screen));
+ ServerPacketHandler.sendTo(player, VideoPackets.idlePlay(
+ screen, supportsIdlePlayMutations(player.getUuid())
+ ));
}
}
boolean loadedPlayback = false;
@@ -640,9 +642,11 @@ public static long handshakeNonce(UUID uuid) {
return handshakeNonces.getOrDefault(uuid, 0L);
}
- public static void recordHandshakeToken(UUID uuid, String remoteToken) {
- if (uuid == null) return;
- handshakeTokens.put(uuid, com.github.squi2rel.vp.network.VideoProtocol.responseToken(VideoPlayerMain.version, remoteToken));
+ public static boolean recordHandshakeToken(UUID uuid, String remoteToken) {
+ if (uuid == null) return false;
+ String token = com.github.squi2rel.vp.network.VideoProtocol.responseToken(VideoPlayerMain.version, remoteToken);
+ String previous = handshakeTokens.put(uuid, token);
+ return previous == null || !previous.equals(token);
}
public static String handshakeToken(UUID uuid) {
@@ -673,6 +677,33 @@ public static boolean protocolActive(UUID uuid) {
return handshakes.get(uuid) == VideoHandshakeState.ACTIVE;
}
+ public static boolean supportsClientPlaybackReporting(UUID uuid) {
+ return protocolActive(uuid)
+ && com.github.squi2rel.vp.network.VideoProtocol.supportsClientPlaybackReporting(handshakeToken(uuid));
+ }
+
+ public static boolean supportsIdlePlayMutations(UUID uuid) {
+ return protocolActive(uuid)
+ && com.github.squi2rel.vp.network.VideoProtocol.supportsIdlePlayMutations(handshakeToken(uuid));
+ }
+
+ public static void refreshPlayerProtocol(ServerPlayerEntity player) {
+ if (player == null || !protocolActive(player.getUuid())) return;
+ UUID uuid = player.getUuid();
+ boolean mutations = supportsIdlePlayMutations(uuid);
+ for (HashMap world : areas.values()) {
+ for (VideoArea area : world.values()) {
+ if (!area.containsPlayer(uuid)) continue;
+ for (VideoScreen screen : area.screens) {
+ screen.addPlayer(uuid);
+ if (!screen.idlePlayEntries.isEmpty() || screen.idlePlayRandom) {
+ ServerPacketHandler.sendTo(player, VideoPackets.idlePlay(screen, mutations));
+ }
+ }
+ }
+ }
+ }
+
public static long lifecycleEpoch() {
return lifecycleEpoch;
}
diff --git a/src/main/java/com/github/squi2rel/vp/network/ServerPacketHandler.java b/src/main/java/com/github/squi2rel/vp/network/ServerPacketHandler.java
index 4ca0d92..152db4a 100644
--- a/src/main/java/com/github/squi2rel/vp/network/ServerPacketHandler.java
+++ b/src/main/java/com/github/squi2rel/vp/network/ServerPacketHandler.java
@@ -67,13 +67,13 @@ public static void handle(ServerPlayerEntity player, ByteBuf buf, long receivedA
sendTo(player, VideoPackets.protocolReject(VideoPlayerMain.version));
reject(player, VpTranslation.of(
"error.videoplayer.protocol_mismatch",
- "VideoPlayer client and server must use the same release version. Client: %s, server: %s",
+ "VideoPlayer client version %s is not compatible with server %s",
VideoProtocol.displayVersion(remoteToken), VideoPlayerMain.version
));
}
return;
}
- DataHolder.recordHandshakeToken(player.getUuid(), remoteToken);
+ boolean protocolChanged = DataHolder.recordHandshakeToken(player.getUuid(), remoteToken);
String responseToken = DataHolder.handshakeToken(player.getUuid());
VideoHandshakeState previous = DataHolder.handshakeState(player.getUuid());
if (previous == VideoHandshakeState.NEEDS_RESET) {
@@ -87,6 +87,7 @@ public static void handle(ServerPlayerEntity player, ByteBuf buf, long receivedA
} else if (previous == VideoHandshakeState.ACTIVE) {
sendTo(player, VideoPackets.config(responseToken, DataHolder.config));
sendGlobalPermissions(player);
+ if (protocolChanged) DataHolder.refreshPlayerProtocol(player);
}
}
case HANDSHAKE_ACK -> {
@@ -363,27 +364,35 @@ url, new PlayerProviderSource(player, bilibiliLimit, youtubeLimit)
}
case IDLE_PLAY -> {
int requestId = buf.readInt();
- VideoArea area = requireArea(player, requestId, VideoPackets.readName(buf));
+ String areaName = VideoPackets.readName(buf);
+ String screenName = VideoPackets.readName(buf);
+ boolean mutations = DataHolder.supportsIdlePlayMutations(player.getUuid());
+ IdlePlayMutation mutation = mutations ? VideoPackets.readIdlePlayMutation(buf) : null;
+ VideoPackets.LegacyIdlePlayConfig legacy = mutations ? null : VideoPackets.readLegacyIdlePlayConfig(buf);
+ VideoArea area = requireArea(player, requestId, areaName);
if (area == null) return;
- VideoScreen screen = requireScreen(player, requestId, area, VideoPackets.readName(buf));
+ VideoScreen screen = requireScreen(player, requestId, area, screenName);
if (screen == null) return;
- IdlePlayMutation mutation = VideoPackets.readIdlePlayMutation(buf);
if (!requirePermission(player, requestId, VideoPermissionAction.SET_IDLE_PLAY, VideoPermissionContext.screen(screen))) return;
- if (mutation.action() == IdlePlayAction.ADD) {
+ if (mutation != null && mutation.action() == IdlePlayAction.ADD) {
mutation = IdlePlayMutation.add(VideoUrlNormalizer.normalizeSubmittedUrl(mutation.url()), mutation.priority());
if (!VideoScreen.validIdlePlayUrl(mutation.url())) {
requestError(player, requestId, VpTranslation.of("error.videoplayer.idle_play_url_invalid", "IdlePlay URL is invalid"));
return;
}
}
- if (!screen.applyIdlePlayMutation(mutation, player.getUuid(), player.getName().getString())) {
+ if (legacy != null) {
+ screen.replaceLegacyIdlePlayConfig(
+ legacy.urls(), legacy.random(), player.getUuid(), player.getName().getString()
+ );
+ } else if (!screen.applyIdlePlayMutation(mutation, player.getUuid(), player.getName().getString())) {
requestError(player, requestId, VpTranslation.of("error.videoplayer.idle_play_mutation_failed", "Unable to update IdlePlay entry"));
return;
}
DataHolder.queueWorldSave(area.dim);
screen.idlePlayConfigChanged();
if (area.hasPlayer()) {
- sendToPlayers(players(player.getEntityWorld().getServer().getPlayerManager(), area.playerSnapshot()), VideoPackets.idlePlay(screen));
+ screen.syncIdlePlay();
}
message(player, VpTranslation.of("message.videoplayer.idle_play_updated", "Updated IdlePlay list for screen %s", screen.name));
requestOk(player, requestId);
diff --git a/src/main/java/com/github/squi2rel/vp/network/VideoPackets.java b/src/main/java/com/github/squi2rel/vp/network/VideoPackets.java
index 8589d6f..2e7bd08 100644
--- a/src/main/java/com/github/squi2rel/vp/network/VideoPackets.java
+++ b/src/main/java/com/github/squi2rel/vp/network/VideoPackets.java
@@ -125,6 +125,48 @@ public static void writeIdlePlayConfig(ByteBuf buf, List requestedUrls,
writeIdlePlayEntries(buf, entries, random);
}
+ public static LegacyIdlePlayConfig readLegacyIdlePlayConfig(ByteBuf buf) {
+ boolean random = buf.readBoolean();
+ int count = buf.readUnsignedByte();
+ if (count > VideoScreen.MAX_IDLE_PLAY_ITEMS) {
+ throw new IllegalStateException("IdlePlay item count exceeds " + VideoScreen.MAX_IDLE_PLAY_ITEMS);
+ }
+ ArrayList urls = new ArrayList<>(count);
+ int totalBytes = 0;
+ for (int i = 0; i < count; i++) {
+ String url = ByteBufUtils.readString(buf, VideoScreen.MAX_IDLE_PLAY_URL_LENGTH);
+ totalBytes += ByteBufUtils.utf8Length(url);
+ if (totalBytes > VideoScreen.MAX_IDLE_PLAY_TOTAL_BYTES) {
+ throw new IllegalStateException("IdlePlay URL payload exceeds " + VideoScreen.MAX_IDLE_PLAY_TOTAL_BYTES + " bytes");
+ }
+ urls.add(url);
+ }
+ try {
+ return new LegacyIdlePlayConfig(VideoScreen.validatedIdlePlayConfig(urls), random);
+ } catch (IllegalArgumentException error) {
+ throw new IllegalStateException(error.getMessage(), error);
+ }
+ }
+
+ public static void writeLegacyIdlePlayConfig(ByteBuf buf, VideoScreen screen) {
+ screen.ensureValidState();
+ ArrayList urls = new ArrayList<>(screen.idlePlayEntries.size());
+ for (IdlePlayEntry entry : screen.idlePlayEntries) urls.add(entry.url());
+ writeLegacyIdlePlayConfig(buf, urls, screen.idlePlayRandom);
+ }
+
+ public static void writeLegacyIdlePlayConfig(ByteBuf buf, List requestedUrls, boolean random) {
+ List validated;
+ try {
+ validated = VideoScreen.validatedIdlePlayConfig(requestedUrls);
+ } catch (IllegalArgumentException error) {
+ throw new IllegalStateException(error.getMessage(), error);
+ }
+ buf.writeBoolean(random);
+ buf.writeByte(validated.size());
+ for (String url : validated) ByteBufUtils.writeString(buf, url);
+ }
+
public static void writeIdlePlayEntries(ByteBuf buf, List requestedEntries, boolean random) {
List entries;
try {
@@ -649,10 +691,18 @@ public static byte[] skip(VideoScreen screen) {
}
public static byte[] idlePlay(VideoScreen screen) {
+ return idlePlay(screen, true);
+ }
+
+ public static byte[] idlePlay(VideoScreen screen, boolean mutations) {
ByteBuf buf = create(VideoPacketType.IDLE_PLAY);
writeString(buf, screen.area.name);
writeString(buf, screen.name);
- writeIdlePlayConfig(buf, screen);
+ if (mutations) {
+ writeIdlePlayConfig(buf, screen);
+ } else {
+ writeLegacyIdlePlayConfig(buf, screen);
+ }
return toByteArray(buf);
}
@@ -729,4 +779,10 @@ public static byte[] updateScreen(VideoScreen screen, List vertices, S
VideoScreen.writeDisplayConfig(buf, displayConfig == null ? screen : displayConfig);
return toByteArray(buf);
}
+
+ public record LegacyIdlePlayConfig(List urls, boolean random) {
+ public LegacyIdlePlayConfig {
+ urls = urls == null ? List.of() : List.copyOf(urls);
+ }
+ }
}
diff --git a/src/main/java/com/github/squi2rel/vp/network/VideoProtocol.java b/src/main/java/com/github/squi2rel/vp/network/VideoProtocol.java
index 31d1a36..a953ad9 100644
--- a/src/main/java/com/github/squi2rel/vp/network/VideoProtocol.java
+++ b/src/main/java/com/github/squi2rel/vp/network/VideoProtocol.java
@@ -2,6 +2,9 @@
public final class VideoProtocol {
public static final String WIRE_REVISION = "vp5";
+ private static final int LEGACY_WIRE_REVISION = 2;
+ private static final int REPORTING_WIRE_REVISION = 4;
+ private static final int IDLE_PLAY_MUTATION_WIRE_REVISION = 5;
private VideoProtocol() {
}
@@ -10,11 +13,16 @@ public static String token(String version) {
return safe(version) + "|" + WIRE_REVISION;
}
- // Compatibility intentionally compares release versions only. Do not require the wire revision here.
+ public static String legacyToken() {
+ return "2.0.1|vp" + LEGACY_WIRE_REVISION;
+ }
+
public static boolean compatible(String localVersion, String remoteToken) {
String localRelease = releaseVersion(localVersion);
String remoteRelease = releaseVersion(remoteToken);
- return !localRelease.isEmpty() && localRelease.equals(remoteRelease);
+ if (localRelease.isEmpty() || remoteRelease.isEmpty() || !supportedWireRevision(remoteToken)) return false;
+ return localRelease.equals(remoteRelease)
+ || optionalUpdateRelease(localRelease) && optionalUpdateRelease(remoteRelease);
}
public static String responseToken(String localVersion, String remoteToken) {
@@ -32,6 +40,38 @@ public static String displayVersion(String token) {
return releaseVersion(token);
}
+ public static boolean supportsClientPlaybackReporting(String token) {
+ return wireRevision(token) >= REPORTING_WIRE_REVISION;
+ }
+
+ public static boolean supportsIdlePlayMutations(String token) {
+ return wireRevision(token) >= IDLE_PLAY_MUTATION_WIRE_REVISION;
+ }
+
+ public static int wireRevision(String token) {
+ String normalized = normalize(token);
+ int separator = normalized.indexOf('|');
+ if (separator < 0 || separator + 3 > normalized.length()) return -1;
+ String revision = normalized.substring(separator + 1).trim();
+ if (!revision.startsWith("vp") || revision.length() == 2) return -1;
+ try {
+ return Integer.parseInt(revision.substring(2));
+ } catch (NumberFormatException ignored) {
+ return -1;
+ }
+ }
+
+ private static boolean supportedWireRevision(String token) {
+ int revision = wireRevision(token);
+ return revision == LEGACY_WIRE_REVISION
+ || revision == REPORTING_WIRE_REVISION
+ || revision == IDLE_PLAY_MUTATION_WIRE_REVISION;
+ }
+
+ private static boolean optionalUpdateRelease(String release) {
+ return "2.0.1".equals(release) || "2.0.2".equals(release);
+ }
+
private static String releaseVersion(String token) {
String normalized = normalize(token);
int separator = normalized.indexOf('|');
diff --git a/src/main/java/com/github/squi2rel/vp/video/PlaybackController.java b/src/main/java/com/github/squi2rel/vp/video/PlaybackController.java
index 0f48c3f..4942c29 100644
--- a/src/main/java/com/github/squi2rel/vp/video/PlaybackController.java
+++ b/src/main/java/com/github/squi2rel/vp/video/PlaybackController.java
@@ -71,7 +71,7 @@ public PlaybackController(VideoScreen screen, PlaybackQueue queue, ScreenBroadca
stateExecutor(screen),
delayedExecutor(screen),
screen.captureLifecycleToken(),
- DataHolder::protocolActive
+ DataHolder::supportsClientPlaybackReporting
);
}
diff --git a/src/main/java/com/github/squi2rel/vp/video/ScreenBroadcaster.java b/src/main/java/com/github/squi2rel/vp/video/ScreenBroadcaster.java
index c2a2b94..01cb527 100644
--- a/src/main/java/com/github/squi2rel/vp/video/ScreenBroadcaster.java
+++ b/src/main/java/com/github/squi2rel/vp/video/ScreenBroadcaster.java
@@ -42,6 +42,27 @@ public void syncPlaylist() {
send(VideoPackets.updatePlaylist(List.of(screen)));
}
+ public void syncIdlePlay() {
+ if (server == null) return;
+ PlayerManager pm = server.getPlayerManager();
+ byte[] current = null;
+ byte[] legacy = null;
+ for (var uuid : screen.area.playerSnapshot()) {
+ ServerPlayerEntity player = pm.getPlayer(uuid);
+ if (player == null) continue;
+ boolean mutations = DataHolder.supportsIdlePlayMutations(uuid);
+ byte[] data;
+ if (mutations) {
+ if (current == null) current = VideoPackets.idlePlay(screen, true);
+ data = current;
+ } else {
+ if (legacy == null) legacy = VideoPackets.idlePlay(screen, false);
+ data = legacy;
+ }
+ ServerPacketHandler.sendTo(player, data);
+ }
+ }
+
public void playbackNotice(VpTranslation message, boolean error) {
for (var uuid : screen.area.playerSnapshot()) {
DataHolder.message(uuid, screen.serverPluginEpoch(), message);
diff --git a/src/main/java/com/github/squi2rel/vp/video/VideoScreen.java b/src/main/java/com/github/squi2rel/vp/video/VideoScreen.java
index 101cfa4..40efccc 100644
--- a/src/main/java/com/github/squi2rel/vp/video/VideoScreen.java
+++ b/src/main/java/com/github/squi2rel/vp/video/VideoScreen.java
@@ -166,6 +166,28 @@ public void setIdlePlayEntries(List entries, boolean random) {
resetIdlePlayOrder();
}
+ public void replaceLegacyIdlePlayConfig(List urls, boolean random, UUID addedBy, String addedByName) {
+ List validated = validatedIdlePlayConfig(urls);
+ sanitizeIdlePlay();
+ ArrayList existing = new ArrayList<>(idlePlayEntries);
+ boolean[] retained = new boolean[existing.size()];
+ ArrayList next = new ArrayList<>(validated.size());
+ for (String url : validated) {
+ IdlePlayEntry match = null;
+ for (int i = 0; i < existing.size(); i++) {
+ if (!retained[i] && existing.get(i).url().equals(url)) {
+ retained[i] = true;
+ match = existing.get(i);
+ break;
+ }
+ }
+ next.add(match == null
+ ? IdlePlayEntry.create(url, addedBy, addedByName, IdlePlayEntry.MIN_PRIORITY)
+ : match);
+ }
+ setIdlePlayEntries(next, random);
+ }
+
public boolean addIdlePlayEntry(String url, UUID addedBy, String addedByName, int priority) {
sanitizeIdlePlay();
if (idlePlayEntries.size() >= MAX_IDLE_PLAY_ITEMS) return false;
@@ -319,6 +341,10 @@ public void syncInfo() {
broadcaster.syncPlaylist();
}
+ public void syncIdlePlay() {
+ if (broadcaster != null) broadcaster.syncIdlePlay();
+ }
+
public static int clampSphereSegments(int value) {
return Math.clamp(value <= 0 ? DEFAULT_SPHERE_SEGMENTS : value, MIN_SPHERE_SEGMENTS, MAX_SPHERE_SEGMENTS);
}
diff --git a/src/main/resources/assets/videoplayer/lang/en_us.json b/src/main/resources/assets/videoplayer/lang/en_us.json
index b9ad1b1..8a3efa4 100644
--- a/src/main/resources/assets/videoplayer/lang/en_us.json
+++ b/src/main/resources/assets/videoplayer/lang/en_us.json
@@ -147,7 +147,7 @@
"error.videoplayer.permission_denied": "Permission denied",
"error.videoplayer.playback_stale": "Playback control request is stale",
"error.videoplayer.progress_unavailable": "Playback progress is unavailable",
- "error.videoplayer.protocol_mismatch": "VideoPlayer client and server must use the same release version. Client: %s, server: %s",
+ "error.videoplayer.protocol_mismatch": "VideoPlayer client version %s is not compatible with server %s",
"error.videoplayer.queue_full": "Video queue is full",
"error.videoplayer.request_failed": "Request failed: %s",
"error.videoplayer.request_incomplete": "Request did not complete",
diff --git a/src/main/resources/assets/videoplayer/lang/zh_cn.json b/src/main/resources/assets/videoplayer/lang/zh_cn.json
index b5a786e..61c8a64 100644
--- a/src/main/resources/assets/videoplayer/lang/zh_cn.json
+++ b/src/main/resources/assets/videoplayer/lang/zh_cn.json
@@ -147,7 +147,7 @@
"error.videoplayer.permission_denied": "权限不足",
"error.videoplayer.playback_stale": "播放控制请求已过期",
"error.videoplayer.progress_unavailable": "无法获取播放进度",
- "error.videoplayer.protocol_mismatch": "VideoPlayer 客户端与服务端必须使用相同发布版本。客户端: %s,服务端: %s",
+ "error.videoplayer.protocol_mismatch": "VideoPlayer 客户端版本 %s 与服务端版本 %s 不兼容",
"error.videoplayer.queue_full": "视频队列已满",
"error.videoplayer.request_failed": "请求失败: %s",
"error.videoplayer.request_incomplete": "请求未完成",
diff --git a/src/test/java/com/github/squi2rel/vp/network/IdlePlayMutationPacketTest.java b/src/test/java/com/github/squi2rel/vp/network/IdlePlayMutationPacketTest.java
index 5741af8..27cfb8f 100644
--- a/src/test/java/com/github/squi2rel/vp/network/IdlePlayMutationPacketTest.java
+++ b/src/test/java/com/github/squi2rel/vp/network/IdlePlayMutationPacketTest.java
@@ -1,9 +1,13 @@
package com.github.squi2rel.vp.network;
+import com.github.squi2rel.vp.video.VideoArea;
+import com.github.squi2rel.vp.video.VideoScreen;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
+import org.joml.Vector3f;
import org.junit.jupiter.api.Test;
+import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -42,6 +46,37 @@ void rejectsUnknownActionsAndOutOfRangePriorities() {
}
}
+ @Test
+ void roundTripsTheVp2IdlePlaySnapshot() {
+ List urls = List.of("https://example.com/first", "https://example.com/second");
+ ByteBuf config = Unpooled.buffer();
+ try {
+ VideoPackets.writeLegacyIdlePlayConfig(config, urls, true);
+ VideoPackets.LegacyIdlePlayConfig decoded = VideoPackets.readLegacyIdlePlayConfig(config);
+ assertEquals(urls, decoded.urls());
+ assertEquals(true, decoded.random());
+ assertFalse(config.isReadable());
+ } finally {
+ config.release();
+ }
+
+ VideoArea area = new VideoArea(new Vector3f(), new Vector3f(1), "area", "world");
+ VideoScreen screen = new VideoScreen(area, "screen", List.of(), "");
+ screen.setIdlePlayConfig(urls, false);
+ ByteBuf packet = Unpooled.wrappedBuffer(VideoPackets.idlePlay(screen, false));
+ try {
+ assertEquals(VideoPacketType.IDLE_PLAY, VideoPackets.readType(packet));
+ assertEquals("area", VideoPackets.readName(packet));
+ assertEquals("screen", VideoPackets.readName(packet));
+ VideoPackets.LegacyIdlePlayConfig decoded = VideoPackets.readLegacyIdlePlayConfig(packet);
+ assertEquals(urls, decoded.urls());
+ assertEquals(false, decoded.random());
+ assertFalse(packet.isReadable());
+ } finally {
+ packet.release();
+ }
+ }
+
private static void assertRoundTrip(IdlePlayMutation expected) {
ByteBuf buf = Unpooled.buffer();
try {
diff --git a/src/test/java/com/github/squi2rel/vp/network/VideoProtocolTest.java b/src/test/java/com/github/squi2rel/vp/network/VideoProtocolTest.java
index e7b1875..0f9a2ea 100644
--- a/src/test/java/com/github/squi2rel/vp/network/VideoProtocolTest.java
+++ b/src/test/java/com/github/squi2rel/vp/network/VideoProtocolTest.java
@@ -5,6 +5,7 @@
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class VideoProtocolTest {
@@ -17,23 +18,26 @@ void createsAndMatchesTheCurrentWireToken() {
}
@Test
- void acceptsEveryWireRevisionForTheSameRelease() {
+ void acceptsOnlyPublishedWireRevisions() {
assertTrue(VideoProtocol.compatible("2.0.1", "2.0.1|vp5"));
- assertTrue(VideoProtocol.compatible("2.0.1", "2.0.1"));
assertTrue(VideoProtocol.compatible("2.0.1", "2.0.1|vp2"));
- assertTrue(VideoProtocol.compatible("2.0.1", "2.0.1|custom-build"));
+ assertTrue(VideoProtocol.compatible("2.0.1", "2.0.1|vp4"));
+ assertFalse(VideoProtocol.compatible("2.0.1", "2.0.1"));
+ assertFalse(VideoProtocol.compatible("2.0.1", "2.0.1|vp3"));
+ assertFalse(VideoProtocol.compatible("2.0.1", "2.0.1|custom-build"));
}
@Test
void enforcesTheReleaseVersionMatrix() {
List cases = List.of(
new CompatibilityCase("2.0.1", "2.0.1|vp5", true),
- new CompatibilityCase("2.0.1", "2.0.2|vp5", false),
+ new CompatibilityCase("2.0.1", "2.0.2|vp5", true),
+ new CompatibilityCase("2.0.2", "2.0.1|vp2", true),
new CompatibilityCase("2.0.1", "2.0.10|vp5", false),
- new CompatibilityCase("2.0.1", "2.0.1|vp1", true),
- new CompatibilityCase("2.0.1", "2.0.1|vp5-extra", true),
- new CompatibilityCase("2.0.1", "2.0.1|", true),
- new CompatibilityCase("2.0.1", "2.0.1", true),
+ new CompatibilityCase("2.0.1", "2.0.1|vp1", false),
+ new CompatibilityCase("2.0.1", "2.0.1|vp5-extra", false),
+ new CompatibilityCase("2.0.1", "2.0.1|", false),
+ new CompatibilityCase("2.0.1", "2.0.1", false),
new CompatibilityCase("2.0.1", "", false),
new CompatibilityCase("2.0.1", null, false)
);
@@ -52,8 +56,22 @@ void respondsWithTheLocalTokenUnlessThePeerTokenIsCompatible() {
assertEquals("2.0.1|vp5", VideoProtocol.responseToken("2.0.1", "2.0.1|vp5"));
assertEquals("2.0.1|vp5", VideoProtocol.responseToken("2.0.1", " 2.0.1|vp5 "));
assertEquals("2.0.1|vp2", VideoProtocol.responseToken("2.0.1", "2.0.1|vp2"));
- assertEquals("2.0.1", VideoProtocol.responseToken("2.0.1", "2.0.1"));
- assertEquals("2.0.1|vp5", VideoProtocol.responseToken("2.0.1", "2.0.2|vp5"));
+ assertEquals("2.0.1|vp5", VideoProtocol.responseToken("2.0.1", "2.0.1"));
+ assertEquals("2.0.1|vp2", VideoProtocol.responseToken("2.0.2", "2.0.1|vp2"));
+ assertEquals("2.0.2|vp5", VideoProtocol.responseToken("2.0.2", "2.0.1|custom-build"));
+ }
+
+ @Test
+ void exposesWireCapabilitiesForPerPlayerPacketSelection() {
+ assertEquals("2.0.1|vp2", VideoProtocol.legacyToken());
+ assertEquals(2, VideoProtocol.wireRevision("2.0.1|vp2"));
+ assertEquals(4, VideoProtocol.wireRevision("2.0.1|vp4"));
+ assertEquals(5, VideoProtocol.wireRevision("2.0.2|vp5"));
+ assertEquals(-1, VideoProtocol.wireRevision("2.0.1|custom-build"));
+ assertFalse(VideoProtocol.supportsClientPlaybackReporting("2.0.1|vp2"));
+ assertTrue(VideoProtocol.supportsClientPlaybackReporting("2.0.1|vp4"));
+ assertFalse(VideoProtocol.supportsIdlePlayMutations("2.0.1|vp4"));
+ assertTrue(VideoProtocol.supportsIdlePlayMutations("2.0.2|vp5"));
}
@Test
From 083ffcf85c41896b6172b821e8213061113126b9 Mon Sep 17 00:00:00 2001
From: cloudfl4re
Date: Wed, 5 Aug 2026 19:15:57 +0800
Subject: [PATCH 05/23] =?UTF-8?q?test:=20=E4=BF=AE=E5=A4=8D=20Windows=20?=
=?UTF-8?q?=E6=8D=A2=E8=A1=8C=E5=9F=BA=E7=BA=BF=E6=A3=80=E6=9F=A5=EF=BC=88?=
=?UTF-8?q?=E4=BB=BB=E5=8A=A1=200/7=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../squi2rel/vp/permission/PluginPermissionDefaultsTest.java | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/paper-plugin/src/test/java/com/github/squi2rel/vp/permission/PluginPermissionDefaultsTest.java b/paper-plugin/src/test/java/com/github/squi2rel/vp/permission/PluginPermissionDefaultsTest.java
index 8a9336a..696e690 100644
--- a/paper-plugin/src/test/java/com/github/squi2rel/vp/permission/PluginPermissionDefaultsTest.java
+++ b/paper-plugin/src/test/java/com/github/squi2rel/vp/permission/PluginPermissionDefaultsTest.java
@@ -28,7 +28,9 @@ void permissionDefaultsMatchPublicAndRestrictedActions() throws IOException {
}
assertEquals(VideoPermissionAction.values().length + 3, defaults.size());
- String plugin = Files.readString(Path.of("src/main/resources/plugin.yml"));
+ String plugin = Files.readString(Path.of("src/main/resources/plugin.yml"))
+ .replace("\r\n", "\n")
+ .replace('\r', '\n');
assertTrue(plugin.contains(" vlc:\n description: Manage VideoPlayer server notifications.\n usage: /videoplayer:vlc joinmessage\n permission: videoplayer.joinmessage"));
assertTrue(plugin.contains(" vlcversion:\n description: Show connected VideoPlayer client versions.\n usage: /vlcversion\n permission: videoplayer.version"));
}
From 8462e56642723f48565bdbca4a069ca929b166a6 Mon Sep 17 00:00:00 2001
From: cloudfl4re
Date: Wed, 5 Aug 2026 19:19:09 +0800
Subject: [PATCH 06/23] =?UTF-8?q?fix:=20=E9=99=90=E5=88=B6=E6=8F=A1?=
=?UTF-8?q?=E6=89=8B=E5=8D=8F=E8=AE=AE=E7=89=88=E6=9C=AC=E9=95=BF=E5=BA=A6?=
=?UTF-8?q?=EF=BC=88=E4=BB=BB=E5=8A=A1=201/7=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../com/github/squi2rel/vp/network/VideoProtocol.java | 10 +++++++++-
.../github/squi2rel/vp/network/VideoProtocolTest.java | 10 ++++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/src/main/java/com/github/squi2rel/vp/network/VideoProtocol.java b/src/main/java/com/github/squi2rel/vp/network/VideoProtocol.java
index a953ad9..82b0d1e 100644
--- a/src/main/java/com/github/squi2rel/vp/network/VideoProtocol.java
+++ b/src/main/java/com/github/squi2rel/vp/network/VideoProtocol.java
@@ -1,7 +1,10 @@
package com.github.squi2rel.vp.network;
+import java.nio.charset.StandardCharsets;
+
public final class VideoProtocol {
public static final String WIRE_REVISION = "vp5";
+ public static final int MAX_TOKEN_BYTES = 16;
private static final int LEGACY_WIRE_REVISION = 2;
private static final int REPORTING_WIRE_REVISION = 4;
private static final int IDLE_PLAY_MUTATION_WIRE_REVISION = 5;
@@ -10,7 +13,12 @@ private VideoProtocol() {
}
public static String token(String version) {
- return safe(version) + "|" + WIRE_REVISION;
+ String token = safe(version) + "|" + WIRE_REVISION;
+ int length = token.getBytes(StandardCharsets.UTF_8).length;
+ if (length > MAX_TOKEN_BYTES) {
+ throw new IllegalArgumentException("VideoPlayer protocol token exceeds " + MAX_TOKEN_BYTES + " UTF-8 bytes");
+ }
+ return token;
}
public static String legacyToken() {
diff --git a/src/test/java/com/github/squi2rel/vp/network/VideoProtocolTest.java b/src/test/java/com/github/squi2rel/vp/network/VideoProtocolTest.java
index 0f9a2ea..451d4bc 100644
--- a/src/test/java/com/github/squi2rel/vp/network/VideoProtocolTest.java
+++ b/src/test/java/com/github/squi2rel/vp/network/VideoProtocolTest.java
@@ -6,6 +6,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class VideoProtocolTest {
@@ -87,6 +88,15 @@ void allowsHandshakePacketsAfterClientRejection() {
}
}
+ @Test
+ void rejectsTokensThatCannotFitTheServerHandshakeField() {
+ assertEquals(16, VideoProtocol.MAX_TOKEN_BYTES);
+ assertEquals("2.0.2|vp5", VideoProtocol.token("2.0.2"));
+ assertEquals("2.0.2-26.2|vp5", VideoProtocol.token("2.0.2-26.2"));
+ assertThrows(IllegalArgumentException.class, () -> VideoProtocol.token("2.0.2-26.2-long"));
+ assertThrows(IllegalArgumentException.class, () -> VideoProtocol.token("版本版本版本版本"));
+ }
+
private record CompatibilityCase(String localVersion, String remoteToken, boolean expected) {
}
}
From 4918e0269eb15016a349f856628e597e9a773afd Mon Sep 17 00:00:00 2001
From: cloudfl4re
Date: Wed, 5 Aug 2026 19:31:19 +0800
Subject: [PATCH 07/23] =?UTF-8?q?build:=20=E5=BB=BA=E7=AB=8B=E5=8F=8C?=
=?UTF-8?q?=E7=89=88=E6=9C=AC=E8=81=9A=E5=90=88=E4=B8=8E=E6=9C=8D=E5=8A=A1?=
=?UTF-8?q?=E7=AB=AF=E5=B7=A5=E5=85=B7=E9=93=BE=EF=BC=88=E4=BB=BB=E5=8A=A1?=
=?UTF-8?q?=202/7=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
build.gradle | 203 ++-------------------
fabric-1.21.11/build.gradle | 177 ++++++++++++++++++
gradle.properties | 9 +-
mcng-core/build.gradle | 3 +
mcng-fabric-client/build.gradle | 3 +
paper-plugin-26.2/build.gradle | 160 ++++++++++++++++
paper-plugin/build.gradle | 8 +-
paper-plugin/src/main/resources/plugin.yml | 2 +-
settings.gradle | 6 +
src/main/resources/fabric.mod.json | 6 +-
10 files changed, 379 insertions(+), 198 deletions(-)
create mode 100644 fabric-1.21.11/build.gradle
create mode 100644 paper-plugin-26.2/build.gradle
diff --git a/build.gradle b/build.gradle
index fc88c26..03fe021 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,208 +1,29 @@
plugins {
- id 'fabric-loom' version "${loom_version}"
- id 'com.gradleup.shadow' version '9.2.2'
- id 'maven-publish'
+ id 'base'
}
version = project.mod_version
group = project.maven_group
-base {
- archivesName = project.archives_base_name
-}
-
-repositories {
- mavenCentral()
- // Add repositories to retrieve artifacts from in here.
- // You should only use this when depending on other mods because
- // Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
- // See https://docs.gradle.org/current/userguide/declaring_repositories.html
- // for more information about repositories.
- exclusiveContent {
- forRepository {
- maven {
- name = "Modrinth"
- url = "https://api.modrinth.com/maven"
- }
- }
- filter {
- includeGroup "maven.modrinth"
- }
- }
+allprojects {
+ version = rootProject.mod_version
+ group = rootProject.maven_group
}
subprojects {
repositories {
mavenCentral()
maven {
- name = "Fabric"
- url = "https://maven.fabricmc.net/"
- }
- }
-}
-
-configurations {
- shadowBundle
-}
-
-loom {
- splitEnvironmentSourceSets()
-
- mods {
- "videoplayer" {
- sourceSet sourceSets.main
- sourceSet sourceSets.client
- }
- }
-
-}
-
-if (findProject(":mcng-core") == null) {
- sourceSets.client.java {
- exclude "com/github/squi2rel/vp/filtergraph/MpvFilterGraphCompiler.java"
- exclude "com/github/squi2rel/vp/filtergraph/MpvFilterGraphManager.java"
- exclude "com/github/squi2rel/vp/filtergraph/MpvFilterGraphNodes.java"
- exclude "com/github/squi2rel/vp/filtergraph/MpvFilterGraphStore.java"
- exclude "com/github/squi2rel/vp/filtergraph/MpvFilterGraphTypes.java"
- exclude "com/github/squi2rel/vp/creation/MpvFilterGraphScreen.java"
- }
-}
-
-dependencies {
- // To change the versions see the gradle.properties file
- minecraft "com.mojang:minecraft:${project.minecraft_version}"
- mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
- modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
-
- // Fabric API. This is technically optional, but you probably want it anyway.
- modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
-
- implementation "net.java.dev.jna:jna:5.17.0"
- implementation "org.brotli:dec:0.1.2"
- include "org.brotli:dec:0.1.2"
- implementation "com.google.zxing:core:3.5.3"
- include "com.google.zxing:core:3.5.3"
-
- if (findProject(":mcng-core") != null) {
- implementation project(":mcng-core")
- implementation project(path: ":mcng-fabric-client", configuration: "namedElements")
- shadowBundle project(":mcng-core")
- shadowBundle project(path: ":mcng-fabric-client", configuration: "namedElements")
- }
-
- modCompileOnly "maven.modrinth:modmenu:${project.modmenu_version}"
- modApi "maven.modrinth:vivecraft:1.21.11-1.3.9-fabric"
- runtimeOnly "com.electronwill.night-config:core:3.6.6"
- runtimeOnly "com.electronwill.night-config:toml:3.6.6"
- testImplementation platform("org.junit:junit-bom:5.11.4")
- testImplementation "org.junit.jupiter:junit-jupiter"
- testRuntimeOnly "org.junit.platform:junit-platform-launcher"
-}
-
-test {
- useJUnitPlatform()
-}
-
-def bundledAndroidVlcUrl = 'https://github.com/squi2rel/VideoPlayer-Library/releases/download/runtime-20260712-064900/libvlc-android-arm64-v8a.zip'
-def bundledAndroidVlcSha256 = 'dbae70c264a9d86cd8d7fbd7ca35388cbe973de03636c08f0ac8d7cdb493f9ec'
-def bundledAndroidVlcZip = layout.buildDirectory.file('bundled-native/libvlc-android-arm64-v8a.zip')
-
-tasks.register('downloadBundledAndroidVlc') {
- inputs.property 'url', bundledAndroidVlcUrl
- inputs.property 'sha256', bundledAndroidVlcSha256
- outputs.file bundledAndroidVlcZip
- doLast {
- def target = bundledAndroidVlcZip.get().asFile
- target.parentFile.mkdirs()
- if (target.isFile()) {
- def digest = java.security.MessageDigest.getInstance('SHA-256').digest(target.bytes).encodeHex().toString()
- if (digest == bundledAndroidVlcSha256) return
- target.delete()
- }
- def temporary = new File(target.parentFile, target.name + '.tmp')
- temporary.delete()
- new URI(bundledAndroidVlcUrl).toURL().withInputStream { input ->
- temporary.withOutputStream { output -> output << input }
+ name = 'Fabric'
+ url = 'https://maven.fabricmc.net/'
}
- def digest = java.security.MessageDigest.getInstance('SHA-256').digest(temporary.bytes).encodeHex().toString()
- if (digest != bundledAndroidVlcSha256) {
- temporary.delete()
- throw new GradleException("Bundled Android VLC SHA-256 mismatch: ${digest}")
- }
- if (!temporary.renameTo(target)) throw new GradleException('Failed to store bundled Android VLC package')
- }
-}
-
-processResources {
- dependsOn tasks.named('downloadBundledAndroidVlc')
- inputs.property "version", project.version
- from(bundledAndroidVlcZip) {
- into 'assets/videoplayer/native/vlc'
- rename { 'android_arm64-v8a.zip' }
- }
-
- filesMatching("fabric.mod.json") {
- expand "version": inputs.properties.version
}
}
-tasks.withType(JavaCompile).configureEach {
- it.options.release = 21
-}
-
-java {
- // Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task
- // if it is present.
- // If you remove this line, sources will not be generated.
- withSourcesJar()
-
- sourceCompatibility = JavaVersion.VERSION_21
- targetCompatibility = JavaVersion.VERSION_21
-}
-
-jar {
- inputs.property "archivesName", project.base.archivesName
-
- from("LICENSE") {
- rename { "${it}_${inputs.properties.archivesName}"}
- }
- from("MCNG_LICENSE") {
- rename { "${it}_${inputs.properties.archivesName}"}
- }
-}
-
-shadowJar {
- archiveClassifier = "dev-shadow"
- configurations = [project.configurations.shadowBundle]
- from sourceSets.client.output
- from("LICENSE") {
- rename { "${it}_${project.base.archivesName.get()}"}
- }
- from("MCNG_LICENSE") {
- rename { "${it}_${project.base.archivesName.get()}"}
- }
- relocate "com.github.squi2rel.mcng", "com.github.squi2rel.vp.shadow.mcng"
-}
-
-remapJar {
- dependsOn shadowJar
- inputFile.set(shadowJar.archiveFile)
-}
-
-// configure the maven publication
-publishing {
- publications {
- create("mavenJava", MavenPublication) {
- artifactId = project.archives_base_name
- from components.java
- }
- }
-
- // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
- repositories {
- // Add repositories to publish to here.
- // Notice: This block does NOT have the same function as the block in the top level.
- // The repositories here will be used for publishing your artifact, not for
- // retrieving dependencies.
- }
+tasks.named('build') {
+ dependsOn ':fabric-1.21.11:build'
+ dependsOn ':paper-plugin:build'
+ dependsOn ':paper-plugin-26.2:build'
+ dependsOn ':mcng-core:build'
+ dependsOn ':mcng-fabric-client:build'
}
diff --git a/fabric-1.21.11/build.gradle b/fabric-1.21.11/build.gradle
new file mode 100644
index 0000000..d587a6b
--- /dev/null
+++ b/fabric-1.21.11/build.gradle
@@ -0,0 +1,177 @@
+plugins {
+ id 'fabric-loom' version "${loom_version}"
+ id 'com.gradleup.shadow' version '9.2.2'
+ id 'maven-publish'
+}
+
+base {
+ archivesName = rootProject.archives_base_name
+}
+
+repositories {
+ mavenCentral()
+ exclusiveContent {
+ forRepository {
+ maven {
+ name = 'Modrinth'
+ url = 'https://api.modrinth.com/maven'
+ }
+ }
+ filter {
+ includeGroup 'maven.modrinth'
+ }
+ }
+}
+
+configurations {
+ shadowBundle
+}
+
+loom {
+ splitEnvironmentSourceSets()
+ mods {
+ videoplayer {
+ sourceSet sourceSets.main
+ sourceSet sourceSets.client
+ }
+ }
+}
+
+sourceSets {
+ main {
+ java.setSrcDirs([rootProject.file('src/main/java')])
+ resources.setSrcDirs([rootProject.file('src/main/resources')])
+ }
+ client {
+ java.setSrcDirs([rootProject.file('src/client/java')])
+ resources.setSrcDirs([rootProject.file('src/client/resources')])
+ }
+ test {
+ java.setSrcDirs([rootProject.file('src/test/java')])
+ resources.setSrcDirs([rootProject.file('src/test/resources')])
+ }
+}
+
+dependencies {
+ minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
+ mappings "net.fabricmc:yarn:${rootProject.yarn_mappings}:v2"
+ modImplementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
+ modImplementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_version}"
+ implementation 'net.java.dev.jna:jna:5.17.0'
+ implementation 'org.brotli:dec:0.1.2'
+ include 'org.brotli:dec:0.1.2'
+ implementation 'com.google.zxing:core:3.5.3'
+ include 'com.google.zxing:core:3.5.3'
+ implementation project(':mcng-core')
+ implementation project(path: ':mcng-fabric-client', configuration: 'namedElements')
+ shadowBundle project(':mcng-core')
+ shadowBundle project(path: ':mcng-fabric-client', configuration: 'namedElements')
+ modCompileOnly "maven.modrinth:modmenu:${rootProject.modmenu_version}"
+ modApi 'maven.modrinth:vivecraft:1.21.11-1.3.9-fabric'
+ runtimeOnly 'com.electronwill.night-config:core:3.6.6'
+ runtimeOnly 'com.electronwill.night-config:toml:3.6.6'
+ testImplementation platform('org.junit:junit-bom:5.11.4')
+ testImplementation 'org.junit.jupiter:junit-jupiter'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}
+
+test {
+ useJUnitPlatform()
+}
+
+def bundledAndroidVlcUrl = 'https://github.com/squi2rel/VideoPlayer-Library/releases/download/runtime-20260712-064900/libvlc-android-arm64-v8a.zip'
+def bundledAndroidVlcSha256 = 'dbae70c264a9d86cd8d7fbd7ca35388cbe973de03636c08f0ac8d7cdb493f9ec'
+def bundledAndroidVlcZip = layout.buildDirectory.file('bundled-native/libvlc-android-arm64-v8a.zip')
+
+tasks.register('downloadBundledAndroidVlc') {
+ inputs.property 'url', bundledAndroidVlcUrl
+ inputs.property 'sha256', bundledAndroidVlcSha256
+ outputs.file bundledAndroidVlcZip
+ doLast {
+ def target = bundledAndroidVlcZip.get().asFile
+ target.parentFile.mkdirs()
+ if (target.isFile()) {
+ def digest = java.security.MessageDigest.getInstance('SHA-256').digest(target.bytes).encodeHex().toString()
+ if (digest == bundledAndroidVlcSha256) return
+ target.delete()
+ }
+ def temporary = new File(target.parentFile, target.name + '.tmp')
+ temporary.delete()
+ new URI(bundledAndroidVlcUrl).toURL().withInputStream { input ->
+ temporary.withOutputStream { output -> output << input }
+ }
+ def digest = java.security.MessageDigest.getInstance('SHA-256').digest(temporary.bytes).encodeHex().toString()
+ if (digest != bundledAndroidVlcSha256) {
+ temporary.delete()
+ throw new GradleException("Bundled Android VLC SHA-256 mismatch: ${digest}")
+ }
+ if (!temporary.renameTo(target)) throw new GradleException('Failed to store bundled Android VLC package')
+ }
+}
+
+processResources {
+ dependsOn tasks.named('downloadBundledAndroidVlc')
+ inputs.properties([
+ version: project.version,
+ loader_min_version: rootProject.loader_version,
+ minecraft_dependency: '~1.21.11',
+ java_version: '21'
+ ])
+ from(bundledAndroidVlcZip) {
+ into 'assets/videoplayer/native/vlc'
+ rename { 'android_arm64-v8a.zip' }
+ }
+ filesMatching('fabric.mod.json') {
+ expand inputs.properties
+ }
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.release = 21
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+ withSourcesJar()
+ sourceCompatibility = JavaVersion.VERSION_21
+ targetCompatibility = JavaVersion.VERSION_21
+}
+
+jar {
+ inputs.property 'archivesName', project.base.archivesName
+ from(rootProject.file('LICENSE')) {
+ rename { "${it}_${inputs.properties.archivesName}" }
+ }
+ from(rootProject.file('MCNG_LICENSE')) {
+ rename { "${it}_${inputs.properties.archivesName}" }
+ }
+}
+
+shadowJar {
+ archiveClassifier = 'dev-shadow'
+ configurations = [project.configurations.shadowBundle]
+ from sourceSets.client.output
+ from(rootProject.file('LICENSE')) {
+ rename { "${it}_${project.base.archivesName.get()}" }
+ }
+ from(rootProject.file('MCNG_LICENSE')) {
+ rename { "${it}_${project.base.archivesName.get()}" }
+ }
+ relocate 'com.github.squi2rel.mcng', 'com.github.squi2rel.vp.shadow.mcng'
+}
+
+remapJar {
+ dependsOn shadowJar
+ inputFile.set(shadowJar.archiveFile)
+}
+
+publishing {
+ publications {
+ create('mavenJava', MavenPublication) {
+ artifactId = rootProject.archives_base_name
+ from components.java
+ }
+ }
+}
diff --git a/gradle.properties b/gradle.properties
index f870ac5..b03cbe8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,7 +7,7 @@ org.gradle.parallel=true
minecraft_version=1.21.11
yarn_mappings=1.21.11+build.6
loader_version=0.17.3
-loom_version=1.17.12
+loom_version=1.17.17
# Mod Properties
mod_version=2.0.2
@@ -18,3 +18,10 @@ archives_base_name=VideoPlayer
fabric_version=0.141.4+1.21.11
modmenu_version=17.0.0
paper_api_version=1.21.11-R0.1-SNAPSHOT
+paper_api_26_2_version=26.2.build.92-stable
+minecraft_26_2_version=26.2
+loader_26_2_version=0.19.3
+fabric_26_2_version=0.156.0+26.2
+modmenu_26_2_version=20.0.1
+vivecraft_26_2_version=26.2-1.3.15-fabric
+mockito_version=5.23.0
diff --git a/mcng-core/build.gradle b/mcng-core/build.gradle
index b92c3e1..cd00ed4 100644
--- a/mcng-core/build.gradle
+++ b/mcng-core/build.gradle
@@ -20,6 +20,9 @@ tasks.withType(JavaCompile).configureEach {
}
java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
diff --git a/mcng-fabric-client/build.gradle b/mcng-fabric-client/build.gradle
index 429acc1..104fdfd 100644
--- a/mcng-fabric-client/build.gradle
+++ b/mcng-fabric-client/build.gradle
@@ -28,6 +28,9 @@ tasks.withType(JavaCompile).configureEach {
}
java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
diff --git a/paper-plugin-26.2/build.gradle b/paper-plugin-26.2/build.gradle
new file mode 100644
index 0000000..64e3387
--- /dev/null
+++ b/paper-plugin-26.2/build.gradle
@@ -0,0 +1,160 @@
+plugins {
+ id 'java'
+ id 'com.gradleup.shadow' version '9.2.2'
+}
+
+base {
+ archivesName = "${rootProject.archives_base_name}-Paper"
+}
+
+repositories {
+ mavenCentral()
+ maven {
+ name = 'Paper'
+ url = 'https://repo.papermc.io/repository/maven-public/'
+ }
+}
+
+sourceSets {
+ main {
+ java.setSrcDirs([rootProject.file('paper-plugin/src/main/java')])
+ resources.setSrcDirs([rootProject.file('paper-plugin/src/main/resources')])
+ }
+ test {
+ java.setSrcDirs([rootProject.file('paper-plugin/src/test/java')])
+ resources.setSrcDirs([rootProject.file('paper-plugin/src/test/resources')])
+ }
+}
+
+def sharedMainSources = fileTree("${rootProject.projectDir}/src/main/java") {
+ exclude 'android/**'
+ exclude 'com/github/squi2rel/vp/VideoPlayerMain.java'
+ exclude 'com/github/squi2rel/vp/DataHolder.java'
+ exclude 'com/github/squi2rel/vp/network/ServerPacketHandler.java'
+ exclude 'com/github/squi2rel/vp/network/VideoPayload.java'
+ exclude 'com/github/squi2rel/vp/provider/PlayerProviderSource.java'
+ exclude 'com/github/squi2rel/vp/permission/VideoPermissions.java'
+ exclude 'com/github/squi2rel/vp/video/ScreenBroadcaster.java'
+ exclude 'com/github/squi2rel/vp/video/VideoArea.java'
+ exclude 'com/github/squi2rel/vp/i18n/MinecraftTexts.java'
+}
+
+dependencies {
+ compileOnly "io.papermc.paper:paper-api:${rootProject.paper_api_26_2_version}"
+ compileOnly 'org.jetbrains:annotations:26.0.2'
+ compileOnly 'org.slf4j:slf4j-api:2.0.16'
+ implementation 'io.netty:netty-buffer:4.1.118.Final'
+ implementation 'org.joml:joml:1.10.8'
+ implementation 'com.google.code.gson:gson:2.11.0'
+ implementation 'net.java.dev.jna:jna:5.17.0'
+ implementation 'org.brotli:dec:0.1.2'
+ testImplementation platform('org.junit:junit-bom:5.11.4')
+ testImplementation 'org.junit.jupiter:junit-jupiter'
+ testImplementation "org.mockito:mockito-core:${rootProject.mockito_version}"
+ testImplementation "io.papermc.paper:paper-api:${rootProject.paper_api_26_2_version}"
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}
+
+tasks.test {
+ useJUnitPlatform()
+ workingDir rootProject.file('paper-plugin')
+}
+
+def bundledMpvUrl = 'https://github.com/squi2rel/VideoPlayer-Library/releases/download/runtime-20260712-064900/libmpv-windows-x64.zip'
+def bundledMpvSha256 = '0a1e614d3b3db315895d19b1e97013fd12da9bc20c50d02d5de3b71a959dfdfb'
+def bundledMpvZip = layout.buildDirectory.file('bundled-native/libmpv-windows-x64.zip')
+
+tasks.register('downloadBundledWindowsMpv') {
+ inputs.property 'url', bundledMpvUrl
+ inputs.property 'sha256', bundledMpvSha256
+ outputs.file bundledMpvZip
+ doLast {
+ def target = bundledMpvZip.get().asFile
+ target.parentFile.mkdirs()
+ if (target.isFile()) {
+ def digest = java.security.MessageDigest.getInstance('SHA-256').digest(target.bytes).encodeHex().toString()
+ if (digest == bundledMpvSha256) return
+ target.delete()
+ }
+ def temporary = new File(target.parentFile, target.name + '.tmp')
+ temporary.delete()
+ new URI(bundledMpvUrl).toURL().withInputStream { input ->
+ temporary.withOutputStream { output -> output << input }
+ }
+ def digest = java.security.MessageDigest.getInstance('SHA-256').digest(temporary.bytes).encodeHex().toString()
+ if (digest != bundledMpvSha256) {
+ temporary.delete()
+ throw new GradleException("Bundled MPV SHA-256 mismatch: ${digest}")
+ }
+ if (!temporary.renameTo(target)) throw new GradleException('Failed to store bundled MPV package')
+ }
+}
+
+tasks.processResources {
+ dependsOn tasks.named('downloadBundledWindowsMpv')
+ inputs.property 'version', project.version
+ inputs.property 'apiVersion', '26.2'
+ from("${rootProject.projectDir}/src/main/resources/assets/videoplayer/native-downloads.json") {
+ into 'assets/videoplayer'
+ }
+ from(bundledMpvZip) {
+ into 'assets/videoplayer/native'
+ }
+ filesMatching('plugin.yml') {
+ expand 'version': project.version, 'apiVersion': '26.2'
+ }
+}
+
+tasks.withType(JavaCompile).configureEach {
+ options.release = 25
+ options.encoding = 'UTF-8'
+}
+
+tasks.named('compileJava', JavaCompile) {
+ source sharedMainSources
+}
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(25)
+ }
+ sourceCompatibility = JavaVersion.VERSION_25
+ targetCompatibility = JavaVersion.VERSION_25
+}
+
+tasks.jar {
+ archiveClassifier = 'plain'
+ archiveVersion = "${project.version}-26.2"
+}
+
+tasks.shadowJar {
+ archiveClassifier = ''
+ archiveVersion = "${project.version}-26.2"
+ mergeServiceFiles()
+ relocate 'com.google.gson', 'com.github.squi2rel.vp.paper.shadow.gson'
+ relocate 'io.netty', 'com.github.squi2rel.vp.paper.shadow.netty'
+ relocate 'org.brotli', 'com.github.squi2rel.vp.paper.shadow.brotli'
+ relocate 'org.joml', 'com.github.squi2rel.vp.paper.shadow.joml'
+}
+
+def shadowJarTask = tasks.named('shadowJar')
+
+tasks.register('verifyPaperPluginDescriptor') {
+ dependsOn shadowJarTask
+ inputs.file(shadowJarTask.flatMap { it.archiveFile })
+ doLast {
+ def archive = shadowJarTask.get().archiveFile.get().asFile
+ java.util.jar.JarFile jar = new java.util.jar.JarFile(archive)
+ try {
+ if (jar.getJarEntry('plugin.yml') == null) {
+ throw new GradleException("Paper plugin archive does not contain plugin.yml: ${archive}")
+ }
+ } finally {
+ jar.close()
+ }
+ }
+}
+
+tasks.build {
+ dependsOn tasks.verifyPaperPluginDescriptor
+}
diff --git a/paper-plugin/build.gradle b/paper-plugin/build.gradle
index 8b8c55f..7ac99e2 100644
--- a/paper-plugin/build.gradle
+++ b/paper-plugin/build.gradle
@@ -55,7 +55,7 @@ dependencies {
testImplementation platform("org.junit:junit-bom:5.11.4")
testImplementation "org.junit.jupiter:junit-jupiter"
- testImplementation "org.mockito:mockito-core:5.15.2"
+ testImplementation "org.mockito:mockito-core:${rootProject.mockito_version}"
testImplementation "io.papermc.paper:paper-api:${rootProject.paper_api_version}"
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
}
@@ -95,6 +95,7 @@ tasks.register('downloadBundledWindowsMpv') {
tasks.processResources {
dependsOn tasks.named('downloadBundledWindowsMpv')
inputs.property "version", project.version
+ inputs.property "apiVersion", '1.21'
from("${rootProject.projectDir}/src/main/resources/assets/videoplayer/native-downloads.json") {
into 'assets/videoplayer'
}
@@ -102,7 +103,7 @@ tasks.processResources {
into 'assets/videoplayer/native'
}
filesMatching("plugin.yml") {
- expand "version": project.version
+ expand "version": project.version, "apiVersion": '1.21'
}
}
@@ -118,6 +119,9 @@ tasks.withType(JavaCompile).configureEach {
}
java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
diff --git a/paper-plugin/src/main/resources/plugin.yml b/paper-plugin/src/main/resources/plugin.yml
index b8c0682..a4afa12 100644
--- a/paper-plugin/src/main/resources/plugin.yml
+++ b/paper-plugin/src/main/resources/plugin.yml
@@ -1,7 +1,7 @@
name: VideoPlayer
main: com.github.squi2rel.vp.VideoPlayerPaperPlugin
version: ${version}
-api-version: '1.21'
+api-version: '${apiVersion}'
folia-supported: true
softdepend: [Residence]
authors: [squi2rel, cloudfl4re]
diff --git a/settings.gradle b/settings.gradle
index 13e4c0e..b3ece1a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -9,7 +9,13 @@ pluginManagement {
}
}
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
+}
+
rootProject.name = "VideoPlayer"
include "paper-plugin"
+include "paper-plugin-26.2"
+include "fabric-1.21.11"
include "mcng-core"
include "mcng-fabric-client"
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index b1f4855..beab1ec 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -33,9 +33,9 @@
}
],
"depends": {
- "fabricloader": ">=0.17.3",
- "minecraft": "~1.21.11",
- "java": ">=21",
+ "fabricloader": ">=${loader_min_version}",
+ "minecraft": "${minecraft_dependency}",
+ "java": ">=${java_version}",
"fabric-api": "*"
},
"suggests": {
From 58bd8a6c4933142bba4e41be3775d7c2ac208916 Mon Sep 17 00:00:00 2001
From: cloudfl4re
Date: Wed, 5 Aug 2026 19:48:37 +0800
Subject: [PATCH 08/23] =?UTF-8?q?refactor:=20=E8=BF=81=E7=A7=BB=201.21.11?=
=?UTF-8?q?=20=E8=87=B3=20Mojang=20=E5=AE=98=E6=96=B9=E6=98=A0=E5=B0=84?=
=?UTF-8?q?=EF=BC=88=E4=BB=BB=E5=8A=A1=203/7=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
build.gradle | 2 +
fabric-1.21.11/build.gradle | 6 +-
mcng-fabric-client/build.gradle | 4 +-
.../mcng/fabric/client/EdgeRenderer.java | 19 +-
.../fabric/client/EditorStyleRenderer.java | 20 +-
.../fabric/client/GraphCanvasComponent.java | 58 +--
.../client/GraphContextMenuComponent.java | 17 +-
.../fabric/client/GraphCursorManager.java | 8 +-
.../fabric/client/GraphEditorComponent.java | 26 +-
.../fabric/client/GraphInputModifiers.java | 12 +-
.../mcng/fabric/client/GraphInputText.java | 6 +-
.../client/GraphTextFieldComponent.java | 24 +-
.../fabric/client/GraphTextInputRenderer.java | 34 +-
.../fabric/client/GraphTextInputState.java | 13 +-
.../client/ImagePreviewNodeBodyComponent.java | 77 ++--
.../mcng/fabric/client/MCNGDebugScreen.java | 86 ++---
.../fabric/client/NodeBodyRenderContext.java | 9 +-
.../fabric/client/NodePaletteComponent.java | 26 +-
.../fabric/client/NodeWidgetRenderer.java | 83 +++--
.../GraphEditorComponentPlacementTest.java | 1 +
.../client/GraphEditorComponentTest.java | 1 +
.../client/MCNGDebugScreenLayoutTest.java | 3 +-
.../NodeBodyComponentIntegrationTest.java | 1 +
.../fabric/client/NodePaletteCatalogTest.java | 1 +
.../client/NodePaletteComponentTest.java | 1 +
.../client/NodeWidgetInlineLayoutTest.java | 2 +
paper-plugin-26.2/build.gradle | 2 +-
paper-plugin/build.gradle | 2 +-
.../github/squi2rel/vp/CameraRenderer.java | 20 +-
.../squi2rel/vp/ClientPacketHandler.java | 41 ++-
.../squi2rel/vp/ClientYtDlpInstaller.java | 4 +-
.../github/squi2rel/vp/ScreenRenderer.java | 222 ++++++------
.../github/squi2rel/vp/VideoPlayerClient.java | 203 +++++------
.../squi2rel/vp/creation/BiliLoginScreen.java | 63 ++--
.../vp/creation/IdlePlayListScreen.java | 117 +++----
.../vp/creation/MpvFilterGraphScreen.java | 58 +--
.../vp/creation/SelectionPreviewRenderer.java | 120 ++++---
.../vp/creation/StartupGuideScreen.java | 153 ++++----
.../vp/creation/VideoCreationEditor.java | 170 ++++-----
.../vp/creation/VideoCreationScreen.java | 125 ++++---
.../vp/creation/VideoManagementScreen.java | 330 +++++++++---------
.../vp/creation/VideoMappingScreen.java | 113 +++---
.../squi2rel/vp/creation/VpButtonWidget.java | 61 ++--
.../vp/creation/VpProgressSliderWidget.java | 51 ++-
.../squi2rel/vp/creation/VpSliderWidget.java | 41 ++-
.../vp/creation/VpTextFieldWidget.java | 104 +++---
.../squi2rel/vp/creation/VpUiRenderer.java | 6 +-
.../vp/creation/YouTubeAuthScreen.java | 77 ++--
.../vp/danmaku/ClientDanmakuController.java | 7 +-
.../vp/danmaku/ClientDanmakuRenderer.java | 196 +++++------
.../vp/danmaku/DanmakuTextLayoutCache.java | 39 +--
.../github/squi2rel/vp/i18n/VpInputTexts.java | 12 +-
.../com/github/squi2rel/vp/i18n/VpTexts.java | 14 +-
.../client/ClientPlayNetworkHandlerMixin.java | 6 +-
.../vp/mixin/client/DrawContextAccessor.java | 6 +-
.../vp/mixin/client/GameRendererAccessor.java | 4 +-
.../vp/mixin/client/GameRendererMixin.java | 10 +-
.../mixin/client/MinecraftClientAccessor.java | 14 +-
.../vp/mixin/client/MinecraftClientMixin.java | 6 +-
.../squi2rel/vp/mixin/client/WindowMixin.java | 6 +-
.../vp/mixin/client/WorldRendererMixin.java | 6 +-
.../vp/video/AbstractCameraPlayer.java | 36 +-
.../squi2rel/vp/video/ClientVideoScreen.java | 16 +-
.../github/squi2rel/vp/video/ClonePlayer.java | 2 +-
.../squi2rel/vp/video/Degree360Player.java | 64 ++--
.../squi2rel/vp/video/EntityCameraPlayer.java | 13 +-
.../squi2rel/vp/video/ExternalGlTexture.java | 18 +-
.../vp/video/FramebufferBackedTexture.java | 22 +-
.../squi2rel/vp/video/IVideoPlayer.java | 8 +-
.../squi2rel/vp/video/MpvVideoBackend.java | 23 +-
.../github/squi2rel/vp/video/PBOManager.java | 4 +-
.../vp/video/UnavailableVideoBackend.java | 14 +-
.../github/squi2rel/vp/video/VideoPlayer.java | 10 +-
.../vp/video/VideoPlayerRenderer.java | 28 +-
.../github/squi2rel/vp/video/VideoQuad.java | 4 +-
.../com/github/squi2rel/vp/DataHolder.java | 95 +++--
.../github/squi2rel/vp/VideoPlayerMain.java | 14 +-
.../squi2rel/vp/i18n/MinecraftTexts.java | 14 +-
.../vp/network/ServerPacketHandler.java | 174 ++++-----
.../squi2rel/vp/network/VideoPayload.java | 18 +-
.../vp/permission/VideoPermissions.java | 12 +-
.../vp/provider/PlayerProviderSource.java | 9 +-
.../squi2rel/vp/video/MpvStreamListener.java | 3 +
.../squi2rel/vp/video/ScreenBroadcaster.java | 15 +-
.../github/squi2rel/vp/video/VideoArea.java | 4 +-
.../vp/provider/LocalPlaybackInfoTest.java | 1 +
.../vp/provider/YouTubeProviderTest.java | 1 +
.../vp/video/OrderedPlayAdmissionsTest.java | 1 +
.../vp/video/PlaybackControllerTest.java | 1 +
.../vp/video/VideoAreaBoundsTest.java | 13 +-
.../VideoScreenIdlePlayAntiRepeatTest.java | 1 +
.../VideoScreenIdlePlayPriorityTest.java | 1 +
.../VideoScreenPlaylistPersistenceTest.java | 1 +
93 files changed, 1801 insertions(+), 1788 deletions(-)
diff --git a/build.gradle b/build.gradle
index 03fe021..4b4d922 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,7 @@
plugins {
id 'base'
+ id 'fabric-loom' version "${loom_version}" apply false
+ id 'com.gradleup.shadow' version '9.2.2' apply false
}
version = project.mod_version
diff --git a/fabric-1.21.11/build.gradle b/fabric-1.21.11/build.gradle
index d587a6b..ce09330 100644
--- a/fabric-1.21.11/build.gradle
+++ b/fabric-1.21.11/build.gradle
@@ -1,6 +1,6 @@
plugins {
- id 'fabric-loom' version "${loom_version}"
- id 'com.gradleup.shadow' version '9.2.2'
+ id 'fabric-loom'
+ id 'com.gradleup.shadow'
id 'maven-publish'
}
@@ -54,7 +54,7 @@ sourceSets {
dependencies {
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
- mappings "net.fabricmc:yarn:${rootProject.yarn_mappings}:v2"
+ mappings loom.officialMojangMappings()
modImplementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
modImplementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_version}"
implementation 'net.java.dev.jna:jna:5.17.0'
diff --git a/mcng-fabric-client/build.gradle b/mcng-fabric-client/build.gradle
index 104fdfd..188ae47 100644
--- a/mcng-fabric-client/build.gradle
+++ b/mcng-fabric-client/build.gradle
@@ -1,5 +1,5 @@
plugins {
- id 'fabric-loom' version "${loom_version}"
+ id 'fabric-loom'
id 'maven-publish'
}
@@ -9,7 +9,7 @@ base {
dependencies {
minecraft "com.mojang:minecraft:${project.minecraft_version}"
- mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
+ mappings loom.officialMojangMappings()
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
implementation project(':mcng-core')
diff --git a/mcng-fabric-client/src/main/java/com/github/squi2rel/mcng/fabric/client/EdgeRenderer.java b/mcng-fabric-client/src/main/java/com/github/squi2rel/mcng/fabric/client/EdgeRenderer.java
index d3aa2f8..4135338 100644
--- a/mcng-fabric-client/src/main/java/com/github/squi2rel/mcng/fabric/client/EdgeRenderer.java
+++ b/mcng-fabric-client/src/main/java/com/github/squi2rel/mcng/fabric/client/EdgeRenderer.java
@@ -1,9 +1,8 @@
package com.github.squi2rel.mcng.fabric.client;
-import net.minecraft.client.gui.DrawContext;
-
import java.util.ArrayList;
import java.util.List;
+import net.minecraft.client.gui.GuiGraphics;
public final class EdgeRenderer {
private static final int MIN_CONTROL_OFFSET = 28;
@@ -16,7 +15,7 @@ private EdgeRenderer() {
}
public static void drawEdge(
- DrawContext context,
+ GuiGraphics context,
int startX,
int startY,
int endX,
@@ -34,12 +33,12 @@ public static void drawEdge(
}
}
- private static void drawStraight(DrawContext context, int startX, int startY, int endX, int endY, int startColor, int endColor) {
+ private static void drawStraight(GuiGraphics context, int startX, int startY, int endX, int endY, int startColor, int endColor) {
drawGradientSegment(context, startX, startY, endX, endY, startColor, endColor, 0.0, 1.0);
}
private static void drawOrthogonal(
- DrawContext context,
+ GuiGraphics context,
int startX,
int startY,
int endX,
@@ -74,7 +73,7 @@ private static void drawOrthogonal(
}
private static void drawCurve(
- DrawContext context,
+ GuiGraphics context,
int startX,
int startY,
int endX,
@@ -113,7 +112,7 @@ static CurveControls curveControls(int startX, int startY, int endX, int endY, N
);
}
- private static void drawGradientPolyline(DrawContext context, List points, int startColor, int endColor) {
+ private static void drawGradientPolyline(GuiGraphics context, List points, int startColor, int endColor) {
double totalLength = 0.0;
for (int index = 1; index < points.size(); index++) {
totalLength += distance(points.get(index - 1), points.get(index));
@@ -134,7 +133,7 @@ private static void drawGradientPolyline(DrawContext context, List points
}
}
- private static void drawGradientOrthogonalPolyline(DrawContext context, List points, int startColor, int endColor) {
+ private static void drawGradientOrthogonalPolyline(GuiGraphics context, List points, int startColor, int endColor) {
double totalLength = 0.0;
for (int index = 1; index < points.size(); index++) {
totalLength += distance(points.get(index - 1), points.get(index));
@@ -159,7 +158,7 @@ private static void drawGradientOrthogonalPolyline(DrawContext context, List uiConfigSupplier;
private final NodeComponentRegistry componentRegistry;
private final Map bodyComponents = new LinkedHashMap<>();
- private TextRenderer textRenderer;
+ private Font textRenderer;
private ActiveTextEdit activeTextEdit;
private NodeId focusedBodyNodeId;
private CapturedBodyInteraction capturedBodyInteraction;
@@ -52,7 +52,7 @@ public GraphCanvasComponent(GraphEditorSession session, GraphInteractionControll
viewport.reset();
}
- public void init(TextRenderer textRenderer, GraphEditorBounds bounds) {
+ public void init(Font textRenderer, GraphEditorBounds bounds) {
this.textRenderer = textRenderer;
setBounds(bounds);
}
@@ -93,7 +93,7 @@ public boolean contains(double mouseX, double mouseY) {
return bounds.contains(mouseX, mouseY);
}
- public void render(DrawContext context, TextRenderer textRenderer, int mouseX, int mouseY) {
+ public void render(GuiGraphics context, Font textRenderer, int mouseX, int mouseY) {
this.textRenderer = textRenderer;
GraphEditorUiConfig uiConfig = uiConfigSupplier.get();
GraphEditorTheme theme = uiConfig.theme();
@@ -102,10 +102,10 @@ public void render(DrawContext context, TextRenderer textRenderer, int mouseX, i
context.enableScissor(bounds.x(), bounds.y(), bounds.right(), bounds.bottom());
try {
List widgets = widgets();
- context.getMatrices().pushMatrix();
- context.getMatrices().translate(bounds.x(), bounds.y());
- context.getMatrices().translate((float) viewport.offsetX(), (float) viewport.offsetY());
- context.getMatrices().scale((float) viewport.zoom(), (float) viewport.zoom());
+ context.pose().pushMatrix();
+ context.pose().translate(bounds.x(), bounds.y());
+ context.pose().translate((float) viewport.offsetX(), (float) viewport.offsetY());
+ context.pose().scale((float) viewport.zoom(), (float) viewport.zoom());
try {
for (EdgeDefinition edge : session.edges()) {
NodeWidget fromNode = widgets.stream().filter(widget -> widget.node().id().equals(edge.fromNodeId())).findFirst().orElse(null);
@@ -187,7 +187,7 @@ public void render(DrawContext context, TextRenderer textRenderer, int mouseX, i
EditorStyleRenderer.drawBorder(context, (int) selectionBox.minX(), (int) selectionBox.minY(), (int) Math.max(1, selectionBox.maxX() - selectionBox.minX()), (int) Math.max(1, selectionBox.maxY() - selectionBox.minY()), 0xFF6EA8FF);
}
} finally {
- context.getMatrices().popMatrix();
+ context.pose().popMatrix();
}
} finally {
context.disableScissor();
@@ -549,10 +549,10 @@ public List nodesInRect(double minX, double minY, double maxX, double ma
return nodeIds;
}
- private void renderGrid(DrawContext context, GraphEditorTheme theme) {
+ private void renderGrid(GuiGraphics context, GraphEditorTheme theme) {
context.fill(bounds.x(), bounds.y(), bounds.right(), bounds.bottom(), theme.canvasBackgroundColor());
- context.getMatrices().pushMatrix();
- context.getMatrices().translate(bounds.x(), bounds.y());
+ context.pose().pushMatrix();
+ context.pose().translate(bounds.x(), bounds.y());
try {
double step = Math.max(8, 24 * viewport.zoom());
double startX = viewport.offsetX() % step;
@@ -564,24 +564,24 @@ private void renderGrid(DrawContext context, GraphEditorTheme theme) {
context.fill(0, (int) y, bounds.width(), (int) y + 1, theme.gridColor());
}
} finally {
- context.getMatrices().popMatrix();
+ context.pose().popMatrix();
}
}
- private void renderActiveTextEditor(DrawContext context, TextRenderer textRenderer, GraphEditorTheme theme, GraphEditorUiConfig uiConfig) {
+ private void renderActiveTextEditor(GuiGraphics context, Font textRenderer, GraphEditorTheme theme, GraphEditorUiConfig uiConfig) {
if (activeTextEdit == null) {
return;
}
context.enableScissor(bounds.x(), bounds.y(), bounds.right(), bounds.bottom());
try {
- context.getMatrices().pushMatrix();
+ context.pose().pushMatrix();
try {
- context.getMatrices().translate(bounds.x(), bounds.y());
- context.getMatrices().translate((float) viewport.offsetX(), (float) viewport.offsetY());
- context.getMatrices().scale((float) viewport.zoom(), (float) viewport.zoom());
+ context.pose().translate(bounds.x(), bounds.y());
+ context.pose().translate((float) viewport.offsetX(), (float) viewport.offsetY());
+ context.pose().scale((float) viewport.zoom(), (float) viewport.zoom());
GraphTextInputRenderer.renderFrame(context, activeTextEdit.bounds(), theme, uiConfig);
} finally {
- context.getMatrices().popMatrix();
+ context.pose().popMatrix();
}
NodeWidget.Bounds screenBounds = screenBounds(activeTextEdit.bounds());
@@ -591,14 +591,14 @@ private void renderActiveTextEditor(DrawContext context, TextRenderer textRender
int scissorBottom = Math.min(bounds.bottom(), screenBounds.y() + screenBounds.height() - GraphTextInputRenderer.CONTENT_PADDING_Y);
context.enableScissor(scissorLeft, scissorTop, scissorRight, scissorBottom);
try {
- context.getMatrices().pushMatrix();
+ context.pose().pushMatrix();
try {
- context.getMatrices().translate(bounds.x(), bounds.y());
- context.getMatrices().translate((float) viewport.offsetX(), (float) viewport.offsetY());
- context.getMatrices().scale((float) viewport.zoom(), (float) viewport.zoom());
+ context.pose().translate(bounds.x(), bounds.y());
+ context.pose().translate((float) viewport.offsetX(), (float) viewport.offsetY());
+ context.pose().scale((float) viewport.zoom(), (float) viewport.zoom());
GraphTextInputRenderer.renderContent(context, textRenderer, activeTextEdit.bounds(), activeTextEdit.state(), theme, true);
} finally {
- context.getMatrices().popMatrix();
+ context.pose().popMatrix();
}
} finally {
context.disableScissor();
@@ -1008,7 +1008,7 @@ private GraphTextInputState state() {
return state;
}
- private void handlePointerDown(TextRenderer textRenderer, double worldX, long timeMs) {
+ private void handlePointerDown(Font textRenderer, double worldX, long timeMs) {
int index = indexForWorldX(textRenderer, worldX);
if ((timeMs - lastPointerDownAt) <= DOUBLE_CLICK_WINDOW_MS && Math.abs(index - lastPointerIndex) <= 1) {
state.selectWordAt(index);
@@ -1022,7 +1022,7 @@ private void handlePointerDown(TextRenderer textRenderer, double worldX, long ti
ensureCursorVisible(textRenderer);
}
- private void handlePointerDrag(TextRenderer textRenderer, double worldX) {
+ private void handlePointerDrag(Font textRenderer, double worldX) {
if (!draggingPointer) {
return;
}
@@ -1034,7 +1034,7 @@ private void finishPointerDrag() {
draggingPointer = false;
}
- private void ensureCursorVisible(TextRenderer textRenderer) {
+ private void ensureCursorVisible(Font textRenderer) {
state.ensureCursorVisible(textRenderer, Math.max(1, bounds.width() - 8));
}
@@ -1046,7 +1046,7 @@ private boolean matchesControl(NodeId nodeId, String key) {
return this.nodeId.equals(nodeId) && this.key != null && this.key.equals(key);
}
- private int indexForWorldX(TextRenderer textRenderer, double worldX) {
+ private int indexForWorldX(Font textRenderer, double worldX) {
double localX = worldX - (bounds.x() + 4) + state.scrollX();
return state.indexForX(textRenderer, localX);
}
diff --git a/mcng-fabric-client/src/main/java/com/github/squi2rel/mcng/fabric/client/GraphContextMenuComponent.java b/mcng-fabric-client/src/main/java/com/github/squi2rel/mcng/fabric/client/GraphContextMenuComponent.java
index 5b59959..315f9ad 100644
--- a/mcng-fabric-client/src/main/java/com/github/squi2rel/mcng/fabric/client/GraphContextMenuComponent.java
+++ b/mcng-fabric-client/src/main/java/com/github/squi2rel/mcng/fabric/client/GraphContextMenuComponent.java
@@ -1,10 +1,9 @@
package com.github.squi2rel.mcng.fabric.client;
-import net.minecraft.client.font.TextRenderer;
-import net.minecraft.client.gui.DrawContext;
-
import java.util.List;
import java.util.function.Supplier;
+import net.minecraft.client.gui.Font;
+import net.minecraft.client.gui.GuiGraphics;
public final class GraphContextMenuComponent {
private static final int PADDING = 4;
@@ -30,7 +29,7 @@ public GraphContextMenuComponent(Supplier uiConfigSupplier,
this.i18nSupplier = i18nSupplier;
}
- public void openNodeMenu(double anchorScreenX, double anchorScreenY, GraphEditorBounds bounds, TextRenderer textRenderer) {
+ public void openNodeMenu(double anchorScreenX, double anchorScreenY, GraphEditorBounds bounds, Font textRenderer) {
open(anchorScreenX, anchorScreenY, bounds, textRenderer, List.of(
new MenuItem(GraphEditorTranslations.ui(i18nSupplier.get(), "context_menu.copy", "Copy"), MenuAction.COPY, true),
new MenuItem(GraphEditorTranslations.ui(i18nSupplier.get(), "context_menu.cut", "Cut"), MenuAction.CUT, true),
@@ -38,7 +37,7 @@ public void openNodeMenu(double anchorScreenX, double anchorScreenY, GraphEditor
));
}
- public void openCanvasMenu(double anchorScreenX, double anchorScreenY, GraphEditorBounds bounds, TextRenderer textRenderer, boolean canPaste) {
+ public void openCanvasMenu(double anchorScreenX, double anchorScreenY, GraphEditorBounds bounds, Font textRenderer, boolean canPaste) {
open(anchorScreenX, anchorScreenY, bounds, textRenderer, List.of(new MenuItem(GraphEditorTranslations.ui(i18nSupplier.get(), "context_menu.paste", "Paste"), MenuAction.PASTE, canPaste)));
}
@@ -67,7 +66,7 @@ public MenuAction mouseClicked(double mouseX, double mouseY, int button) {
return item.action();
}
- public void render(DrawContext context, TextRenderer textRenderer) {
+ public void render(GuiGraphics context, Font textRenderer) {
if (!open) {
return;
}
@@ -83,7 +82,7 @@ public void render(DrawContext context, TextRenderer textRenderer) {
: EditorStyleRenderer.darken(theme.panelBackgroundColor(), 0.08f);
int border = item.active() ? theme.panelBorderColor() : EditorStyleRenderer.darken(theme.panelBorderColor(), 0.25f);
EditorStyleRenderer.drawBox(context, x + PADDING, itemY, width - (PADDING * 2), ITEM_HEIGHT - 2, fill, border, uiConfig);
- context.drawText(textRenderer, item.label(), x + PADDING + ITEM_PADDING_X, itemY + 6, item.active() ? theme.primaryTextColor() : theme.secondaryTextColor(), false);
+ context.drawString(textRenderer, item.label(), x + PADDING + ITEM_PADDING_X, itemY + 6, item.active() ? theme.primaryTextColor() : theme.secondaryTextColor(), false);
}
}
@@ -95,11 +94,11 @@ public void render(DrawContext context, TextRenderer textRenderer) {
return anchorScreenY;
}
- private void open(double anchorScreenX, double anchorScreenY, GraphEditorBounds bounds, TextRenderer textRenderer, List