diff --git a/.gitignore b/.gitignore index b3f5a691..a5876b08 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ docker-compose.yml .DS_Store settings/.DS_Store target/ -backup/ \ No newline at end of file +backup/ +*.json diff --git a/src/io/github/sammers/pla/blizzard/Cutoffs.java b/src/io/github/sammers/pla/blizzard/Cutoffs.java index e256a7f9..4b8f2e2b 100644 --- a/src/io/github/sammers/pla/blizzard/Cutoffs.java +++ b/src/io/github/sammers/pla/blizzard/Cutoffs.java @@ -15,22 +15,40 @@ public class Cutoffs implements JsonConvertable { public final String region; public final String season; public final Map cutoffs; - private final Map spotsCounts = new HashMap<>(); + public final Long timestamp; + public final Map spotsCounts = new HashMap<>(); + public final Map spotWithNoAlts = new HashMap<>(); - public Cutoffs(String region, - String season, Map cutoffs, - Long timestamp) { + public Cutoffs(String region, String season, Map cutoffs, Long timestamp) { this.region = region; this.season = season; this.cutoffs = cutoffs; + this.timestamp = timestamp; + } + + public Cutoffs( + String region, + String season, + Map cutoffs, + Map spotsCounts, + Map spotWithNoAlts, + Long timestamp) { + this(region, season, cutoffs, timestamp); + this.spotsCounts.putAll(spotsCounts); + this.spotWithNoAlts.putAll(spotWithNoAlts); + } + + public void setSpotWithNoAlts(String bracket, long count) { + spotWithNoAlts.put(bracket, (long)count); } public void setSpotCount(String bracket, int count) { - spotsCounts.put(bracket, count); + spotsCounts.put(bracket, (long)count); } public int spotCount(String bracket) { - return spotsCounts.getOrDefault(bracket, 0); + Long res = spotsCounts.getOrDefault(bracket, 0L); + return res.intValue(); } public static Cutoffs fromBlizzardJson(String region, JsonObject entries) { @@ -124,7 +142,21 @@ public JsonObject toJson() { return new JsonObject() .put("region", region) .put("season", season) - .put("rewards", new JsonObject(cutoffs.entrySet().stream().map(x -> Map.entry(x.getKey(), x.getValue())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + .put("timestamp", timestamp) + .put("rewards", new JsonObject(cutoffs.entrySet().stream().map(x -> Map.entry(x.getKey(), x.getValue())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))) + .put("spotCounts", new JsonObject(spotsCounts.entrySet().stream().map(x -> Map.entry(x.getKey(), x.getValue())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))) + .put("spotWithNoAlts", new JsonObject(spotWithNoAlts.entrySet().stream().map(x -> Map.entry(x.getKey(), x.getValue())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); + } + + public static Cutoffs fromJson(JsonObject json) { + return new Cutoffs( + json.getString("region"), + json.getString("season"), + json.getJsonObject("rewards").stream().collect(Collectors.toMap(Map.Entry::getKey, x -> (Long) x.getValue())), + json.getJsonObject("spotCounts").stream().collect(Collectors.toMap(Map.Entry::getKey, x -> (Long) x.getValue())), + json.getJsonObject("spotWithNoAlts").stream().collect(Collectors.toMap(Map.Entry::getKey, x -> (Long) x.getValue())), + json.getLong("timestamp") + ); } public Long cutoffByBracketType(String btype) { @@ -137,4 +169,15 @@ public Long cutoffByBracketType(String btype) { } return cutoffs.get(btype); } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Cutoffs) { + Cutoffs other = (Cutoffs) obj; + return region.equals(other.region) + && season.equals(other.season) + && cutoffs.equals(other.cutoffs); + } + return false; + } } diff --git a/src/io/github/sammers/pla/db/DB.java b/src/io/github/sammers/pla/db/DB.java index 4a96b1e3..f9deb28a 100644 --- a/src/io/github/sammers/pla/db/DB.java +++ b/src/io/github/sammers/pla/db/DB.java @@ -1,6 +1,7 @@ package io.github.sammers.pla.db; import io.github.sammers.pla.Main; +import io.github.sammers.pla.blizzard.Cutoffs; import io.github.sammers.pla.blizzard.Realm; import io.github.sammers.pla.blizzard.Realms; import io.github.sammers.pla.blizzard.WowAPICharacter; @@ -189,6 +190,35 @@ public Completable insertRealms(Realms realms) { ).ignoreElement(); } + public Completable insertCutoffsIfDifferent(Cutoffs cutoffs) { + return getLastCutoffs(cutoffs.region).flatMapCompletable(last -> { + if (last.isEmpty() || !last.get().equals(cutoffs)) { + return mongoClient.rxSave("cutoffs", cutoffs.toJson()) + .doOnSuccess(ok -> log.info("Inserted cutoffs for region={} season={}", cutoffs.region, cutoffs.season)) + .ignoreElement(); + } else { + log.info("Cutoffs for region={} season={} are the same, skipping", cutoffs.region, cutoffs.season); + return Completable.complete(); + } + }); + } + + public Single> getLastCutoffs(String region) { + FindOptions fopts = new FindOptions() + .setSort(new JsonObject().put("timestamp", -1)) + .setLimit(1); + JsonObject opts = new JsonObject() + .put("region", new JsonObject().put("$eq", region)); + return mongoClient.rxFindWithOptions("cutoffs", opts, fopts).flatMap(res -> { + List cutoffs = res.stream().map(Cutoffs::fromJson).toList(); + if (!cutoffs.isEmpty()) { + return Single.just(Optional.of(cutoffs.getFirst())); + } else { + return Single.just(Optional.empty()); + } + }); + } + public Single loadRealms() { return Single.defer(() -> { long tick = System.nanoTime(); diff --git a/src/io/github/sammers/pla/db/Snapshot.java b/src/io/github/sammers/pla/db/Snapshot.java index 98db9a97..aae65735 100644 --- a/src/io/github/sammers/pla/db/Snapshot.java +++ b/src/io/github/sammers/pla/db/Snapshot.java @@ -7,8 +7,6 @@ import io.github.sammers.pla.http.JsonConvertable; import io.github.sammers.pla.http.Resp; import io.github.sammers.pla.logic.Calculator; -import io.github.sammers.pla.logic.CharAndDiff; -import io.github.sammers.pla.logic.SnapshotDiff; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -98,7 +96,16 @@ public Snapshot applyCutoffs(String bracket, Cutoffs cutoffs) { return ch; } }).collect(Collectors.toList()); + List charsInCutoff = chars.stream().filter(Character::inCutoff).toList(); cutoffs.setSpotCount("ARENA_3v3", charsWithCutoff.intValue()); + Map> petHashes = charsInCutoff.stream() + .collect(Collectors.groupingBy(ch -> { + if (ch.pethash().isEmpty() || ch.pethash().get() == -1) { + return ch.fullNameWSpec().hashCode(); + } + return ch.pethash().get(); + })); + cutoffs.setSpotWithNoAlts("ARENA_3v3", petHashes.size()); return new Snapshot(chars, this.timestamp(), this.region(), this.dateTime()); } else if (bracket.equals(RBG)) { Long cutoff = cutoffs.battlegrounds("ALLIANCE"); @@ -111,11 +118,22 @@ public Snapshot applyCutoffs(String bracket, Cutoffs cutoffs) { return ch; } }).collect(Collectors.toList()); + Map> petHashes = chars.stream() + .filter(Character::inCutoff) + .collect(Collectors.groupingBy(ch -> { + if (ch.pethash().isEmpty() || ch.pethash().get() == -1) { + return ch.fullNameWSpec().hashCode(); + } + return ch.pethash().get(); + })); cutoffs.setSpotCount("BATTLEGROUNDS/alliance", charsWithCutoff.intValue()); cutoffs.setSpotCount("BATTLEGROUNDS/horde", charsWithCutoff.intValue()); + cutoffs.setSpotWithNoAlts("BATTLEGROUNDS/alliance", petHashes.size()); + cutoffs.setSpotWithNoAlts("BATTLEGROUNDS/horde", petHashes.size()); return new Snapshot(chars, this.timestamp(), this.region(), this.dateTime()); } else if (bracket.equals(SHUFFLE)) { Map specCodeAndSpotCount = new HashMap<>(); + Map>> petHashes = new HashMap<>(); List chars = this.characters().stream().map(ch -> { String fullSpec = ch.fullSpec(); String specCode = Cutoffs.specCodeByFullName(fullSpec); @@ -125,13 +143,30 @@ public Snapshot applyCutoffs(String bracket, Cutoffs cutoffs) { } if (ch.rating() >= cutoff) { specCodeAndSpotCount.compute(specCode, (k, v) -> v == null ? 1 : v + 1); + petHashes.compute(specCode, (k, v) -> { + if (v == null) { + v = new HashMap<>(); + } + Integer petHash = ch.pethash().map(p -> p == -1 ? ch.fullNameWSpec().hashCode() : p).orElse(ch.fullNameWSpec().hashCode()); + v.compute(petHash, (k1, v1) -> { + if (v1 == null) { + v1 = new ArrayList<>(); + } + v1.add(ch); + return v1; + }); + return v; + }); return ch.changeCutoff(true); } else { return ch; } }).collect(Collectors.toList()); specCodeAndSpotCount.entrySet() - .forEach(cutoff -> cutoffs.setSpotCount("SHUFFLE/" + cutoff.getKey(), cutoff.getValue().intValue())); + .forEach((var cutoff) -> { + cutoffs.setSpotWithNoAlts("SHUFFLE/" + cutoff.getKey(), petHashes.get(cutoff.getKey()).size()); + cutoffs.setSpotCount("SHUFFLE/" + cutoff.getKey(), cutoff.getValue().intValue()); + }); return new Snapshot(chars, this.timestamp(), this.region(), this.dateTime()); } return this; diff --git a/src/io/github/sammers/pla/logic/Ladder.java b/src/io/github/sammers/pla/logic/Ladder.java index c592acb2..eb1b3ad9 100644 --- a/src/io/github/sammers/pla/logic/Ladder.java +++ b/src/io/github/sammers/pla/logic/Ladder.java @@ -62,6 +62,7 @@ public Ladder(WebClient web, DB db, BlizzardAPI blizzardAPI, CharacterCache char this.charUpdater = new CharUpdater(blizzardAPI, characterCache, charsLoaded, refs, charSearchIndex, db); } + @SuppressWarnings("unchecked") public void start() { boolean updatesEnabled = true; int euPeriod = 5; @@ -78,7 +79,12 @@ public void start() { } else { updates = Observable.never(); } - loadRealms().andThen(loadRegionData(EU)).andThen(loadRegionData(US)).andThen(charsAreLoaded()).andThen(updates).doOnError(e -> log.error("Error fetching ladder", e)).onErrorReturnItem(0L).subscribe(); + loadRealms() + .andThen(loadRegionData(EU)) + .andThen(loadRegionData(US)) + .andThen(charsAreLoaded()) + .andThen(updates).doOnError(e -> log.error("Error fetching ladder", e)) + .onErrorReturnItem(0L).subscribe(); Observable.interval(24, 24, HOURS).flatMapCompletable(tick -> { log.info("Updating realms"); return updateRealms(EU).andThen(updateRealms(US)); @@ -103,6 +109,7 @@ private Observable runDataUpdater(String region, int timeout, TimeUnit tim .andThen(twoVTwo(region).ignoreElement()) .andThen(battlegrounds(region).ignoreElement()) .andThen(shuffle(region).ignoreElement()) + .andThen(updateCutoffs(region)) .andThen(calculateMulticlasserLeaderboard(region)) .andThen(calculateMeta(region)) .andThen(charUpdater.updateCharacters(region, 1, DAYS, timeout, timeoutUnits)).onErrorComplete(e -> { @@ -146,6 +153,7 @@ public Completable loadRegionData(String region) { .andThen(loadLast(THREE_V_THREE, region)) .andThen(loadLast(RBG, region)) .andThen(loadLast(SHUFFLE, region)) + .andThen(updateCutoffs(region)) .andThen(calculateMeta(region)) .andThen(loadWowCharApiData(region)) .andThen(calculateMulticlasserLeaderboard(region)); @@ -490,10 +498,25 @@ private Completable loadCutoffs(String region) { return Completable.defer(() -> { log.info("Load cutoffs for region " + region); return blizzardAPI.cutoffs(region).map(cutoffs -> { - regionCutoff.put(oldRegion(region), cutoffs); - regionCutoff.put(realRegion(region), cutoffs); - return cutoffs; - }).doAfterSuccess(cutoffs -> log.info("Cutoffs for region={} has been loaded", region)).ignoreElement().onErrorComplete(); + regionCutoff.put(oldRegion(region), cutoffs); + regionCutoff.put(realRegion(region), cutoffs); + return cutoffs; + }).doAfterSuccess(cutoffs -> log.info("Cutoffs for region={} has been loaded", region)) + .ignoreElement() + .onErrorComplete(); + }); + } + + private Completable updateCutoffs(String region) { + return Completable.defer(() -> { + Cutoffs cutoffs = regionCutoff.get(region); + if (cutoffs == null) { + log.info("No cutoffs to update in DB"); + return Completable.complete(); + } else { + log.info("Updating cutoffs in DB"); + return db.insertCutoffsIfDifferent(cutoffs); + } }); }