/*
* 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());
}
}