Skip to content

Commit c108f49

Browse files
Merge branch 'WiIIiam278:master' into master
2 parents 2d53c9b + 482dfa6 commit c108f49

8 files changed

Lines changed: 428 additions & 7 deletions

File tree

src/main/java/net/william278/velocitab/Velocitab.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import net.william278.desertwell.util.UpdateChecker;
3535
import net.william278.desertwell.util.Version;
3636
import net.william278.toilet.Toilet;
37+
import net.william278.velocitab.api.EventDispatcher;
3738
import net.william278.velocitab.api.PluginMessageAPI;
3839
import net.william278.velocitab.api.VelocitabAPI;
3940
import net.william278.velocitab.commands.VelocitabCommand;
@@ -87,6 +88,7 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
8788
private PacketEventManager packetEventManager;
8889
private PluginMessageAPI pluginMessageAPI;
8990
private PlaceholderManager placeholderManager;
91+
private EventDispatcher eventDispatcher;
9092
@Setter
9193
private Toilet toilet;
9294

@@ -158,6 +160,7 @@ public ScoreboardManager getScoreboardManager() {
158160

159161
private void prepareAPI() {
160162
VelocitabAPI.register(this);
163+
eventDispatcher = new EventDispatcher(this);
161164
if (settings.isEnablePluginMessageApi()) {
162165
pluginMessageAPI = new PluginMessageAPI(this);
163166
pluginMessageAPI.registerChannel();
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* This file is part of Velocitab, licensed under the Apache License 2.0.
3+
*
4+
* Copyright (c) William278 <will27528@gmail.com>
5+
* Copyright (c) contributors
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package net.william278.velocitab.api;
21+
22+
import com.velocitypowered.api.proxy.Player;
23+
import lombok.RequiredArgsConstructor;
24+
import net.kyori.adventure.text.Component;
25+
import net.william278.velocitab.Velocitab;
26+
import net.william278.velocitab.player.TabPlayer;
27+
import org.jetbrains.annotations.NotNull;
28+
import org.jetbrains.annotations.Nullable;
29+
import org.slf4j.event.Level;
30+
31+
import java.util.concurrent.TimeUnit;
32+
import java.util.concurrent.TimeoutException;
33+
34+
/**
35+
* Utility class responsible for firing Velocitab API events with timeout protection
36+
* and snapshot-based rollback to prevent partial mutations from misbehaving listeners.
37+
*
38+
* <p>If a listener does not complete within {@link #EVENT_TIMEOUT_MS} milliseconds, the
39+
* original (pre-event) component values are used and a warning is logged.</p>
40+
*
41+
* @since 1.6.9
42+
*/
43+
@RequiredArgsConstructor
44+
public class EventDispatcher {
45+
46+
/**
47+
* Maximum time in milliseconds a set of event listeners may collectively take.
48+
* If exceeded, original values are used and a warning is logged.
49+
*/
50+
public static final long EVENT_TIMEOUT_MS = 50;
51+
52+
@NotNull
53+
private final Velocitab plugin;
54+
55+
/**
56+
* Fires a {@link TabDisplayNameEvent} and returns the (possibly modified) display name.
57+
*
58+
* <p>Original values are snapshotted before firing. On timeout or exception the
59+
* snapshot is returned, guaranteeing no partial mutation reaches the caller.</p>
60+
*
61+
* @param player the player whose display name is being set
62+
* @param viewer the player who will see the entry
63+
* @param displayName the pre-computed display name (snapshot)
64+
* @return the display name to use, possibly modified by a listener
65+
*/
66+
@NotNull
67+
public Component fireDisplayNameEvent(@NotNull TabPlayer player, @NotNull TabPlayer viewer,
68+
@NotNull Component displayName) {
69+
final TabDisplayNameEvent event = new TabDisplayNameEvent(player, viewer, displayName);
70+
try {
71+
plugin.getServer().getEventManager()
72+
.fire(event)
73+
.get(EVENT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
74+
return event.getDisplayName();
75+
} catch (TimeoutException e) {
76+
plugin.log(Level.WARN, "TabDisplayNameEvent timed out after %dms, using original value"
77+
.formatted(EVENT_TIMEOUT_MS));
78+
return displayName;
79+
} catch (Exception e) {
80+
plugin.log(Level.ERROR, "Error firing TabDisplayNameEvent", e);
81+
return displayName;
82+
}
83+
}
84+
85+
/**
86+
* Fires a {@link TabHeaderFooterEvent} and returns the (possibly modified) header and footer
87+
* as a two-element array {@code [header, footer]}.
88+
*
89+
* <p>Original values are snapshotted before firing. On timeout or exception the
90+
* snapshots are returned, guaranteeing no partial mutation reaches the caller.</p>
91+
*
92+
* @param player the player who will receive the header/footer
93+
* @param header the pre-computed header component (snapshot)
94+
* @param footer the pre-computed footer component (snapshot)
95+
* @return a two-element array containing {@code [header, footer]} to use
96+
*/
97+
@NotNull
98+
public Component[] fireHeaderFooterEvent(@NotNull TabPlayer player,
99+
@NotNull Component header,
100+
@NotNull Component footer) {
101+
final TabHeaderFooterEvent event = new TabHeaderFooterEvent(player, header, footer);
102+
try {
103+
plugin.getServer().getEventManager()
104+
.fire(event)
105+
.get(EVENT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
106+
return new Component[]{event.getHeader(), event.getFooter()};
107+
} catch (TimeoutException e) {
108+
plugin.log(Level.WARN, "TabHeaderFooterEvent timed out after %dms, using original values"
109+
.formatted(EVENT_TIMEOUT_MS));
110+
return new Component[]{header, footer};
111+
} catch (Exception e) {
112+
plugin.log(Level.ERROR, "Error firing TabHeaderFooterEvent", e);
113+
return new Component[]{header, footer};
114+
}
115+
}
116+
117+
/**
118+
* Fires a {@link TabTeamUpdateEvent} and returns the (possibly modified) event.
119+
*
120+
* <p>Original values are snapshotted before firing. On timeout or exception the
121+
* snapshots are returned via a restored event, guaranteeing no partial mutation
122+
* reaches the caller.</p>
123+
*
124+
* @param player the player whose nametag is being sent
125+
* @param viewer the player receiving the team packet
126+
* @param prefix the pre-computed prefix component (snapshot), or null
127+
* @param suffix the pre-computed suffix component (snapshot), or null
128+
* @param displayName the pre-computed display name component (snapshot), or null
129+
* @param mode whether this is a CREATE or UPDATE packet
130+
* @return the event whose fields reflect the final component values to use
131+
*/
132+
@NotNull
133+
public TabTeamUpdateEvent fireTeamUpdateEvent(@NotNull TabPlayer player, @NotNull Player viewer,
134+
@Nullable Component prefix, @Nullable Component suffix,
135+
@Nullable Component displayName,
136+
@NotNull TabTeamUpdateEvent.Mode mode) {
137+
final TabTeamUpdateEvent event = new TabTeamUpdateEvent(player, viewer, prefix, suffix, displayName, mode);
138+
try {
139+
plugin.getServer().getEventManager()
140+
.fire(event)
141+
.get(EVENT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
142+
return event;
143+
} catch (TimeoutException e) {
144+
plugin.log(Level.WARN, "TabTeamUpdateEvent timed out after %dms, using original values"
145+
.formatted(EVENT_TIMEOUT_MS));
146+
// Restore snapshots to guard against partial mutation
147+
return new TabTeamUpdateEvent(player, viewer, prefix, suffix, displayName, mode);
148+
} catch (Exception e) {
149+
plugin.log(Level.ERROR, "Error firing TabTeamUpdateEvent", e);
150+
return new TabTeamUpdateEvent(player, viewer, prefix, suffix, displayName, mode);
151+
}
152+
}
153+
154+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* This file is part of Velocitab, licensed under the Apache License 2.0.
3+
*
4+
* Copyright (c) William278 <will27528@gmail.com>
5+
* Copyright (c) contributors
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package net.william278.velocitab.api;
21+
22+
import lombok.Getter;
23+
import lombok.Setter;
24+
import net.kyori.adventure.text.Component;
25+
import net.william278.velocitab.player.TabPlayer;
26+
import org.jetbrains.annotations.NotNull;
27+
28+
/**
29+
* Fired when Velocitab is about to set a player's display name in the tab list.
30+
* <p>
31+
* Listeners may modify {@link #setDisplayName(Component)} to transform the component
32+
* before it is sent (e.g. to resolve custom font glyphs).
33+
* This event is a pure transformation hook and is not cancellable.
34+
*
35+
* @since 1.6.9
36+
*/
37+
@Getter
38+
@SuppressWarnings("unused")
39+
public class TabDisplayNameEvent {
40+
41+
@NotNull
42+
private final TabPlayer player;
43+
@NotNull
44+
private final TabPlayer viewer;
45+
@Setter
46+
@NotNull
47+
private Component displayName;
48+
49+
/**
50+
* Creates a new {@link TabDisplayNameEvent}.
51+
*
52+
* @param player the player whose display name is being set
53+
* @param viewer the player who will see this tab list entry
54+
* @param displayName the computed display name component
55+
*/
56+
public TabDisplayNameEvent(@NotNull TabPlayer player, @NotNull TabPlayer viewer,
57+
@NotNull Component displayName) {
58+
this.player = player;
59+
this.viewer = viewer;
60+
this.displayName = displayName;
61+
}
62+
63+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* This file is part of Velocitab, licensed under the Apache License 2.0.
3+
*
4+
* Copyright (c) William278 <will27528@gmail.com>
5+
* Copyright (c) contributors
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package net.william278.velocitab.api;
21+
22+
import lombok.Getter;
23+
import lombok.Setter;
24+
import net.kyori.adventure.text.Component;
25+
import net.william278.velocitab.player.TabPlayer;
26+
import org.jetbrains.annotations.NotNull;
27+
28+
/**
29+
* Fired when Velocitab is about to send the tab list header and footer to a player.
30+
* <p>
31+
* Listeners may modify {@link #setHeader(Component)} and/or {@link #setFooter(Component)}
32+
* to transform the components before they are sent (e.g. to resolve custom font glyphs).
33+
* This event is a pure transformation hook and is not cancellable.
34+
*
35+
* @since 1.6.9
36+
*/
37+
@Getter
38+
@SuppressWarnings("unused")
39+
public class TabHeaderFooterEvent {
40+
41+
@NotNull
42+
private final TabPlayer player;
43+
@Setter
44+
@NotNull
45+
private Component header;
46+
@Setter
47+
@NotNull
48+
private Component footer;
49+
50+
/**
51+
* Creates a new {@link TabHeaderFooterEvent}.
52+
*
53+
* @param player the player who is about to receive the header and footer
54+
* @param header the computed header component
55+
* @param footer the computed footer component
56+
*/
57+
public TabHeaderFooterEvent(@NotNull TabPlayer player,
58+
@NotNull Component header,
59+
@NotNull Component footer) {
60+
this.player = player;
61+
this.header = header;
62+
this.footer = footer;
63+
}
64+
65+
}

0 commit comments

Comments
 (0)