Skip to content

Commit b3119ff

Browse files
committed
feat: Support Minecraft Java 1.21.11
Implement boss bar management for server transitions in versions 1.20.2 and above - Introduced the ManagedViewer interface to handle boss bar registration and unregistration during server transitions. - Added a bossBarManager to manage boss bars for connected players, ensuring proper handling of boss bar packets. - Updated existing boss bar handling logic to respect the new management system, including packet writing and state management. - Enhanced player session handling to accommodate the new boss bar management features. This change improves the stability and user experience during server transitions by preventing disconnections related to boss bar updates.
1 parent 5a2ae81 commit b3119ff

13 files changed

Lines changed: 357 additions & 100 deletions

File tree

pkg/edition/java/bossbar/bossbar.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,20 @@ type Viewer interface {
8585
proto.PacketWriter
8686
}
8787

88+
// ManagedViewer is an optional interface for viewers that support boss bar management
89+
// during server transitions (1.20.2+). If a viewer implements this interface, the boss bar
90+
// will register/unregister with the viewer's manager and use WriteBossBarPacket for packets.
91+
type ManagedViewer interface {
92+
Viewer
93+
// RegisterBossBar registers a boss bar with this viewer's manager.
94+
RegisterBossBar(bar BossBar)
95+
// UnregisterBossBar unregisters a boss bar from this viewer's manager.
96+
UnregisterBossBar(bar BossBar)
97+
// WriteBossBarPacket writes a boss bar packet, respecting the dropping state.
98+
// Returns true if the packet was written (or dropped intentionally).
99+
WriteBossBarPacket(p *packet.BossBar) bool
100+
}
101+
88102
// Color is the color of the percent bar.
89103
type Color = packet.Color
90104

pkg/edition/java/bossbar/internal.go

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ func (b *bossBar) RemoveViewer(viewer Viewer) error {
4444
p := b.createRemovePacket()
4545
b.mu.Unlock()
4646

47-
return viewer.WritePacket(p)
47+
// Unregister from the viewer's manager if supported
48+
if m, ok := viewer.(ManagedViewer); ok {
49+
m.UnregisterBossBar(b)
50+
}
51+
52+
return writePacketToViewer(viewer, p)
4853
}
4954

5055
func (b *bossBar) AddViewer(viewer Viewer) error {
@@ -65,12 +70,21 @@ func (b *bossBar) AddViewer(viewer Viewer) error {
6570
p := b.createAddPacket()
6671
b.mu.Unlock()
6772

68-
err := viewer.WritePacket(p)
73+
// Register with the viewer's manager if supported
74+
if m, ok := viewer.(ManagedViewer); ok {
75+
m.RegisterBossBar(b)
76+
}
77+
78+
err := writePacketToViewer(viewer, p)
6979
if err != nil {
7080
b.mu.Lock()
7181
delete(b.viewers, viewer.ID())
7282
b.mu.Unlock()
7383
v.removed()
84+
// Unregister on failure
85+
if m, ok := viewer.(ManagedViewer); ok {
86+
m.UnregisterBossBar(b)
87+
}
7488
return err
7589
}
7690

@@ -105,10 +119,24 @@ var _ BossBar = (*bossBar)(nil)
105119

106120
func (b *bossBar) writeToViewers(p proto.Packet) {
107121
for _, viewer := range b.viewers {
108-
go func(v Viewer) { _ = v.WritePacket(p) }(viewer)
122+
go func(v Viewer) { _ = writePacketToViewer(v, p) }(viewer)
109123
}
110124
}
111125

126+
// writePacketToViewer writes a packet to a viewer, using the managed writer if available.
127+
// For 1.20.2+ viewers with a manager, this respects the dropping state during server transitions.
128+
func writePacketToViewer(viewer Viewer, p proto.Packet) error {
129+
if m, ok := viewer.(ManagedViewer); ok {
130+
if bb, ok := p.(*bossbar.BossBar); ok {
131+
if !m.WriteBossBarPacket(bb) {
132+
return nil // dropped intentionally
133+
}
134+
return nil
135+
}
136+
}
137+
return viewer.WritePacket(p)
138+
}
139+
112140
func (b *bossBar) Name() component.Component {
113141
b.mu.RLock()
114142
defer b.mu.RUnlock()

pkg/edition/java/ping/pong.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,15 @@ func (p *ServerPing) UnmarshalJSON(data []byte) error {
6161
return fmt.Errorf("error decoding json: %w", err)
6262
}
6363

64-
var err error
65-
out.Alias.Description, err = componentutil.ParseTextComponent(out.Version.Protocol, string(out.Description))
66-
if err != nil {
67-
return fmt.Errorf("error decoding description: %w", err)
64+
// Handle null or missing description (e.g., from backend server ping passthrough)
65+
if len(out.Description) == 0 || string(out.Description) == "null" {
66+
out.Alias.Description = &component.Text{} // empty component
67+
} else {
68+
var err error
69+
out.Alias.Description, err = componentutil.ParseTextComponent(out.Version.Protocol, string(out.Description))
70+
if err != nil {
71+
return fmt.Errorf("error decoding description: %w", err)
72+
}
6873
}
6974

7075
*p = ServerPing(out.Alias)

pkg/edition/java/proto/packet/chat/session_command.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"go.minekube.com/gate/pkg/edition/java/proto/util"
8+
"go.minekube.com/gate/pkg/edition/java/proto/version"
89
"go.minekube.com/gate/pkg/gate/proto"
910
"go.minekube.com/gate/pkg/util/errs"
1011
)
@@ -46,8 +47,13 @@ func (s *SessionPlayerCommand) Encode(c *proto.PacketContext, wr io.Writer) erro
4647
}
4748

4849
func (s *SessionPlayerCommand) Decode(c *proto.PacketContext, rd io.Reader) error {
50+
// Command length cap increased from 256 to 65536 in 1.20.5+
51+
cap := 256
52+
if c.Protocol.GreaterEqual(version.Minecraft_1_20_5) {
53+
cap = util.DefaultMaxStringSize // 65536
54+
}
4955
var err error
50-
s.Command, err = util.ReadStringMax(rd, 256)
56+
s.Command, err = util.ReadStringMax(rd, cap)
5157
if err != nil {
5258
return err
5359
}

pkg/edition/java/proto/packet/chat/unsigned_command.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package chat
22

33
import (
4+
"io"
5+
46
"go.minekube.com/gate/pkg/edition/java/proto/util"
57
"go.minekube.com/gate/pkg/gate/proto"
6-
"io"
78
)
89

910
type UnsignedPlayerCommand struct {
@@ -21,7 +22,8 @@ func (u *UnsignedPlayerCommand) Encode(c *proto.PacketContext, wr io.Writer) err
2122
}
2223

2324
func (u *UnsignedPlayerCommand) Decode(c *proto.PacketContext, rd io.Reader) (err error) {
24-
u.Command, err = util.ReadStringMax(rd, MaxServerBoundMessageLength)
25+
// UnsignedPlayerCommand always uses 65536 cap since it's only available in 1.20.5+
26+
u.Command, err = util.ReadStringMax(rd, util.DefaultMaxStringSize)
2527
return err
2628
}
2729

pkg/edition/java/proto/version/version.go

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,48 +9,49 @@ import (
99
)
1010

1111
var (
12-
Unknown = v(-1, "Unknown")
13-
Legacy = v(-2, "Legacy")
14-
Minecraft_1_7_2 = v(4, "1.7.2", "1.7.3", "1.7.4", "1.7.5")
15-
Minecraft_1_7_6 = v(5, "1.7.6", "1.7.7", "1.7.8", "1.7.9", "1.7.10")
16-
Minecraft_1_8 = v(47, "1.8", "1.8.1", "1.8.2", "1.8.3", "1.8.4", "1.8.5", "1.8.6", "1.8.7", "1.8.8", "1.8.9")
17-
Minecraft_1_9 = v(107, "1.9")
18-
Minecraft_1_9_1 = v(108, "1.9.1")
19-
Minecraft_1_9_4 = v(110, "1.9.3", "1.9.4")
20-
Minecraft_1_10 = v(210, "1.10", "1.10.1", "1.10.2")
21-
Minecraft_1_11 = v(315, "1.11")
22-
Minecraft_1_11_1 = v(316, "1.11.1", "1.11.2")
23-
Minecraft_1_12 = v(335, "1.12")
24-
Minecraft_1_12_1 = v(338, "1.12.1")
25-
Minecraft_1_12_2 = v(340, "1.12.2")
26-
Minecraft_1_13 = v(393, "1.13")
27-
Minecraft_1_13_2 = v(404, "1.13.2")
28-
Minecraft_1_14 = v(477, "1.14")
29-
Minecraft_1_15 = v(573, "1.15")
30-
Minecraft_1_16 = v(735, "1.16")
31-
Minecraft_1_16_1 = v(736, "1.16.1")
32-
Minecraft_1_16_2 = v(751, "1.16.2")
33-
Minecraft_1_16_3 = v(753, "1.16.3")
34-
Minecraft_1_16_4 = v(754, "1.16.4", "1.16.5")
35-
Minecraft_1_17 = v(755, "1.17")
36-
Minecraft_1_17_1 = v(756, "1.17.1")
37-
Minecraft_1_18 = v(757, "1.18", "1.18.1")
38-
Minecraft_1_18_2 = v(758, "1.18.2")
39-
Minecraft_1_19 = v(759, "1.19")
40-
Minecraft_1_19_1 = v(760, "1.19.1", "1.19.2")
41-
Minecraft_1_19_3 = v(761, "1.19.3")
42-
Minecraft_1_19_4 = v(762, "1.19.4")
43-
Minecraft_1_20 = v(763, "1.20", "1.20.1")
44-
Minecraft_1_20_2 = v(764, "1.20.2")
45-
Minecraft_1_20_3 = v(765, "1.20.3", "1.20.4")
46-
Minecraft_1_20_5 = v(766, "1.20.5", "1.20.6")
47-
Minecraft_1_21 = v(767, "1.21", "1.21.1")
48-
Minecraft_1_21_2 = v(768, "1.21.2", "1.21.3")
49-
Minecraft_1_21_4 = v(769, "1.21.4")
50-
Minecraft_1_21_5 = v(770, "1.21.5")
51-
Minecraft_1_21_6 = v(771, "1.21.6")
52-
Minecraft_1_21_7 = v(772, "1.21.7", "1.21.8")
53-
Minecraft_1_21_9 = v(773, "1.21.9", "1.21.10")
12+
Unknown = v(-1, "Unknown")
13+
Legacy = v(-2, "Legacy")
14+
Minecraft_1_7_2 = v(4, "1.7.2", "1.7.3", "1.7.4", "1.7.5")
15+
Minecraft_1_7_6 = v(5, "1.7.6", "1.7.7", "1.7.8", "1.7.9", "1.7.10")
16+
Minecraft_1_8 = v(47, "1.8", "1.8.1", "1.8.2", "1.8.3", "1.8.4", "1.8.5", "1.8.6", "1.8.7", "1.8.8", "1.8.9")
17+
Minecraft_1_9 = v(107, "1.9")
18+
Minecraft_1_9_1 = v(108, "1.9.1")
19+
Minecraft_1_9_4 = v(110, "1.9.3", "1.9.4")
20+
Minecraft_1_10 = v(210, "1.10", "1.10.1", "1.10.2")
21+
Minecraft_1_11 = v(315, "1.11")
22+
Minecraft_1_11_1 = v(316, "1.11.1", "1.11.2")
23+
Minecraft_1_12 = v(335, "1.12")
24+
Minecraft_1_12_1 = v(338, "1.12.1")
25+
Minecraft_1_12_2 = v(340, "1.12.2")
26+
Minecraft_1_13 = v(393, "1.13")
27+
Minecraft_1_13_2 = v(404, "1.13.2")
28+
Minecraft_1_14 = v(477, "1.14")
29+
Minecraft_1_15 = v(573, "1.15")
30+
Minecraft_1_16 = v(735, "1.16")
31+
Minecraft_1_16_1 = v(736, "1.16.1")
32+
Minecraft_1_16_2 = v(751, "1.16.2")
33+
Minecraft_1_16_3 = v(753, "1.16.3")
34+
Minecraft_1_16_4 = v(754, "1.16.4", "1.16.5")
35+
Minecraft_1_17 = v(755, "1.17")
36+
Minecraft_1_17_1 = v(756, "1.17.1")
37+
Minecraft_1_18 = v(757, "1.18", "1.18.1")
38+
Minecraft_1_18_2 = v(758, "1.18.2")
39+
Minecraft_1_19 = v(759, "1.19")
40+
Minecraft_1_19_1 = v(760, "1.19.1", "1.19.2")
41+
Minecraft_1_19_3 = v(761, "1.19.3")
42+
Minecraft_1_19_4 = v(762, "1.19.4")
43+
Minecraft_1_20 = v(763, "1.20", "1.20.1")
44+
Minecraft_1_20_2 = v(764, "1.20.2")
45+
Minecraft_1_20_3 = v(765, "1.20.3", "1.20.4")
46+
Minecraft_1_20_5 = v(766, "1.20.5", "1.20.6")
47+
Minecraft_1_21 = v(767, "1.21", "1.21.1")
48+
Minecraft_1_21_2 = v(768, "1.21.2", "1.21.3")
49+
Minecraft_1_21_4 = v(769, "1.21.4")
50+
Minecraft_1_21_5 = v(770, "1.21.5")
51+
Minecraft_1_21_6 = v(771, "1.21.6")
52+
Minecraft_1_21_7 = v(772, "1.21.7", "1.21.8")
53+
Minecraft_1_21_9 = v(773, "1.21.9", "1.21.10")
54+
Minecraft_1_21_11 = v(774, "1.21.11")
5455

5556
// Versions ordered from lowest to highest
5657
Versions = []*proto.Version{
@@ -70,7 +71,7 @@ var (
7071
Minecraft_1_18, Minecraft_1_18_2,
7172
Minecraft_1_19, Minecraft_1_19_1, Minecraft_1_19_3, Minecraft_1_19_4,
7273
Minecraft_1_20, Minecraft_1_20_2, Minecraft_1_20_3, Minecraft_1_20_5,
73-
Minecraft_1_21, Minecraft_1_21_2, Minecraft_1_21_4, Minecraft_1_21_5, Minecraft_1_21_6, Minecraft_1_21_7, Minecraft_1_21_9,
74+
Minecraft_1_21, Minecraft_1_21_2, Minecraft_1_21_4, Minecraft_1_21_5, Minecraft_1_21_6, Minecraft_1_21_7, Minecraft_1_21_9, Minecraft_1_21_11,
7475
}
7576
)
7677

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package proxy
2+
3+
import (
4+
"sync"
5+
6+
"go.minekube.com/gate/pkg/edition/java/bossbar"
7+
bossbarpacket "go.minekube.com/gate/pkg/edition/java/proto/packet/bossbar"
8+
"go.minekube.com/gate/pkg/edition/java/proto/version"
9+
"go.minekube.com/gate/pkg/util/uuid"
10+
)
11+
12+
// bossBarManager handles dropping and resending boss bar packets on versions 1.20.2 and newer
13+
// because the client deletes all boss bars during the config phase, and sending update packets
14+
// would cause the client to be disconnected.
15+
type bossBarManager struct {
16+
player *connectedPlayer
17+
18+
mu sync.Mutex
19+
bossBars map[uuid.UUID]bossbar.BossBar // keyed by boss bar ID
20+
dropPackets bool
21+
}
22+
23+
func newBossBarManager(player *connectedPlayer) *bossBarManager {
24+
return &bossBarManager{
25+
player: player,
26+
bossBars: make(map[uuid.UUID]bossbar.BossBar),
27+
}
28+
}
29+
30+
// shouldManage returns true if this player's protocol version requires boss bar management.
31+
func (m *bossBarManager) shouldManage() bool {
32+
return m.player.Protocol().GreaterEqual(version.Minecraft_1_20_2)
33+
}
34+
35+
// RegisterBossBar registers a proxy-level boss bar with this player.
36+
// This should be called when a boss bar is shown to the player.
37+
func (m *bossBarManager) RegisterBossBar(bar bossbar.BossBar) {
38+
if !m.shouldManage() {
39+
return
40+
}
41+
m.mu.Lock()
42+
m.bossBars[bar.ID()] = bar
43+
m.mu.Unlock()
44+
}
45+
46+
// UnregisterBossBar unregisters a proxy-level boss bar from this player.
47+
// This should be called when a boss bar is hidden from the player.
48+
func (m *bossBarManager) UnregisterBossBar(bar bossbar.BossBar) {
49+
if !m.shouldManage() {
50+
return
51+
}
52+
m.mu.Lock()
53+
delete(m.bossBars, bar.ID())
54+
m.mu.Unlock()
55+
}
56+
57+
// WritePacket writes a boss bar packet to the player, respecting the drop state.
58+
// Returns true if the packet was written (or dropped intentionally), false on error.
59+
func (m *bossBarManager) WritePacket(p *bossbarpacket.BossBar) bool {
60+
m.mu.Lock()
61+
drop := m.dropPackets
62+
m.mu.Unlock()
63+
64+
if drop {
65+
// Intentionally drop the packet during server transition
66+
return true
67+
}
68+
69+
return m.player.WritePacket(p) == nil
70+
}
71+
72+
// StartDropping prevents boss bar update packets from being sent to the player.
73+
// This should be called when the player enters config state for server switching.
74+
func (m *bossBarManager) StartDropping() {
75+
if !m.shouldManage() {
76+
return
77+
}
78+
m.mu.Lock()
79+
m.dropPackets = true
80+
m.mu.Unlock()
81+
}
82+
83+
// SendBossBars re-creates all proxy-level boss bars for the player and stops dropping packets.
84+
// This should be called after the player has joined a new server.
85+
func (m *bossBarManager) SendBossBars() {
86+
if !m.shouldManage() {
87+
return
88+
}
89+
90+
m.mu.Lock()
91+
bars := make([]bossbar.BossBar, 0, len(m.bossBars))
92+
for _, bar := range m.bossBars {
93+
bars = append(bars, bar)
94+
}
95+
m.dropPackets = false
96+
m.mu.Unlock()
97+
98+
// Re-add the player as a viewer to each boss bar, which will send the ADD packet
99+
for _, bar := range bars {
100+
// The boss bar package handles creating the add packet
101+
_ = bar.AddViewer(m.player)
102+
}
103+
}
104+
105+
// IsDropping returns true if boss bar packets are currently being dropped.
106+
func (m *bossBarManager) IsDropping() bool {
107+
m.mu.Lock()
108+
defer m.mu.Unlock()
109+
return m.dropPackets
110+
}
111+
112+
// Implement bossbar.ManagedViewer interface on connectedPlayer
113+
114+
// RegisterBossBar implements bossbar.ManagedViewer.
115+
func (p *connectedPlayer) RegisterBossBar(bar bossbar.BossBar) {
116+
p.bossBarManager.RegisterBossBar(bar)
117+
}
118+
119+
// UnregisterBossBar implements bossbar.ManagedViewer.
120+
func (p *connectedPlayer) UnregisterBossBar(bar bossbar.BossBar) {
121+
p.bossBarManager.UnregisterBossBar(bar)
122+
}
123+
124+
// WriteBossBarPacket implements bossbar.ManagedViewer.
125+
func (p *connectedPlayer) WriteBossBarPacket(packet *bossbarpacket.BossBar) bool {
126+
return p.bossBarManager.WritePacket(packet)
127+
}
128+
129+
// Compile-time assertion that connectedPlayer implements bossbar.ManagedViewer
130+
var _ bossbar.ManagedViewer = (*connectedPlayer)(nil)

0 commit comments

Comments
 (0)