001/* 002 * PlotSquared, a land and world management plugin for Minecraft. 003 * Copyright (C) IntellectualSites <https://intellectualsites.com> 004 * Copyright (C) IntellectualSites team and contributors 005 * 006 * This program is free software: you can redistribute it and/or modify 007 * it under the terms of the GNU General Public License as published by 008 * the Free Software Foundation, either version 3 of the License, or 009 * (at your option) any later version. 010 * 011 * This program is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 014 * GNU General Public License for more details. 015 * 016 * You should have received a copy of the GNU General Public License 017 * along with this program. If not, see <https://www.gnu.org/licenses/>. 018 */ 019package com.plotsquared.core.plot.expiration; 020 021import com.plotsquared.core.PlotSquared; 022import com.plotsquared.core.configuration.Settings; 023import com.plotsquared.core.generator.HybridUtils; 024import com.plotsquared.core.plot.Plot; 025import com.plotsquared.core.plot.flag.implementations.AnalysisFlag; 026import com.plotsquared.core.util.MathMan; 027import com.plotsquared.core.util.query.PlotQuery; 028import com.plotsquared.core.util.task.RunnableVal; 029import com.plotsquared.core.util.task.TaskManager; 030import org.apache.logging.log4j.LogManager; 031import org.apache.logging.log4j.Logger; 032 033import java.lang.reflect.Array; 034import java.util.ArrayDeque; 035import java.util.ArrayList; 036import java.util.Arrays; 037import java.util.Iterator; 038import java.util.List; 039import java.util.concurrent.atomic.AtomicInteger; 040 041public class PlotAnalysis { 042 043 private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + PlotAnalysis.class.getSimpleName()); 044 045 public static boolean running = false; 046 public int changes; 047 public int faces; 048 public int data; 049 public int air; 050 public int variety; 051 public int changes_sd; 052 public int faces_sd; 053 public int data_sd; 054 public int air_sd; 055 public int variety_sd; 056 private int complexity; 057 058 public static PlotAnalysis getAnalysis(Plot plot, Settings.Auto_Clear settings) { 059 final List<Integer> values = plot.getFlag(AnalysisFlag.class); 060 if (!values.isEmpty()) { 061 PlotAnalysis analysis = new PlotAnalysis(); 062 analysis.changes = values.get(0); // 2126 063 analysis.faces = values.get(1); // 90 064 analysis.data = values.get(2); // 0 065 analysis.air = values.get(3); // 19100 066 analysis.variety = values.get(4); // 266 067 068 analysis.changes_sd = values.get(5); // 2104 069 analysis.faces_sd = values.get(6); // 89 070 analysis.data_sd = values.get(7); // 0 071 analysis.air_sd = values.get(8); // 18909 072 analysis.variety_sd = values.get(9); // 263 073 074 analysis.complexity = settings != null ? analysis.getComplexity(settings) : 0; 075 return analysis; 076 } 077 return null; 078 } 079 080 public static void analyzePlot(Plot plot, RunnableVal<PlotAnalysis> whenDone) { 081 PlotSquared.platform().injector().getInstance(HybridUtils.class).analyzePlot(plot, whenDone); 082 } 083 084 /** 085 * This will set the optimal modifiers for the plot analysis based on the current plot ratings<br> 086 * - Will be used to calibrate the threshold for plot clearing 087 * 088 * @param whenDone task to run when done 089 * @param threshold threshold 090 */ 091 public static void calcOptimalModifiers(final Runnable whenDone, final double threshold) { 092 if (running) { 093 LOGGER.info("Calibration task already in progress!"); 094 return; 095 } 096 if (threshold <= 0 || threshold >= 1) { 097 LOGGER.info("Invalid threshold provided! (Cannot be 0 or 100 as then there's no point in calibrating)"); 098 return; 099 } 100 running = true; 101 final List<Plot> plots = PlotQuery.newQuery().allPlots().asList(); 102 TaskManager.runTaskAsync(new Runnable() { 103 @Override 104 public void run() { 105 Iterator<Plot> iterator = plots.iterator(); 106 LOGGER.info("- Reducing {} plots to those with sufficient data", plots.size()); 107 while (iterator.hasNext()) { 108 Plot plot = iterator.next(); 109 if (plot.getSettings().getRatings() == null || plot.getSettings().getRatings() 110 .isEmpty()) { 111 iterator.remove(); 112 } else { 113 plot.addRunning(); 114 } 115 } 116 117 if (plots.size() < 3) { 118 LOGGER.info("Calibration cancelled due to insufficient comparison data, please try again later"); 119 running = false; 120 for (Plot plot : plots) { 121 plot.removeRunning(); 122 } 123 return; 124 } 125 LOGGER.info("- Analyzing plot contents (this may take a while)"); 126 127 int[] changes = new int[plots.size()]; 128 int[] faces = new int[plots.size()]; 129 int[] data = new int[plots.size()]; 130 int[] air = new int[plots.size()]; 131 int[] variety = new int[plots.size()]; 132 133 int[] changes_sd = new int[plots.size()]; 134 int[] faces_sd = new int[plots.size()]; 135 int[] data_sd = new int[plots.size()]; 136 int[] air_sd = new int[plots.size()]; 137 int[] variety_sd = new int[plots.size()]; 138 139 final int[] ratings = new int[plots.size()]; 140 141 final AtomicInteger mi = new AtomicInteger(0); 142 143 Thread ratingAnalysis = new Thread(() -> { 144 for (; mi.intValue() < plots.size(); mi.incrementAndGet()) { 145 int i = mi.intValue(); 146 Plot plot = plots.get(i); 147 ratings[i] = (int) ( 148 (plot.getAverageRating() + plot.getSettings().getRatings().size()) 149 * 100); 150 LOGGER.info(" | {} (rating) {}", plot, ratings[i]); 151 152 } 153 }); 154 ratingAnalysis.start(); 155 156 ArrayDeque<Plot> plotsQueue = new ArrayDeque<>(plots); 157 while (true) { 158 final Plot queuePlot = plotsQueue.poll(); 159 if (queuePlot == null) { 160 break; 161 } 162 LOGGER.info(" | {}", queuePlot); 163 164 final Object lock = new Object(); 165 TaskManager.runTask(new Runnable() { 166 @Override 167 public void run() { 168 analyzePlot(queuePlot, new RunnableVal<>() { 169 @Override 170 public void run(PlotAnalysis value) { 171 try { 172 synchronized (this) { 173 wait(10000); 174 } 175 } catch (InterruptedException e) { 176 e.printStackTrace(); 177 } 178 synchronized (lock) { 179 queuePlot.removeRunning(); 180 lock.notify(); 181 } 182 } 183 }); 184 } 185 }); 186 try { 187 synchronized (lock) { 188 lock.wait(); 189 } 190 } catch (InterruptedException e) { 191 e.printStackTrace(); 192 } 193 } 194 195 LOGGER.info("- Waiting on plot rating thread: {}%", mi.intValue() * 100 / plots.size()); 196 197 try { 198 ratingAnalysis.join(); 199 } catch (InterruptedException e) { 200 e.printStackTrace(); 201 } 202 203 LOGGER.info("- Processing and grouping single plot analysis for bulk processing"); 204 205 for (int i = 0; i < plots.size(); i++) { 206 Plot plot = plots.get(i); 207 LOGGER.info("| {}", plot); 208 209 PlotAnalysis analysis = plot.getComplexity(null); 210 211 changes[i] = analysis.changes; 212 faces[i] = analysis.faces; 213 data[i] = analysis.data; 214 air[i] = analysis.air; 215 variety[i] = analysis.variety; 216 217 changes_sd[i] = analysis.changes_sd; 218 faces_sd[i] = analysis.faces_sd; 219 data_sd[i] = analysis.data_sd; 220 air_sd[i] = analysis.air_sd; 221 variety_sd[i] = analysis.variety_sd; 222 } 223 224 LOGGER.info("- Calculating rankings"); 225 226 int[] rankRatings = rank(ratings); 227 int n = rankRatings.length; 228 229 int optimalIndex = (int) Math.round((1 - threshold) * (n - 1)); 230 231 LOGGER.info("- Calculating rank correlation: "); 232 LOGGER.info( 233 "- The analyzed plots which were processed and put into bulk data will be compared and correlated to the plot ranking"); 234 LOGGER.info( 235 "- The calculated correlation constant will then be used to calibrate the threshold for auto plot clearing"); 236 237 Settings.Auto_Clear settings = new Settings.Auto_Clear(); 238 239 int[] rankChanges = rank(changes); 240 int[] sdChanges = getSD(rankChanges, rankRatings); 241 int[] varianceChanges = square(sdChanges); 242 int sumChanges = sum(varianceChanges); 243 double factorChanges = getCC(n, sumChanges); 244 settings.CALIBRATION.CHANGES = factorChanges == 1 ? 245 0 : 246 (int) (factorChanges * 1000 / MathMan.getMean(changes)); 247 248 LOGGER.info("- | changes {}", factorChanges); 249 250 int[] rankFaces = rank(faces); 251 int[] sdFaces = getSD(rankFaces, rankRatings); 252 int[] varianceFaces = square(sdFaces); 253 int sumFaces = sum(varianceFaces); 254 double factorFaces = getCC(n, sumFaces); 255 settings.CALIBRATION.FACES = 256 factorFaces == 1 ? 0 : (int) (factorFaces * 1000 / MathMan.getMean(faces)); 257 258 LOGGER.info("- | faces {}", factorFaces); 259 260 int[] rankData = rank(data); 261 int[] sdData = getSD(rankData, rankRatings); 262 int[] variance_data = square(sdData); 263 int sum_data = sum(variance_data); 264 double factor_data = getCC(n, sum_data); 265 settings.CALIBRATION.DATA = 266 factor_data == 1 ? 0 : (int) (factor_data * 1000 / MathMan.getMean(data)); 267 268 LOGGER.info("- | data {}", factor_data); 269 270 int[] rank_air = rank(air); 271 int[] sd_air = getSD(rank_air, rankRatings); 272 int[] variance_air = square(sd_air); 273 int sum_air = sum(variance_air); 274 double factor_air = getCC(n, sum_air); 275 settings.CALIBRATION.AIR = 276 factor_air == 1 ? 0 : (int) (factor_air * 1000 / MathMan.getMean(air)); 277 278 LOGGER.info("- | air {}", factor_air); 279 280 281 int[] rank_variety = rank(variety); 282 int[] sd_variety = getSD(rank_variety, rankRatings); 283 int[] variance_variety = square(sd_variety); 284 int sum_variety = sum(variance_variety); 285 double factor_variety = getCC(n, sum_variety); 286 settings.CALIBRATION.VARIETY = factor_variety == 1 ? 287 0 : 288 (int) (factor_variety * 1000 / MathMan.getMean(variety)); 289 290 LOGGER.info("- | variety {}", factor_variety); 291 292 int[] rank_changes_sd = rank(changes_sd); 293 int[] sd_changes_sd = getSD(rank_changes_sd, rankRatings); 294 int[] variance_changes_sd = square(sd_changes_sd); 295 int sum_changes_sd = sum(variance_changes_sd); 296 double factor_changes_sd = getCC(n, sum_changes_sd); 297 settings.CALIBRATION.CHANGES_SD = factor_changes_sd == 1 ? 298 0 : 299 (int) (factor_changes_sd * 1000 / MathMan.getMean(changes_sd)); 300 301 LOGGER.info("- | changed_sd {}", factor_changes_sd); 302 303 int[] rank_faces_sd = rank(faces_sd); 304 int[] sd_faces_sd = getSD(rank_faces_sd, rankRatings); 305 int[] variance_faces_sd = square(sd_faces_sd); 306 int sum_faces_sd = sum(variance_faces_sd); 307 double factor_faces_sd = getCC(n, sum_faces_sd); 308 settings.CALIBRATION.FACES_SD = factor_faces_sd == 1 ? 309 0 : 310 (int) (factor_faces_sd * 1000 / MathMan.getMean(faces_sd)); 311 312 LOGGER.info("- | faced_sd {}", factor_faces_sd); 313 314 int[] rank_data_sd = rank(data_sd); 315 int[] sd_data_sd = getSD(rank_data_sd, rankRatings); 316 int[] variance_data_sd = square(sd_data_sd); 317 int sum_data_sd = sum(variance_data_sd); 318 double factor_data_sd = getCC(n, sum_data_sd); 319 settings.CALIBRATION.DATA_SD = factor_data_sd == 1 ? 320 0 : 321 (int) (factor_data_sd * 1000 / MathMan.getMean(data_sd)); 322 323 LOGGER.info("- | data_sd {}", factor_data_sd); 324 325 int[] rank_air_sd = rank(air_sd); 326 int[] sd_air_sd = getSD(rank_air_sd, rankRatings); 327 int[] variance_air_sd = square(sd_air_sd); 328 int sum_air_sd = sum(variance_air_sd); 329 double factor_air_sd = getCC(n, sum_air_sd); 330 settings.CALIBRATION.AIR_SD = 331 factor_air_sd == 1 ? 0 : (int) (factor_air_sd * 1000 / MathMan.getMean(air_sd)); 332 333 LOGGER.info("- | air_sd {}", factor_air_sd); 334 335 int[] rank_variety_sd = rank(variety_sd); 336 int[] sd_variety_sd = getSD(rank_variety_sd, rankRatings); 337 int[] variance_variety_sd = square(sd_variety_sd); 338 int sum_variety_sd = sum(variance_variety_sd); 339 double factor_variety_sd = getCC(n, sum_variety_sd); 340 settings.CALIBRATION.VARIETY_SD = factor_variety_sd == 1 ? 341 0 : 342 (int) (factor_variety_sd * 1000 / MathMan.getMean(variety_sd)); 343 344 LOGGER.info("- | variety_sd {}", factor_variety_sd); 345 346 int[] complexity = new int[n]; 347 348 LOGGER.info("Calculating threshold"); 349 350 int max = 0; 351 int min = 0; 352 for (int i = 0; i < n; i++) { 353 Plot plot = plots.get(i); 354 PlotAnalysis analysis = plot.getComplexity(settings); 355 complexity[i] = analysis.complexity; 356 if (analysis.complexity < min) { 357 min = analysis.complexity; 358 } else if (analysis.complexity > max) { 359 max = analysis.complexity; 360 } 361 } 362 int optimalComplexity = Integer.MAX_VALUE; 363 if (min > 0 && max < 102400) { // If low size, use my fast ranking algorithm 364 int[] rankComplexity = rank(complexity, max + 1); 365 for (int i = 0; i < n; i++) { 366 if (rankComplexity[i] == optimalIndex) { 367 optimalComplexity = complexity[i]; 368 break; 369 } 370 } 371 logln("Complexity: "); 372 logln(rankComplexity); 373 logln("Ratings: "); 374 logln(rankRatings); 375 logln("Correlation: "); 376 logln(getCC(n, sum(square(getSD(rankComplexity, rankRatings))))); 377 if (optimalComplexity == Integer.MAX_VALUE) { 378 LOGGER.info("Insufficient data to determine correlation! {} | {}", 379 optimalIndex, n 380 ); 381 running = false; 382 for (Plot plot : plots) { 383 plot.removeRunning(); 384 } 385 return; 386 } 387 } else { // Use the fast radix sort algorithm 388 int[] sorted = complexity.clone(); 389 sort(sorted); 390 logln("Complexity: "); 391 logln(complexity); 392 logln("Ratings: "); 393 logln(rankRatings); 394 } 395 396 // Save calibration 397 LOGGER.info("Saving calibration"); 398 Settings.AUTO_CLEAR.put("auto-calibrated", settings); 399 Settings.save(PlotSquared.get().getWorldsFile()); 400 running = false; 401 for (Plot plot : plots) { 402 plot.removeRunning(); 403 } 404 LOGGER.info("Done!"); 405 whenDone.run(); 406 } 407 }); 408 } 409 410 public static void logln(Object obj) { 411 LOGGER.info("" + log(obj)); 412 } 413 414 public static String log(Object obj) { 415 StringBuilder result = new StringBuilder(); 416 if (obj.getClass().isArray()) { 417 String prefix = ""; 418 419 for (int i = 0; i < Array.getLength(obj); i++) { 420 result.append(prefix).append(log(Array.get(obj, i))); 421 prefix = ","; 422 } 423 return "( " + result + " )"; 424 } else if (obj instanceof List<?>) { 425 String prefix = ""; 426 for (Object element : (List<?>) obj) { 427 result.append(prefix).append(log(element)); 428 prefix = ","; 429 } 430 return "[ " + result + " ]"; 431 } else { 432 return obj.toString(); 433 } 434 } 435 436 /** 437 * Get correlation coefficient. 438 * 439 * @param n n 440 * @param sum sum 441 * @return result 442 */ 443 public static double getCC(int n, int sum) { 444 return 1 - 6 * (double) sum / (n * (n * n - 1)); 445 } 446 447 /** 448 * Calls {@code Arrays.stream(array).sum()} 449 * 450 * @param array array 451 * @return sum 452 */ 453 public static int sum(int[] array) { 454 return Arrays.stream(array).sum(); 455 } 456 457 /** 458 * A simple array squaring algorithm. 459 * - Used for calculating the variance 460 * 461 * @param array array 462 * @return result 463 */ 464 public static int[] square(int[] array) { 465 array = array.clone(); 466 for (int i = 0; i < array.length; i++) { 467 array[i] *= array[i]; 468 } 469 return array; 470 } 471 472 /** 473 * An optimized lossy standard deviation algorithm. 474 * 475 * @param ranks ranks 476 * @return result 477 */ 478 public static int[] getSD(int[]... ranks) { 479 if (ranks.length == 0) { 480 return null; 481 } 482 int[] result = new int[ranks[0].length]; 483 for (int j = 0; j < ranks[0].length; j++) { 484 int sum = 0; 485 for (int[] rank : ranks) { 486 sum += rank[j]; 487 } 488 int mean = sum / ranks.length; 489 int sd = 0; 490 for (int[] rank : ranks) { 491 int value = rank[j]; 492 sd += value < mean ? mean - value : value - mean; 493 } 494 result[j] = sd; 495 } 496 return result; 497 } 498 499 /** 500 * An optimized algorithm for ranking a very specific set of inputs<br> 501 * - Input is an array of int with a max size of 102400<br> 502 * - A reduced sample space allows for sorting (and ranking in this case) in linear time 503 * 504 * @param input input 505 * @return result 506 */ 507 public static int[] rank(int[] input) { 508 return rank(input, 102400); 509 } 510 511 /** 512 * An optimized algorithm for ranking a very specific set of inputs 513 * 514 * @param input input 515 * @param size size 516 * @return result 517 */ 518 public static int[] rank(int[] input, int size) { 519 int[] cache = new int[size]; 520 int max = 0; 521 if (input.length < size) { 522 for (int value : input) { 523 if (value > max) { 524 max = value; 525 } 526 cache[value]++; 527 } 528 } else { 529 max = cache.length - 1; 530 for (int value : input) { 531 cache[value]++; 532 } 533 } 534 int last = 0; 535 for (int i = max; i >= 0; i--) { 536 if (cache[i] != 0) { 537 cache[i] += last; 538 last = cache[i]; 539 if (last == input.length) { 540 break; 541 } 542 } 543 } 544 545 int[] ranks = new int[input.length]; 546 for (int i = 0; i < input.length; i++) { 547 int index = input[i]; 548 ranks[i] = cache[index]; 549 cache[index]--; 550 } 551 return ranks; 552 } 553 554 @SuppressWarnings("unchecked") 555 public static void sort(int[] input) { 556 int SIZE = 10; 557 List<Integer>[] bucket = new ArrayList[SIZE]; 558 for (int i = 0; i < bucket.length; i++) { 559 bucket[i] = new ArrayList<>(); 560 } 561 boolean maxLength = false; 562 int placement = 1; 563 while (!maxLength) { 564 maxLength = true; 565 for (Integer i : input) { 566 int tmp = i / placement; 567 bucket[tmp % SIZE].add(i); 568 if (maxLength && tmp > 0) { 569 maxLength = false; 570 } 571 } 572 int a = 0; 573 for (int b = 0; b < SIZE; b++) { 574 for (Integer i : bucket[b]) { 575 input[a++] = i; 576 } 577 bucket[b].clear(); 578 } 579 placement *= SIZE; 580 } 581 } 582 583 public List<Integer> asList() { 584 return Arrays 585 .asList(this.changes, this.faces, this.data, this.air, this.variety, this.changes_sd, 586 this.faces_sd, this.data_sd, this.air_sd, this.variety_sd 587 ); 588 } 589 590 public int getComplexity(Settings.Auto_Clear settings) { 591 Settings.Auto_Clear.CALIBRATION modifiers = settings.CALIBRATION; 592 if (this.complexity != 0) { 593 return this.complexity; 594 } 595 this.complexity = this.changes * modifiers.CHANGES + this.faces * modifiers.FACES 596 + this.data * modifiers.DATA + this.air * modifiers.AIR 597 + this.variety * modifiers.VARIETY + this.changes_sd * modifiers.CHANGES_SD 598 + this.faces_sd * modifiers.FACES_SD + this.data_sd * modifiers.DATA_SD 599 + this.air_sd * modifiers.AIR_SD + this.variety_sd * modifiers.VARIETY_SD; 600 return this.complexity; 601 } 602 603}