/* * This file is part of Player Analytics (Plan). * * Plan is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License v3 as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Plan is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Plan. If not, see . */ package com.djrapitops.plan.delivery.domain.mutators; import com.djrapitops.plan.delivery.domain.DateObj; import com.djrapitops.plan.delivery.domain.container.DataContainer; import com.djrapitops.plan.delivery.domain.container.PlayerContainer; import com.djrapitops.plan.delivery.domain.keys.PlayerKeys; import com.djrapitops.plan.delivery.domain.keys.ServerKeys; import com.djrapitops.plan.delivery.domain.keys.SessionKeys; import com.djrapitops.plan.gathering.domain.GeoInfo; import com.djrapitops.plan.gathering.domain.Ping; import com.djrapitops.plan.gathering.domain.Session; import com.djrapitops.plugin.api.TimeAmount; import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; /** * Mutator for a bunch of {@link PlayerContainer}s. * * @author Rsl1122 */ public class PlayersMutator { private final List players; public PlayersMutator(List players) { this.players = players; } public static PlayersMutator copyOf(PlayersMutator mutator) { return new PlayersMutator(new ArrayList<>(mutator.players)); } public static PlayersMutator forContainer(DataContainer container) { return new PlayersMutator(container.getValue(ServerKeys.PLAYERS).orElse(new ArrayList<>())); } public > PlayersMutator filterBy(T by) { return new PlayersMutator(players.stream().filter(by).collect(Collectors.toList())); } public PlayersMutator filterPlayedBetween(long after, long before) { return filterBy( player -> player.getValue(PlayerKeys.SESSIONS) .map(sessions -> sessions.stream().anyMatch(session -> { long start = session.getValue(SessionKeys.START).orElse(-1L); long end = session.getValue(SessionKeys.END).orElse(-1L); return (after <= start && start <= before) || (after <= end && end <= before); })).orElse(false) ); } public PlayersMutator filterRegisteredBetween(long after, long before) { return filterBy( player -> player.getValue(PlayerKeys.REGISTERED) .map(date -> after <= date && date <= before).orElse(false) ); } public PlayersMutator filterRetained(long after, long before) { return filterBy( player -> { long backLimit = Math.max(after, player.getValue(PlayerKeys.REGISTERED).orElse(0L)); long half = backLimit + ((before - backLimit) / 2L); SessionsMutator sessionsMutator = SessionsMutator.forContainer(player); return sessionsMutator.playedBetween(backLimit, half) && sessionsMutator.playedBetween(half, before); } ); } public PlayersMutator filterActive(long date, long msThreshold, double limit) { return filterBy(player -> player.getActivityIndex(date, msThreshold).getValue() >= limit); } public PlayersMutator filterPlayedOnServer(UUID serverUUID) { return filterBy(player -> !SessionsMutator.forContainer(player) .filterPlayedOnServer(serverUUID) .all().isEmpty() ); } public List all() { return players; } public List registerDates() { List registerDates = new ArrayList<>(); for (PlayerContainer player : players) { registerDates.add(player.getValue(PlayerKeys.REGISTERED).orElse(-1L)); } return registerDates; } public List getGeolocations() { List geolocations = new ArrayList<>(); for (PlayerContainer player : players) { Optional mostRecent = GeoInfoMutator.forContainer(player).mostRecent(); geolocations.add(mostRecent.map(GeoInfo::getGeolocation).orElse("Unknown")); } return geolocations; } public Map> getPingPerCountry(UUID serverUUID) { Map> pingPerCountry = new HashMap<>(); for (PlayerContainer player : players) { Optional mostRecent = GeoInfoMutator.forContainer(player).mostRecent(); if (!mostRecent.isPresent()) { continue; } List pings = player.getValue(PlayerKeys.PING).orElse(new ArrayList<>()); String country = mostRecent.get().getGeolocation(); List countryPings = pingPerCountry.getOrDefault(country, new ArrayList<>()); pings.stream() .filter(ping -> ping.getServerUUID().equals(serverUUID)) .forEach(countryPings::add); pingPerCountry.put(country, countryPings); } return pingPerCountry; } public TreeMap>> toActivityDataMap(long date, long msThreshold) { TreeMap>> activityData = new TreeMap<>(); for (long time = date; time >= date - TimeAmount.MONTH.toMillis(2L); time -= TimeAmount.WEEK.toMillis(1L)) { Map> map = activityData.getOrDefault(time, new HashMap<>()); if (!players.isEmpty()) { for (PlayerContainer player : players) { if (player.getValue(PlayerKeys.REGISTERED).orElse(0L) > time) { continue; } ActivityIndex activityIndex = player.getActivityIndex(time, msThreshold); String activityGroup = activityIndex.getGroup(); Set uuids = map.getOrDefault(activityGroup, new HashSet<>()); uuids.add(player.getUnsafe(PlayerKeys.UUID)); map.put(activityGroup, uuids); } } activityData.put(time, map); } return activityData; } public int count() { return players.size(); } public int averageNewPerDay(TimeZone timeZone) { return MutatorFunctions.average(newPerDay(timeZone)); } public TreeMap newPerDay(TimeZone timeZone) { List registerDates = registerDates().stream() .map(value -> new DateObj<>(value, value)) .collect(Collectors.toList()); // Adds timezone offset SortedMap> byDay = new DateHoldersMutator<>(registerDates).groupByStartOfDay(timeZone); TreeMap byDayCounts = new TreeMap<>(); for (Map.Entry> entry : byDay.entrySet()) { byDayCounts.put( entry.getKey(), entry.getValue().size() ); } return byDayCounts; } /** * Compares players in the mutator to other players in terms of player retention. * * @param compareTo Players to compare to. * @param dateLimit Epoch ms back limit, if the player registered after this their value is not used. * @return Mutator containing the players that are considered to be retained. * @throws IllegalStateException If all players are rejected due to dateLimit. */ public PlayersMutator compareAndFindThoseLikelyToBeRetained( Iterable compareTo, long dateLimit, PlayersOnlineResolver onlineResolver, long activityMsThreshold ) { Collection retainedAfterMonth = new ArrayList<>(); Collection notRetainedAfterMonth = new ArrayList<>(); for (PlayerContainer player : players) { long registered = player.getValue(PlayerKeys.REGISTERED).orElse(System.currentTimeMillis()); // Discard uncertain data if (registered > dateLimit) { continue; } long monthAfterRegister = registered + TimeAmount.MONTH.toMillis(1L); long half = registered + (TimeAmount.MONTH.toMillis(1L) / 2L); if (player.playedBetween(registered, half) && player.playedBetween(half, monthAfterRegister)) { retainedAfterMonth.add(player); } else { notRetainedAfterMonth.add(player); } } if (retainedAfterMonth.isEmpty() || notRetainedAfterMonth.isEmpty()) { throw new IllegalStateException("No players to compare to after rejecting with dateLimit"); } List retained = retainedAfterMonth.stream() .map(player -> new RetentionData(player, onlineResolver, activityMsThreshold)) .collect(Collectors.toList()); List notRetained = notRetainedAfterMonth.stream() .map(player -> new RetentionData(player, onlineResolver, activityMsThreshold)) .collect(Collectors.toList()); RetentionData avgRetained = RetentionData.average(retained); RetentionData avgNotRetained = RetentionData.average(notRetained); List toBeRetained = new ArrayList<>(); for (PlayerContainer player : compareTo) { RetentionData retentionData = new RetentionData(player, onlineResolver, activityMsThreshold); if (retentionData.distance(avgRetained) < retentionData.distance(avgNotRetained)) { toBeRetained.add(player); } } return new PlayersMutator(toBeRetained); } public List getSessions() { return players.stream() .map(player -> player.getValue(PlayerKeys.SESSIONS).orElse(new ArrayList<>())) .flatMap(Collection::stream) .collect(Collectors.toList()); } public List uuids() { return players.stream() .map(player -> player.getValue(PlayerKeys.UUID).orElse(null)) .filter(Objects::nonNull) .collect(Collectors.toList()); } public List operators() { return players.stream() .filter(player -> player.getValue(PlayerKeys.OPERATOR).orElse(false)).collect(Collectors.toList()); } public List pings() { return players.stream() .map(player -> player.getValue(PlayerKeys.PING).orElse(new ArrayList<>())) .flatMap(Collection::stream) .collect(Collectors.toList()); } }