children = new ListTag<>("Children");
62 | for (StructurePiece piece : this.pieces) {
63 | children.add(piece.createTag());
64 | }
65 | tag.putList(children);
66 |
67 | return tag;
68 | }
69 |
70 | protected void moveBelowSeaLevel(int max, NukkitRandom random, int min) {
71 | int range = max - min;
72 | int y = this.boundingBox.getYSpan() + 1;
73 | if (y < range) {
74 | y += random.nextBoundedInt(range - y);
75 | }
76 |
77 | int offset = y - this.boundingBox.y1;
78 | this.boundingBox.move(0, offset, 0);
79 |
80 | for (StructurePiece piece : this.pieces) {
81 | piece.move(0, offset, 0);
82 | }
83 | }
84 |
85 | protected void moveInsideHeights(NukkitRandom random, int min, int max) {
86 | int range = max - min + 1 - this.boundingBox.getYSpan();
87 | int y;
88 | if (range > 1) {
89 | y = min + random.nextBoundedInt(range);
90 | } else {
91 | y = min;
92 | }
93 |
94 | int offset = y - this.boundingBox.y0;
95 | this.boundingBox.move(0, offset, 0);
96 |
97 | for (StructurePiece piece : this.pieces) {
98 | piece.move(0, offset, 0);
99 | }
100 | }
101 |
102 | public boolean isValid() {
103 | return !this.pieces.isEmpty();
104 | }
105 |
106 | public int getChunkX() {
107 | return this.chunkX;
108 | }
109 |
110 | public int getChunkZ() {
111 | return this.chunkZ;
112 | }
113 |
114 | public abstract String getType();
115 | }
116 |
--------------------------------------------------------------------------------
/src/main/java/cn/wode490390/nukkit/vipop/util/MetricsLite.java:
--------------------------------------------------------------------------------
1 | package cn.wode490390.nukkit.vipop.util;
2 |
3 | import cn.nukkit.Server;
4 | import cn.nukkit.plugin.Plugin;
5 | import cn.nukkit.plugin.service.NKServiceManager;
6 | import cn.nukkit.plugin.service.RegisteredServiceProvider;
7 | import cn.nukkit.plugin.service.ServicePriority;
8 | import cn.nukkit.utils.Config;
9 | import com.google.common.base.Preconditions;
10 | import com.google.gson.JsonArray;
11 | import com.google.gson.JsonElement;
12 | import com.google.gson.JsonObject;
13 |
14 | import javax.net.ssl.HttpsURLConnection;
15 | import java.io.BufferedReader;
16 | import java.io.ByteArrayOutputStream;
17 | import java.io.DataOutputStream;
18 | import java.io.File;
19 | import java.io.IOException;
20 | import java.io.InputStreamReader;
21 | import java.lang.reflect.Field;
22 | import java.lang.reflect.InvocationTargetException;
23 | import java.lang.reflect.Modifier;
24 | import java.net.URL;
25 | import java.nio.charset.StandardCharsets;
26 | import java.util.LinkedHashMap;
27 | import java.util.List;
28 | import java.util.Map;
29 | import java.util.Timer;
30 | import java.util.TimerTask;
31 | import java.util.UUID;
32 | import java.util.zip.GZIPOutputStream;
33 |
34 | /**
35 | * bStats collects some data for plugin authors.
36 | *
37 | * Check out https://bStats.org/ to learn more about bStats!
38 | */
39 | @SuppressWarnings({"WeakerAccess", "unused"})
40 | public class MetricsLite {
41 |
42 | static {
43 | // You can use the property to disable the check in your test environment
44 | if (System.getProperty("bstats.relocatecheck") == null || !System.getProperty("bstats.relocatecheck").equals("false")) {
45 | // Maven's Relocate is clever and changes strings, too. So we have to use this little "trick" ... :D
46 | final String defaultPackage = new String(new byte[]{'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's', '.', 'n', 'u', 'k', 'k', 'i', 't'});
47 | final String examplePackage = new String(new byte[]{'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'});
48 | // We want to make sure nobody just copy & pastes the example and use the wrong package names
49 | if (MetricsLite.class.getPackage().getName().equals(defaultPackage) || MetricsLite.class.getPackage().getName().equals(examplePackage)) {
50 | throw new IllegalStateException("bStats Metrics class has not been relocated correctly!");
51 | }
52 | }
53 | }
54 |
55 | // The version of this bStats class
56 | public static final int B_STATS_VERSION = 1;
57 |
58 | // The url to which the data is sent
59 | private static final String URL = "https://bStats.org/submitData/bukkit";
60 |
61 | // Is bStats enabled on this server?
62 | private boolean enabled;
63 |
64 | // Should failed requests be logged?
65 | private static boolean logFailedRequests;
66 |
67 | // Should the sent data be logged?
68 | private static boolean logSentData;
69 |
70 | // Should the response text be logged?
71 | private static boolean logResponseStatusText;
72 |
73 | // The uuid of the server
74 | private static String serverUUID;
75 |
76 | // The plugin
77 | private final Plugin plugin;
78 |
79 | // The plugin id
80 | private final int pluginId;
81 |
82 | /**
83 | * Class constructor.
84 | *
85 | * @param plugin The plugin which stats should be submitted.
86 | * @param pluginId The id of the plugin.
87 | * It can be found at What is my plugin id?
88 | */
89 | public MetricsLite(Plugin plugin, int pluginId) {
90 | Preconditions.checkNotNull(plugin);
91 | this.plugin = plugin;
92 | this.pluginId = pluginId;
93 |
94 | // Get the config file
95 | File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats");
96 | File configFile = new File(bStatsFolder, "config.yml");
97 | Config config = new Config(configFile);
98 |
99 | // Check the config
100 | LinkedHashMap map = (LinkedHashMap) config.getAll();
101 | // Every server gets it's unique random id.
102 | if (!config.isString("serverUuid")) {
103 | map.put("serverUuid", UUID.randomUUID().toString());
104 | } else {
105 | try {
106 | // Check the UUID
107 | UUID.fromString(config.getString("serverUuid"));
108 | } catch (Exception ignored){
109 | map.put("serverUuid", UUID.randomUUID().toString());
110 | }
111 | }
112 | // Add default values
113 | if (!config.isBoolean("enabled")) {
114 | map.put("enabled", true);
115 | }
116 | // Should failed request be logged?
117 | if (!config.isBoolean("logFailedRequests")) {
118 | map.put("logFailedRequests", false);
119 | }
120 | // Should the sent data be logged?
121 | if (!config.isBoolean("logSentData")) {
122 | map.put("logSentData", false);
123 | }
124 | // Should the response text be logged?
125 | if (!config.isBoolean("logResponseStatusText")) {
126 | map.put("logResponseStatusText", false);
127 | }
128 | config.setAll(map);
129 | config.save();
130 |
131 | // Load the data
132 | enabled = config.getBoolean("enabled", true);
133 | serverUUID = config.getString("serverUuid");
134 | logFailedRequests = config.getBoolean("logFailedRequests", false);
135 | logSentData = config.getBoolean("logSentData", false);
136 | logResponseStatusText = config.getBoolean("logResponseStatusText", false);
137 |
138 | if (enabled) {
139 | boolean found = false;
140 | // Search for all other bStats Metrics classes to see if we are the first one
141 | for (Class> service : Server.getInstance().getServiceManager().getKnownService()) {
142 | try {
143 | service.getField("B_STATS_VERSION"); // Our identifier :)
144 | found = true; // We aren't the first
145 | break;
146 | } catch (NoSuchFieldException ignored) { }
147 | }
148 | // Register our service
149 | Server.getInstance().getServiceManager().register(MetricsLite.class, this, plugin, ServicePriority.NORMAL);
150 | if (!found) {
151 | // We are the first!
152 | startSubmitting();
153 | }
154 | }
155 | }
156 |
157 | /**
158 | * Checks if bStats is enabled.
159 | *
160 | * @return Whether bStats is enabled or not.
161 | */
162 | public boolean isEnabled() {
163 | return enabled;
164 | }
165 |
166 | /**
167 | * Starts the Scheduler which submits our data every 30 minutes.
168 | */
169 | private void startSubmitting() {
170 | final Timer timer = new Timer(true); // We use a timer cause want to be independent from the server tps
171 | timer.scheduleAtFixedRate(new TimerTask() {
172 | @Override
173 | public void run() {
174 | if (!plugin.isEnabled()) { // Plugin was disabled
175 | timer.cancel();
176 | return;
177 | }
178 | // Nevertheless we want our code to run in the Nukkit main thread, so we have to use the Nukkit scheduler
179 | // Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;)
180 | Server.getInstance().getScheduler().scheduleTask(plugin, () -> submitData());
181 | }
182 | }, 1000 * 60 * 5, 1000 * 60 * 30);
183 | // Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start
184 | // WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted!
185 | // WARNING: Just don't do it!
186 | }
187 |
188 | /**
189 | * Gets the plugin specific data.
190 | * This method is called using Reflection.
191 | *
192 | * @return The plugin specific data.
193 | */
194 | public JsonObject getPluginData() {
195 | JsonObject data = new JsonObject();
196 |
197 | String pluginName = plugin.getName();
198 | String pluginVersion = plugin.getDescription().getVersion();
199 |
200 | data.addProperty("pluginName", pluginName); // Append the name of the plugin
201 | data.addProperty("id", pluginId); // Append the id of the plugin
202 | data.addProperty("pluginVersion", pluginVersion); // Append the version of the plugin
203 |
204 | JsonArray customCharts = new JsonArray();
205 | data.add("customCharts", customCharts);
206 |
207 | return data;
208 | }
209 |
210 | /**
211 | * Gets the server specific data.
212 | *
213 | * @return The server specific data.
214 | */
215 | private JsonObject getServerData() {
216 | // Minecraft specific data
217 | int playerAmount = Server.getInstance().getOnlinePlayers().size();
218 | int onlineMode = Server.getInstance().getPropertyBoolean("xbox-auth", false) ? 1 : 0;
219 | String minecraftVersion = Server.getInstance().getVersion();
220 | String softwareName = Server.getInstance().getName();
221 |
222 | // OS/Java specific data
223 | String javaVersion = System.getProperty("java.version");
224 | String osName = System.getProperty("os.name");
225 | String osArch = System.getProperty("os.arch");
226 | String osVersion = System.getProperty("os.version");
227 | int coreCount = Runtime.getRuntime().availableProcessors();
228 |
229 | JsonObject data = new JsonObject();
230 |
231 | data.addProperty("serverUUID", serverUUID);
232 |
233 | data.addProperty("playerAmount", playerAmount);
234 | data.addProperty("onlineMode", onlineMode);
235 | data.addProperty("bukkitVersion", minecraftVersion);
236 | data.addProperty("bukkitName", softwareName);
237 |
238 | data.addProperty("javaVersion", javaVersion);
239 | data.addProperty("osName", osName);
240 | data.addProperty("osArch", osArch);
241 | data.addProperty("osVersion", osVersion);
242 | data.addProperty("coreCount", coreCount);
243 |
244 | return data;
245 | }
246 |
247 | /**
248 | * Collects the data and sends it afterwards.
249 | */
250 | @SuppressWarnings("unchecked")
251 | private void submitData() {
252 | final JsonObject data = getServerData();
253 |
254 | JsonArray pluginData = new JsonArray();
255 | // Search for all other bStats Metrics classes to get their plugin data
256 | Server.getInstance().getServiceManager().getKnownService().forEach((service) -> {
257 | try {
258 | service.getField("B_STATS_VERSION"); // Our identifier :)
259 |
260 | List> providers = null;
261 | try {
262 | Field field = Field.class.getDeclaredField("modifiers");
263 | field.setAccessible(true);
264 | Field handle = NKServiceManager.class.getDeclaredField("handle");
265 | field.setInt(handle, handle.getModifiers() & ~Modifier.FINAL);
266 | handle.setAccessible(true);
267 | providers = ((Map, List>>) handle.get((NKServiceManager) (Server.getInstance().getServiceManager()))).get(service);
268 | } catch(IllegalAccessException | IllegalArgumentException | SecurityException e) {
269 | // Something went wrong! :(
270 | if (logFailedRequests) {
271 | plugin.getLogger().warning("Failed to link to metrics class " + service.getName(), e);
272 | }
273 | }
274 |
275 | if (providers != null) {
276 | for (RegisteredServiceProvider> provider : providers) {
277 | try {
278 | Object plugin = provider.getService().getMethod("getPluginData").invoke(provider.getProvider());
279 | if (plugin instanceof JsonObject) {
280 | pluginData.add((JsonElement) plugin);
281 | }
282 | } catch (SecurityException | NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ignored) { }
283 | }
284 | }
285 | } catch (NoSuchFieldException ignored) { }
286 | });
287 |
288 | data.add("plugins", pluginData);
289 |
290 | // Create a new thread for the connection to the bStats server
291 | new Thread(() -> {
292 | try {
293 | // Send the data
294 | sendData(plugin, data);
295 | } catch (Exception e) {
296 | // Something went wrong! :(
297 | if (logFailedRequests) {
298 | plugin.getLogger().warning("Could not submit plugin stats of " + plugin.getName(), e);
299 | }
300 | }
301 | }).start();
302 | }
303 |
304 | /**
305 | * Sends the data to the bStats server.
306 | *
307 | * @param plugin Any plugin. It's just used to get a logger instance.
308 | * @param data The data to send.
309 | * @throws Exception If the request failed.
310 | */
311 | private static void sendData(Plugin plugin, JsonObject data) throws Exception {
312 | Preconditions.checkNotNull(data);
313 | if (Server.getInstance().isPrimaryThread()) {
314 | throw new IllegalAccessException("This method must not be called from the main thread!");
315 | }
316 | if (logSentData) {
317 | plugin.getLogger().info("Sending data to bStats: " + data);
318 | }
319 | HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection();
320 |
321 | // Compress the data to save bandwidth
322 | byte[] compressedData = compress(data.toString());
323 |
324 | // Add headers
325 | connection.setRequestMethod("POST");
326 | connection.addRequestProperty("Accept", "application/json");
327 | connection.addRequestProperty("Connection", "close");
328 | connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request
329 | connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length));
330 | connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format
331 | connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION);
332 |
333 | // Send data
334 | connection.setDoOutput(true);
335 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) {
336 | outputStream.write(compressedData);
337 | }
338 |
339 | StringBuilder builder = new StringBuilder();
340 | try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
341 | String line;
342 | while ((line = bufferedReader.readLine()) != null) {
343 | builder.append(line);
344 | }
345 | }
346 |
347 | if (logResponseStatusText) {
348 | plugin.getLogger().info("Sent data to bStats and received response: " + builder);
349 | }
350 | }
351 |
352 | /**
353 | * Gzips the given String.
354 | *
355 | * @param str The string to gzip.
356 | * @return The gzipped String.
357 | * @throws IOException If the compression failed.
358 | */
359 | private static byte[] compress(final String str) throws IOException {
360 | if (str == null) {
361 | return null;
362 | }
363 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
364 | try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) {
365 | gzip.write(str.getBytes(StandardCharsets.UTF_8));
366 | }
367 | return outputStream.toByteArray();
368 | }
369 |
370 | }
371 |
--------------------------------------------------------------------------------
/src/main/resources/plugin.yml:
--------------------------------------------------------------------------------
1 | main: cn.wode490390.nukkit.vipop.ClassicVillagePlugin
2 | name: "ClassicVillagePopulator"
3 | description: "This is a plugin that implements the old village feature for Nukkit servers"
4 | author: "wode490390"
5 | website: "http://wode490390.cn/"
6 | version: "${pom.version}"
7 | api: ["1.0.0"]
8 | load: STARTUP
9 | softdepend: ["MobPlugin"]
10 |
--------------------------------------------------------------------------------