Skip to content

Commit b4932c0

Browse files
committed
Add scaffolding to start FSC in arbitrary stages without code changes.
Note: there is no config wired in, so it's effectively hardcoded to always start at PLAY or LOGIN (effectively, immediately), depending on if it's connecting to an old Fireblanket server, or a matching server. Some versions may still yield a crash & kick due to desynced packet expectations, but this aims to clear that up. The client will pick the earliest possible entry point presented by the server. The server currently has no ordering guarantees, and may select any random order at boot time. Should FSC be broken by a port or misbehaving mod, or partially disabled by SCRAM: - The client will always fail to understand the packet and proceed as if FSC did not exist. - The server will always disconnect to any request that was understood.
1 parent 7849917 commit b4932c0

10 files changed

Lines changed: 181 additions & 18 deletions

src/main/java/net/modfest/fireblanket/Fireblanket.java

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import net.minecraft.core.Registry;
2525
import net.minecraft.core.registries.BuiltInRegistries;
2626
import net.minecraft.network.Connection;
27+
import net.minecraft.network.chat.Component;
2728
import net.minecraft.network.protocol.Packet;
2829
import net.minecraft.resources.Identifier;
2930
import net.minecraft.server.level.ServerLevel;
@@ -51,6 +52,7 @@
5152
import net.modfest.fireblanket.net.BatchedBEUpdatePayload;
5253
import net.modfest.fireblanket.net.BatchedEntityVelocityUpdatePacket;
5354
import net.modfest.fireblanket.net.CommandBlockPacket;
55+
import net.modfest.fireblanket.net.NetworkState;
5456
import net.modfest.fireblanket.util.LinkedBlocQueue;
5557
import net.modfest.fireblanket.world.ItemBan;
5658
import net.modfest.fireblanket.world.blocks.UpdateSignBlockEntityTypes;
@@ -60,6 +62,7 @@
6062
import org.slf4j.Logger;
6163
import org.slf4j.LoggerFactory;
6264

65+
import java.nio.charset.StandardCharsets;
6366
import java.util.concurrent.atomic.AtomicInteger;
6467
import java.util.concurrent.locks.LockSupport;
6568
import java.util.function.Consumer;
@@ -190,14 +193,49 @@ public void onInitialize() {
190193
ServerLoginConnectionEvents.QUERY_START.addPhaseOrdering(Identifier.parse("fireblanket:pre"), Event.DEFAULT_PHASE);
191194
ServerLoginConnectionEvents.QUERY_START.register(Identifier.parse("fireblanket:pre"), (handler, server, sender, synchronizer) -> {
192195
if (!server.isSingleplayer()) {
193-
sender.sendPacket(FULL_STREAM_COMPRESSION, FriendlyByteBufs.empty());
196+
final var buf = FriendlyByteBufs.create();
197+
buf.writeCollection(NetworkState.VALID, NetworkState.CODEC);
198+
sender.sendPacket(FULL_STREAM_COMPRESSION, buf);
194199
}
195200
});
196201
}
197202

198203
ServerLoginNetworking.registerGlobalReceiver(FULL_STREAM_COMPRESSION, (server, handler, understood, buf, synchronizer, responseSender) -> {
199-
if (understood) {
200-
((FSCConnection) ((ServerLoginNetworkHandlerAccessor) handler).fireblanket$getConnection()).fireblanket$enableFullStreamCompression();
204+
if (!understood) {
205+
return;
206+
}
207+
208+
if (!((((ServerLoginNetworkHandlerAccessor) handler).fireblanket$getConnection()) instanceof FSCConnection connection)) {
209+
responseSender.disconnect(Component.translatableWithFallback("fireblanket.fsc.broken", "FSC Broken"));
210+
return;
211+
}
212+
213+
if (!buf.isReadable()) {
214+
// Old Fireblanket logic; only relevant to backports,
215+
// or cases of "why are you using ViaVersion with Fireblanket's networking?".
216+
connection.fireblanket$enableFullStreamCompression(NetworkState.PLAY);
217+
return;
218+
}
219+
220+
final int len = buf.readByte() & 255;
221+
222+
if (len > 127 || !buf.isReadable(len)) {
223+
responseSender.disconnect(Component.translatableWithFallback("fireblanket.fsc.invalid.size", "FSC Invalid: %s > %s || %1$s > 127", len, buf.readableBytes()));
224+
}
225+
226+
final String rawState = buf.readString(len, StandardCharsets.UTF_8);
227+
final NetworkState state = NetworkState.parse(rawState);
228+
229+
if (!NetworkState.VALID.contains(state)) {
230+
// If the client picked a state that is not valid, we cannot continue as we'll crash the client.
231+
responseSender.disconnect(Component.translatableWithFallback("fireblanket.fsc.invalid.name", "FSC Invalid: %s", state));
232+
return;
233+
}
234+
235+
connection.fireblanket$enableFullStreamCompression(state);
236+
237+
if (state == NetworkState.LOGIN) {
238+
connection.fireblanket$startFullStreamCompression(NetworkState.LOGIN, 0);
201239
}
202240
});
203241

src/main/java/net/modfest/fireblanket/FireblanketClient.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,14 @@
2929
import net.modfest.fireblanket.net.BatchedBEUpdatePayload;
3030
import net.modfest.fireblanket.net.BatchedEntityVelocityUpdatePacket;
3131
import net.modfest.fireblanket.net.CommandBlockPacket;
32+
import net.modfest.fireblanket.net.NetworkState;
3233
import net.modfest.fireblanket.net.VelocityUpdate;
3334
import net.modfest.fireblanket.world.render_regions.RegionSyncRequest;
3435
import net.modfest.fireblanket.world.render_regions.RenderRegions;
3536

37+
import java.nio.charset.StandardCharsets;
38+
import java.util.HashSet;
39+
import java.util.Set;
3640
import java.util.concurrent.CompletableFuture;
3741

3842
public class FireblanketClient implements ClientModInitializer {
@@ -61,12 +65,48 @@ public void onInitializeClient() {
6165
});
6266

6367
ClientLoginNetworking.registerGlobalReceiver(Fireblanket.FULL_STREAM_COMPRESSION, (client, handler, buf, listenerAdder) -> {
64-
if (Fireblanket.CAN_USE_ZSTD) {
65-
((FSCConnection) ((ClientLoginNetworkHandlerAccessor) handler).fireblanket$getConnection()).fireblanket$enableFullStreamCompression();
68+
if (!Fireblanket.CAN_USE_ZSTD) {
69+
return CompletableFuture.completedFuture(null);
70+
}
71+
if (!(((ClientLoginNetworkHandlerAccessor) handler).fireblanket$getConnection() instanceof FSCConnection connection)) {
72+
Fireblanket.LOGGER.error("FSC mixins are broken; ZSTD is allowed but FSCConnection is missing?");
73+
return CompletableFuture.completedFuture(null);
74+
}
75+
76+
if (!buf.isReadable()) {
77+
// We're talking to an older Fireblanket server. Enable at play.
78+
connection.fireblanket$enableFullStreamCompression(NetworkState.PLAY);
79+
// This also means send an empty buffer back.
6680
return CompletableFuture.completedFuture(FriendlyByteBufs.empty());
67-
} else {
81+
}
82+
83+
final Set<NetworkState> states = buf.readCollection(HashSet::new, NetworkState.CODEC);
84+
85+
// Retain all valid states.
86+
states.retainAll(NetworkState.VALID);
87+
88+
if (states.isEmpty()) {
89+
// Signal that we did not understand and proceed.
6890
return CompletableFuture.completedFuture(null);
6991
}
92+
93+
NetworkState state = NetworkState.UNKNOWN;
94+
for (final NetworkState next : states) {
95+
if (state.ordinal() > next.ordinal()) {
96+
state = next;
97+
}
98+
}
99+
100+
connection.fireblanket$enableFullStreamCompression(state);
101+
102+
if (state == NetworkState.LOGIN) {
103+
listenerAdder.accept(_ -> connection.fireblanket$startFullStreamCompression(NetworkState.LOGIN, 0));
104+
}
105+
106+
final var ret = FriendlyByteBufs.create();
107+
ret.writeByte(state.getNetworkName().length());
108+
ret.writeCharSequence(state.getNetworkName(), StandardCharsets.UTF_8);
109+
return CompletableFuture.completedFuture(ret);
70110
});
71111

72112
ClientPlayNetworking.registerGlobalReceiver(BatchedBEUpdatePayload.ID, (payload, context) -> {

src/main/java/net/modfest/fireblanket/mixin/fsc/MixinClientConfigurationNetworkHandler.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import net.minecraft.client.multiplayer.CommonListenerCookie;
77
import net.minecraft.network.Connection;
88
import net.modfest.fireblanket.mixinsupport.FSCConnection;
9+
import net.modfest.fireblanket.net.NetworkState;
910
import org.spongepowered.asm.mixin.Mixin;
1011
import org.spongepowered.asm.mixin.injection.At;
1112
import org.spongepowered.asm.mixin.injection.Inject;
@@ -28,6 +29,6 @@ protected MixinClientConfigurationNetworkHandler(final Minecraft client, final C
2829
**/
2930
@Inject(method = "handleConfigurationFinished", at = @At("RETURN"))
3031
private void fireblanket$startFSC(CallbackInfo ci) {
31-
((FSCConnection) this.connection).fireblanket$startFullStreamCompression(0L);
32+
((FSCConnection) this.connection).fireblanket$startFullStreamCompression(NetworkState.PLAY, 0L);
3233
}
3334
}

src/main/java/net/modfest/fireblanket/mixin/fsc/MixinClientConnection.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
import net.modfest.fireblanket.Fireblanket;
1616
import net.modfest.fireblanket.Fireblanket.QueuedPacket;
1717
import net.modfest.fireblanket.mixinsupport.FSCConnection;
18+
import net.modfest.fireblanket.net.NetworkState;
1819
import net.modfest.fireblanket.net.ZstdDecoder;
1920
import net.modfest.fireblanket.net.ZstdEncoder;
2021
import net.modfest.fireblanket.util.LinkedBlocQueue;
2122
import net.modfest.fireblanket.util.ReassignableOutputStream;
2223
import org.jetbrains.annotations.Nullable;
2324
import org.spongepowered.asm.mixin.Mixin;
2425
import org.spongepowered.asm.mixin.Shadow;
26+
import org.spongepowered.asm.mixin.Unique;
2527
import org.spongepowered.asm.mixin.injection.At;
2628
import org.spongepowered.asm.mixin.injection.Inject;
2729
import org.spongepowered.asm.mixin.injection.Redirect;
@@ -48,8 +50,11 @@ private void sendPacket(Packet<?> packet, ChannelFutureListener callbacks, boole
4850
@Shadow
4951
public abstract void flushChannel();
5052

53+
@Unique
5154
private final LinkedBlocQueue<QueuedPacket> fireblanket$queue = Fireblanket.getNextQueue();
52-
private boolean fireblanket$fsc = false;
55+
@Unique
56+
private NetworkState fireblanket$fsc = NetworkState.NONE;
57+
@Unique
5358
private boolean fireblanket$fscStarted = false;
5459

5560
/**
@@ -85,7 +90,7 @@ private void sendPacket(Packet<?> packet, ChannelFutureListener callbacks, boole
8590
// idk man
8691
if (packet instanceof ClientboundDisconnectPacket || packet instanceof ClientboundLoginDisconnectPacket) {
8792
fireblanket$fscStarted = false;
88-
fireblanket$fsc = false;
93+
fireblanket$fsc = NetworkState.NONE;
8994
}
9095
}
9196

@@ -100,13 +105,15 @@ private void sendPacket(Packet<?> packet, ChannelFutureListener callbacks, boole
100105
* {@inheritDoc}
101106
*/
102107
@Override
103-
public void fireblanket$startFullStreamCompression(final long millis) {
104-
if (!this.fireblanket$fsc) {
108+
public void fireblanket$startFullStreamCompression(final NetworkState state, final long millis) {
109+
if (state.ordinal() < this.fireblanket$fsc.ordinal()) {
105110
return;
106111
}
107112
if (!this.fireblanket$fscStarted) {
113+
Fireblanket.LOGGER.debug("Starting FSC now: {} @ {}ms", state, millis);
108114
this.fireblanket$enableFSCNow(millis);
109115
} else {
116+
Fireblanket.LOGGER.debug("Modifying FSC flush time: {} @ {}ms", state, millis);
110117
this.fireblanket$modifyFSC(millis);
111118
}
112119
}
@@ -151,7 +158,8 @@ private void sendPacket(Packet<?> packet, ChannelFutureListener callbacks, boole
151158
}
152159

153160
@Override
154-
public void fireblanket$enableFullStreamCompression() {
155-
fireblanket$fsc = true;
161+
public void fireblanket$enableFullStreamCompression(final NetworkState state) {
162+
Fireblanket.LOGGER.debug("Enabling FSC: {}", state);
163+
this.fireblanket$fsc = state;
156164
}
157165
}

src/main/java/net/modfest/fireblanket/mixin/fsc/MixinClientLoginPacketListenerImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl;
44
import net.minecraft.network.Connection;
55
import net.modfest.fireblanket.mixinsupport.FSCConnection;
6+
import net.modfest.fireblanket.net.NetworkState;
67
import org.spongepowered.asm.mixin.Final;
78
import org.spongepowered.asm.mixin.Mixin;
89
import org.spongepowered.asm.mixin.Shadow;
@@ -33,6 +34,6 @@ public class MixinClientLoginPacketListenerImpl {
3334
)
3435
)
3536
private void fireblanket$startFSC(CallbackInfo ci) {
36-
((FSCConnection) this.connection).fireblanket$startFullStreamCompression(0L);
37+
((FSCConnection) this.connection).fireblanket$startFullStreamCompression(NetworkState.CONFIGURATION, 0L);
3738
}
3839
}

src/main/java/net/modfest/fireblanket/mixin/fsc/MixinServerConfigurationNetworkHandler.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import net.minecraft.server.network.ServerCommonPacketListenerImpl;
77
import net.minecraft.server.network.ServerConfigurationPacketListenerImpl;
88
import net.modfest.fireblanket.mixinsupport.FSCConnection;
9+
import net.modfest.fireblanket.net.NetworkState;
910
import org.spongepowered.asm.mixin.Mixin;
1011
import org.spongepowered.asm.mixin.injection.At;
1112
import org.spongepowered.asm.mixin.injection.Inject;
@@ -28,6 +29,6 @@ public MixinServerConfigurationNetworkHandler(final MinecraftServer server, fina
2829
**/
2930
@Inject(method = "handleConfigurationFinished", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;setupOutboundProtocol(Lnet/minecraft/network/ProtocolInfo;)V"))
3031
private void fireblanket$startFSC(CallbackInfo ci) {
31-
((FSCConnection) this.connection).fireblanket$startFullStreamCompression(40L);
32+
((FSCConnection) this.connection).fireblanket$startFullStreamCompression(NetworkState.PLAY, 40L);
3233
}
3334
}

src/main/java/net/modfest/fireblanket/mixin/fsc/MixinServerLoginPacketListenerImpl.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import net.minecraft.network.Connection;
44
import net.minecraft.server.network.ServerLoginPacketListenerImpl;
55
import net.modfest.fireblanket.mixinsupport.FSCConnection;
6+
import net.modfest.fireblanket.net.NetworkState;
67
import org.spongepowered.asm.mixin.Final;
78
import org.spongepowered.asm.mixin.Mixin;
89
import org.spongepowered.asm.mixin.Shadow;
@@ -27,6 +28,6 @@ public class MixinServerLoginPacketListenerImpl {
2728
**/
2829
@Inject(method = "handleLoginAcknowledgement", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;setupOutboundProtocol(Lnet/minecraft/network/ProtocolInfo;)V"))
2930
private void fireblanket$startFSC(CallbackInfo ci) {
30-
((FSCConnection) this.connection).fireblanket$startFullStreamCompression(0L);
31+
((FSCConnection) this.connection).fireblanket$startFullStreamCompression(NetworkState.CONFIGURATION, 0L);
3132
}
3233
}

src/main/java/net/modfest/fireblanket/mixinsupport/FSCConnection.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import net.modfest.fireblanket.mixin.fsc.MixinClientLoginPacketListenerImpl;
99
import net.modfest.fireblanket.mixin.fsc.MixinServerConfigurationNetworkHandler;
1010
import net.modfest.fireblanket.mixin.fsc.MixinServerLoginPacketListenerImpl;
11+
import net.modfest.fireblanket.net.NetworkState;
1112

1213
public interface FSCConnection {
1314

@@ -31,8 +32,13 @@ public interface FSCConnection {
3132
* @see MixinClientLoginPacketListenerImpl
3233
* @see MixinServerLoginPacketListenerImpl
3334
*/
34-
void fireblanket$startFullStreamCompression(final long millis);
35+
void fireblanket$startFullStreamCompression(final NetworkState state, final long millis);
3536

36-
void fireblanket$enableFullStreamCompression();
37+
/**
38+
* Sets the starting point for full-stream compression.
39+
*
40+
* @implSpec The client and server <em>must</em> agree, else you will have decoding errors.
41+
*/
42+
void fireblanket$enableFullStreamCompression(final NetworkState state);
3743

3844
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package net.modfest.fireblanket.net;
2+
3+
import io.netty.buffer.ByteBuf;
4+
import net.minecraft.network.codec.ByteBufCodecs;
5+
import net.minecraft.network.codec.StreamCodec;
6+
7+
import java.util.Locale;
8+
import java.util.Set;
9+
10+
/**
11+
* Allows negotiating the network state required to enable FSC.
12+
*
13+
* @author Ampflower
14+
* @implNote The order of the enum is load bearing, and expected to be sequential.
15+
* If this ever requires changes, please be sure to order all new enums to be <em>before</em> {@link #NONE},
16+
* and that the enums are ordered to how they'll be encountered in the network protocol.
17+
* <p>
18+
* If the entrypoint of a given enum has been moved in such a way that starting FSC is now
19+
* incompatible with the previous version, give it a new network name to prevent the old client from using it.
20+
**/
21+
public enum NetworkState {
22+
// This would ideally be via handshake, using a special subdomain that is sent by the client,
23+
// if it sees a magic in the status. This would only be possible if the server has been seen before joining.
24+
// This can break proxies, and may be disableable for proxies.
25+
// However, if it is seen at login stage instead, this can be effectively be considered as enable immediately.
26+
// Since login runs in lockstep, we can safely swap the pipeline pieces to enable FSC.
27+
LOGIN("login"),
28+
CONFIGURATION("configuration"),
29+
PLAY("play"),
30+
// Only be possible on a vanilla client
31+
// A Fireblanket client sending this should be disconnected?
32+
// The client must refuse to acknowledge the packet if it receives this state explicitly.
33+
NONE("none"),
34+
// acts as PLAY if the server can't understand it.
35+
// The client must refuse to acknowledge the packet if it receives this state explicitly.
36+
UNKNOWN("unknown"),
37+
;
38+
39+
public static final StreamCodec<ByteBuf, NetworkState> CODEC = ByteBufCodecs.STRING_UTF8
40+
.map(NetworkState::parse, NetworkState::getNetworkName);
41+
42+
// TODO: configuration-based negotiation
43+
public static final Set<NetworkState> VALID = Set.of(LOGIN, CONFIGURATION, PLAY);
44+
45+
private final String name;
46+
47+
NetworkState(final String name) {
48+
this.name = name;
49+
}
50+
51+
public static NetworkState parse(final String string) {
52+
return switch (string.toLowerCase(Locale.ROOT)) {
53+
case "none" -> NONE;
54+
case "login" -> LOGIN;
55+
case "configuration" -> CONFIGURATION;
56+
case "play" -> PLAY;
57+
default -> UNKNOWN;
58+
};
59+
}
60+
61+
public final String getNetworkName() {
62+
return name;
63+
}
64+
}

src/main/resources/assets/fireblanket/lang/en_us.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"gamerule.fireblanket:lightningBroadcastRadius.description": "How far in blocks players will receive lightning strikes from in-game. Set to 0 to effectively disable broadcasting.",
88
"commands.gamerule.locked": "You do not have permission to modify this gamerule",
99
"commandsBlock.commandSetByPlayer": "%s set the command at (%s) to: %s",
10+
"fireblanket.fsc.broken": "[Fireblanket] Despite relevant mixins being disabled or broken, we're still requesting FSC?",
11+
"fireblanket.fsc.invalid.size": "[Fireblanket] Illegal or invalid string size: %s; exceeded 127 or readable bytes of: %s",
12+
"fireblanket.fsc.invalid.name": "[Fireblanket] Your client gave an invalid FSC entrypoint: %s",
1013
"fireblanket.commands.command.grep.entry": "[%s] [Creator: %s] [Updater: %s] [%s]\n> %s",
1114
"fireblanket.commands.command.grep.entry.same": "[%s] [%s] [%s]\n> %s",
1215
"fireblanket.commands.command.grep.result": "Found %s matches.",

0 commit comments

Comments
 (0)