-
-
Notifications
You must be signed in to change notification settings - Fork 92
/
WrappedServerPing.java
443 lines (389 loc) · 13.5 KB
/
WrappedServerPing.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
package com.comphenix.protocol.wrappers;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import javax.imageio.ImageIO;
import org.bukkit.entity.Player;
import net.minecraft.util.com.mojang.authlib.GameProfile;
import net.minecraft.util.io.netty.buffer.ByteBuf;
import net.minecraft.util.io.netty.buffer.Unpooled;
import net.minecraft.util.io.netty.handler.codec.base64.Base64;
import net.minecraft.util.io.netty.util.IllegalReferenceCountException;
import com.comphenix.protocol.injector.BukkitUnwrapper;
import com.comphenix.protocol.reflect.EquivalentConverter;
import com.comphenix.protocol.reflect.accessors.Accessors;
import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
import com.comphenix.protocol.reflect.accessors.FieldAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
/**
* Represents a server ping packet data.
* @author Kristian
*/
public class WrappedServerPing extends AbstractWrapper {
// Server ping fields
private static Class<?> SERVER_PING = MinecraftReflection.getServerPingClass();
private static ConstructorAccessor SERVER_PING_CONSTRUCTOR = Accessors.getConstructorAccessor(SERVER_PING);
private static FieldAccessor DESCRIPTION = Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getIChatBaseComponentClass(), true);
private static FieldAccessor PLAYERS = Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getServerPingPlayerSampleClass(), true);
private static FieldAccessor VERSION = Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getServerPingServerDataClass(), true);
private static FieldAccessor FAVICON = Accessors.getFieldAccessor(SERVER_PING, String.class, true);
// For converting to the underlying array
private static EquivalentConverter<Iterable<? extends WrappedGameProfile>> PROFILE_CONVERT =
BukkitConverters.getArrayConverter(GameProfile.class, BukkitConverters.getWrappedGameProfileConverter());
// Server ping player sample fields
private static Class<?> PLAYERS_CLASS = MinecraftReflection.getServerPingPlayerSampleClass();
private static ConstructorAccessor PLAYERS_CONSTRUCTOR = Accessors.getConstructorAccessor(PLAYERS_CLASS, int.class, int.class);
private static FieldAccessor[] PLAYERS_INTS = Accessors.getFieldAccessorArray(PLAYERS_CLASS, int.class, true);
private static FieldAccessor PLAYERS_PROFILES = Accessors.getFieldAccessor(PLAYERS_CLASS, GameProfile[].class, true);
private static FieldAccessor PLAYERS_MAXIMUM = PLAYERS_INTS[0];
private static FieldAccessor PLAYERS_ONLINE = PLAYERS_INTS[1];
// Server data fields
private static Class<?> VERSION_CLASS = MinecraftReflection.getServerPingServerDataClass();
private static ConstructorAccessor VERSION_CONSTRUCTOR = Accessors.getConstructorAccessor(VERSION_CLASS, String.class, int.class);
private static FieldAccessor VERSION_NAME = Accessors.getFieldAccessor(VERSION_CLASS, String.class, true);
private static FieldAccessor VERSION_PROTOCOL = Accessors.getFieldAccessor(VERSION_CLASS, int.class, true);
// Get profile from player
private static FieldAccessor ENTITY_HUMAN_PROFILE = Accessors.getFieldAccessor(
MinecraftReflection.getEntityPlayerClass().getSuperclass(), GameProfile.class, true);
// Inner class
private Object players;
private Object version;
/**
* Construct a new server ping initialized with empty values.
*/
public WrappedServerPing() {
super(MinecraftReflection.getServerPingClass());
setHandle(SERVER_PING_CONSTRUCTOR.invoke());
this.players = PLAYERS_CONSTRUCTOR.invoke(0, 0);
this.version = VERSION_CONSTRUCTOR.invoke(MinecraftVersion.WORLD_UPDATE.toString(), 4);
PLAYERS.set(handle, players);
VERSION.set(handle, version);
}
private WrappedServerPing(Object handle) {
super(MinecraftReflection.getServerPingClass());
setHandle(handle);
this.players = PLAYERS.get(handle);
this.version = VERSION.get(handle);
}
/**
* Construct a wrapped server ping from a native NMS object.
* @param handle - the native object.
* @return The wrapped server ping object.
*/
public static WrappedServerPing fromHandle(Object handle) {
return new WrappedServerPing(handle);
}
/**
* Retrieve the message of the day.
* @return The messge of the day.
*/
public WrappedChatComponent getMotD() {
return WrappedChatComponent.fromHandle(DESCRIPTION.get(handle));
}
/**
* Set the message of the day.
* @param description - message of the day.
*/
public void setMotD(WrappedChatComponent description) {
DESCRIPTION.set(handle, description.getHandle());
}
/**
* Set the message of the day.
* <p>
* <b>Warning:</b> Only the first line will be transmitted.
* @param description - the message.
*/
public void setMotD(String message) {
setMotD(WrappedChatComponent.fromChatMessage(message)[0]);
}
/**
* Retrieve the compressed PNG file that is being displayed as a favicon.
* @return The favicon.
*/
public CompressedImage getFavicon() {
return CompressedImage.fromEncodedText((String) FAVICON.get(handle));
}
/**
* Set the compressed PNG file that is being displayed.
* @param image - the new compressed image.
*/
public void setFavicon(CompressedImage image) {
FAVICON.set(handle, image.toEncodedText());
}
/**
* Retrieve the displayed number of online players.
* @return The displayed number.
*/
public int getPlayersOnline() {
return (Integer) PLAYERS_ONLINE.get(players);
}
/**
* Set the displayed number of online players.
* @param online - online players.
*/
public void setPlayersOnline(int online) {
PLAYERS_ONLINE.set(players, online);
}
/**
* Retrieve the displayed maximum number of players.
* @return The maximum number.
*/
public int getPlayersMaximum() {
return (Integer) PLAYERS_MAXIMUM.get(players);
}
/**
* Set the displayed maximum number of players.
* @param maximum - maximum player count.
*/
public void setPlayersMaximum(int maximum) {
PLAYERS_MAXIMUM.set(players, maximum);
}
/**
* Retrieve a copy of all the logged in players.
* @return Logged in players.
*/
public ImmutableList<WrappedGameProfile> getPlayers() {
return ImmutableList.copyOf(PROFILE_CONVERT.getSpecific(PLAYERS_PROFILES.get(players)));
}
/**
* Set the displayed list of logged in players.
* @param profile - every logged in player.
*/
public void setPlayers(Iterable<? extends WrappedGameProfile> profile) {
PLAYERS_PROFILES.set(players, PROFILE_CONVERT.getGeneric(GameProfile[].class, profile));
}
/**
* Set the displayed lst of logged in players.
* @param players - the players to display.
*/
public void setBukkitPlayers(Iterable<? extends Player> players) {
List<WrappedGameProfile> profiles = Lists.newArrayList();
for (Player player : players) {
GameProfile profile = (GameProfile) ENTITY_HUMAN_PROFILE.get(BukkitUnwrapper.getInstance().unwrapItem(player));
profiles.add(new WrappedGameProfile(profile.getId(), profile.getName()));
}
setPlayers(profiles);
}
/**
* Retrieve the version name of the current server.
* @return The version name.
*/
public String getVersionName() {
return (String) VERSION_NAME.get(version);
}
/**
* Set the version name of the current server.
* @param name - the new version name.
*/
public void setVersionName(String name) {
VERSION_NAME.set(version, name);
}
/**
* Retrieve the protocol number.
* @return The protocol.
*/
public int getVersionProtocol() {
return (Integer) VERSION_PROTOCOL.get(version);
}
/**
* Set the version protocol
* @param protocol - the protocol number.
*/
public void setVersionProtocol(int protocol) {
VERSION_PROTOCOL.set(version, protocol);
}
/**
* Retrieve a deep copy of the current wrapper object.
* @return The current object.
*/
public WrappedServerPing deepClone() {
WrappedServerPing copy = new WrappedServerPing();
WrappedChatComponent motd = getMotD();
copy.setPlayers(getPlayers());
copy.setFavicon(getFavicon());
copy.setMotD(motd != null ? motd.deepClone() : null);
copy.setPlayersMaximum(getPlayersMaximum());
copy.setPlayersOnline(getPlayersOnline());
copy.setVersionName(getVersionName());
copy.setVersionProtocol(getVersionProtocol());
return copy;
}
/**
* Represents a compressed favicon.
* @author Kristian
*/
// Should not have been an inner class ... oh well.
public static class CompressedImage {
protected volatile String mime;
protected volatile byte[] data;
protected volatile String encoded;
/**
* Represents a compressed image with no content.
*/
protected CompressedImage() {
// Derived class should initialize some of the fields
}
/**
* Construct a new compressed image.
* @param mime - the mime type.
* @param data - the raw compressed image data.
*/
public CompressedImage(String mime, byte[] data) {
this.mime = Preconditions.checkNotNull(mime, "mime cannot be NULL");
this.data = Preconditions.checkNotNull(data, "data cannot be NULL");
}
/**
* Retrieve a compressed image from an input stream.
* @param input - the PNG as an input stream.
* @return The compressed image.
* @throws IOException If we cannot read the input stream.
*/
public static CompressedImage fromPng(InputStream input) throws IOException {
return new CompressedImage("image/png", ByteStreams.toByteArray(input));
}
/**
* Retrieve a compressed image from a byte array of a PNG file.
* @param data - the file as a byte array.
* @return The compressed image.
*/
public static CompressedImage fromPng(byte[] data) {
return new CompressedImage("image/png", data);
}
/**
* Retrieve a compressed image from a base-64 encoded PNG file.
* @param base64 - the base 64-encoded PNG.
* @return The compressed image.
*/
public static CompressedImage fromBase64Png(String base64) {
try {
return new EncodedCompressedImage("data:image/png;base64," + base64);
} catch (IllegalArgumentException e) {
// Remind the caller
throw new IllegalReferenceCountException("Must be a pure base64 encoded string. Cannot be an encoded text.", e);
}
}
/**
* Retrieve a compressed image from an image.
* @param image - the image.
* @throws IOException If we were unable to compress the image.
*/
public static CompressedImage fromPng(RenderedImage image) throws IOException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
ImageIO.write(image, "png", output);
return new CompressedImage("image/png", output.toByteArray());
}
/**
* Retrieve a compressed image from an encoded text.
* @param text - the encoded text.
* @return The corresponding compressed image.
*/
public static CompressedImage fromEncodedText(String text) {
return new EncodedCompressedImage(text);
}
/**
* Retrieve the MIME type of the image.
* <p>
* This is image/png in vanilla Minecraft.
* @return The MIME type.
*/
public String getMime() {
return mime;
}
/**
* Retrieve a copy of the underlying data array.
* @return The underlying compressed image.
*/
public byte[] getDataCopy() {
return getData().clone();
}
/**
* Retrieve the underlying data, with no copying.
* @return The underlying data.
*/
protected byte[] getData() {
return data;
}
/**
* Uncompress and return the stored image.
* @return The image.
* @throws IOException If the image data could not be decoded.
*/
public BufferedImage getImage() throws IOException {
return ImageIO.read(new ByteArrayInputStream(getData()));
}
/**
* Convert the compressed image to encoded text.
* @return The encoded text.
*/
public String toEncodedText() {
if (encoded == null) {
final ByteBuf buffer = Unpooled.wrappedBuffer(getData());
String computed = "data:" + mime + ";base64," +
Base64.encode(buffer).toString(Charsets.UTF_8);
encoded = computed;
}
return encoded;
}
}
/**
* Represents a compressed image that starts out as an encoded base 64 string.
* @author Kristian
*/
private static class EncodedCompressedImage extends CompressedImage {
public EncodedCompressedImage(String encoded) {
this.encoded = encoded;
}
/**
* Ensure that we have decoded the content of the encoded text.
*/
protected void initialize() {
if (mime == null || data == null) {
decode();
}
}
/**
* Decode the encoded text.
*/
protected void decode() {
for (String segment : Splitter.on(";").split(encoded)) {
if (segment.startsWith("data:")) {
this.mime = segment.substring(5);
} else if (segment.startsWith("base64,")) {
byte[] encoded = segment.substring(7).getBytes(Charsets.UTF_8);
ByteBuf decoded = Base64.decode(Unpooled.wrappedBuffer(encoded));
// Read into a byte array
byte[] data = new byte[decoded.readableBytes()];
decoded.readBytes(data);
this.data = data;
} else {
// We will ignore these segments
}
}
}
@Override
protected byte[] getData() {
initialize();
return super.getData();
}
@Override
public String getMime() {
initialize();
return super.getMime();
}
@Override
public String toEncodedText() {
return encoded;
}
}
}