Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The Plugin has some exclusive features, making its use much easier than the CLI.

* Automatic updates at regular intervals.
* Automatic detection of webservers run by other plugins ([dynmap](https://github.com/webbukkit/dynmap)).
* Support for offline-mode skins via [SkinsRestorer](https://skinsrestorer.net/) (v14.2.2 or later).
* Support for offline-mode skins via [SkinsRestorer](https://skinsrestorer.net/) (v15.0.0 or later).

## Setup

Expand Down Expand Up @@ -282,6 +282,14 @@ Only since the Java implementation, *MinecraftStats* officially has numbered ver

## Changelog

### 3.31

This update uses a newer API of SkinsRestorer and fixes more MOTD issues.

* Updated the [SkinsRestorer](https://skinsrestorer.net/) API – version 15.0.0 or later is now required. Because of breaking changes in their API, older versions of SkinsRestorer are no longer supported.
* Fixed winners of finished events not displayed correctly.
* Fixed newlines being incorrectly replaced in the MOTD, causing the summary to become unparseable.

### 3.3.0

This update adds automatic detection of squremap's webserver and fixes bugs in both the plugin and CLI.
Expand All @@ -307,7 +315,7 @@ This update adds automatic detection of BlueMap's webserver and fixes bugs in th

This update adds SkinsRestorer for the plugin and avoids unnecessary Mojang API calls.

* The plugin can now get skins from [SkinsRestorer](https://skinsrestorer.net/) v14.2.2 or later.
* The plugin can now get skins from [SkinsRestorer](https://skinsrestorer.net/) ~~14.2.2~~ 15.0.0 or later.
* If a player is detected to be an offline player (e.g., Floodgate players or if the Mojang API gave an empty response), no further attempts at asking the Mojang API will be made.
* Minimized the log output of the plugin.

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ repositories {
dependencies {
implementation 'org.json:json:20220924'
compileOnly 'org.spigotmc:spigot-api:1.13.2-R0.1-SNAPSHOT'
compileOnly 'net.skinsrestorer:skinsrestorer-api:14.2.10'
compileOnly 'net.skinsrestorer:skinsrestorer-api:15.0.0'
}

task concatJs {
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/de/pdinklag/mcstats/EventWinner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package de.pdinklag.mcstats;

import java.util.function.Function;

import org.json.JSONArray;
import org.json.JSONObject;

/**
* Contains summarizing information about the winner of an Event.
*/
public class EventWinner {
/**
* Restores an event winner from a ranking in JSON format.
*
* @param ranking the ranking in JSON format
* @param uuidToPlayer the function that translates a player's UUID to the corresponding Player object
* @return the parsed event winner
*/
public static EventWinner fromJsonRanking(JSONArray ranking, Function<String, Player> uuidToPlayer) {
if (ranking.length() > 0) {
final JSONObject first = ranking.getJSONObject(0);
return new EventWinner(uuidToPlayer.apply(first.getString("uuid")), first);
} else {
return null;
}
}

private final Player player;
private final JSONObject json;

private EventWinner(Player player, JSONObject json) {
this.player = player;
this.json = json;
}

public EventWinner(Ranking<?>.Entry e) {
this.player = e.getPlayer();
this.json = e.toJSON();
}

public Player getPlayer() {
return player;
}

public JSONObject getJSON() {
return json;
}
}
50 changes: 35 additions & 15 deletions src/main/java/de/pdinklag/mcstats/Updater.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public abstract class Updater {
private static final String DATABASE_PLAYERLIST_ACTIVE_FORMAT = "active%d.json.gz";
private static final String DATABASE_SUMMARY = "summary.json.gz";

private static final String EVENT_INITIAL_SCORE_FIELD = "initialRanking";
private static final String EVENT_INITIAL_RANKING_FIELD = "initialRanking";
private static final String EVENT_RANKING_FIELD = "ranking";

private static final int MINUTES_TO_TICKS = 60 * MinecraftServerUtils.TICKS_PER_SECOND;

Expand Down Expand Up @@ -441,13 +442,31 @@ public void run() {
});

// process events
HashMap<Event, Ranking<IntValue>.Entry> eventWinners = new HashMap<>();
HashMap<Event, EventWinner> eventWinners = new HashMap<>();
events.values().forEach(event -> {
if (!event.hasEnded(now)) {
final Path eventDataPath = dbEventsPath.resolve(event.getId() + JSON_FILE_EXT);
if (event.hasEnded(now)) {
// event has ended, get winner from JSON and store it into summary
if (Files.exists(eventDataPath)) {
try {
final JSONArray eventRanking = new JSONObject(Files.readString(eventDataPath))
.getJSONArray(EVENT_RANKING_FIELD);
final EventWinner winner = EventWinner.fromJsonRanking(eventRanking, uuid -> {
return allPlayers.get(uuid);
});
if (winner != null) {
eventWinners.put(event, winner);
}
} catch (Exception e) {
log.writeError("failed to load winner for ended event " + event.getId(), e);
}
} else {
log.writeLine("event has ended, but data file does not exist: " + event.getId());
}
} else {
// event has not yet ended, update ranking
final Stat linkedStat = awards.get(event.getLinkedStatId());
if (linkedStat != null) {
final Path eventDataPath = dbEventsPath.resolve(event.getId() + JSON_FILE_EXT);

final JSONObject eventData = new JSONObject();
eventData.put("name", event.getId());
eventData.put("title", event.getTitle());
Expand All @@ -462,29 +481,29 @@ public void run() {
if (Files.exists(eventDataPath)) {
try {
final JSONObject initialRanking = new JSONObject(Files.readString(eventDataPath))
.getJSONObject(EVENT_INITIAL_SCORE_FIELD);
.getJSONObject(EVENT_INITIAL_RANKING_FIELD);
event.setInitialScores(initialRanking);
eventData.put(EVENT_INITIAL_SCORE_FIELD, initialRanking);
eventData.put(EVENT_INITIAL_RANKING_FIELD, initialRanking);
} catch (Exception e) {
log.writeError("failed to load initial scores for event " + event.getId(), e);
eventData.put(EVENT_INITIAL_SCORE_FIELD, new JSONObject());
eventData.put(EVENT_INITIAL_RANKING_FIELD, new JSONObject());
}
} else {
log.writeLine("event is already running, but no initial scores are available: "
+ event.getId());
eventData.put(EVENT_INITIAL_SCORE_FIELD, new JSONObject());
eventData.put(EVENT_INITIAL_RANKING_FIELD, new JSONObject());
}

final Ranking<IntValue> eventRanking = new Ranking<IntValue>(validPlayers, player -> {
return new IntValue(
player.getStats().get(linkedStat).toInt() - event.getInitialScore(player));
});
eventData.put("ranking", eventRanking.toJSON());
eventData.put(EVENT_RANKING_FIELD, eventRanking.toJSON());

// store best for front page
List<Ranking<IntValue>.Entry> rankingEntries = eventRanking.getOrderedEntries();
if (rankingEntries.size() > 0) {
eventWinners.put(event, rankingEntries.get(0));
eventWinners.put(event, new EventWinner(rankingEntries.get(0)));
}
} else {
// the event has not yet started, update initial scores
Expand All @@ -496,7 +515,7 @@ public void run() {
if (score > 0)
initialScores.put(uuid, score);
});
eventData.put(EVENT_INITIAL_SCORE_FIELD, initialScores);
eventData.put(EVENT_INITIAL_RANKING_FIELD, initialScores);
}

try {
Expand Down Expand Up @@ -574,8 +593,9 @@ public void run() {
if (serverName == null) {
// try all data sources for a server.properties file
serverName = getServerMotd();

if (serverName != null) {
serverName = serverName.replace("\\n", "<br>");
serverName = serverName.replace("\n", "<br>");
}

if (serverName == null) {
Expand Down Expand Up @@ -655,9 +675,9 @@ public void run() {
eventSummary.put("link", event.getLinkedStatId());
eventSummary.put("active", event.hasStarted(now) && !event.hasEnded(now));

final Ranking<IntValue>.Entry winner = eventWinners.get(event);
final EventWinner winner = eventWinners.get(event);
if (winner != null) {
eventSummary.put("best", winner.toJSON());
eventSummary.put("best", winner.getJSON());
summaryRelevantPlayers.add(winner.getPlayer());
}

Expand Down
12 changes: 8 additions & 4 deletions src/main/java/de/pdinklag/mcstats/bukkit/BukkitUpdater.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

public class BukkitUpdater extends Updater {
private static final String SKINS_RESTORER_PLUGIN_NAME = "SkinsRestorer";
private static final Version SKINS_RESTORER_MIN_VERSION = new Version(14, 2, 2);
private static final Version SKINS_RESTORER_MIN_VERSION = new Version(15, 0, 0);

private final MinecraftStatsPlugin plugin;
private boolean isSkinsRestorerAvailable;
Expand Down Expand Up @@ -44,10 +44,14 @@ public BukkitUpdater(MinecraftStatsPlugin plugin, Config config, LogWriter log)
@Override
protected PlayerProfileProvider getAuthenticProfileProvider() {
if (isSkinsRestorerAvailable) {
return new SkinsRestorerProfileProvider();
} else {
return super.getAuthenticProfileProvider();
try {
return new SkinsRestorerProfileProvider();
} catch (Exception e) {
// trying to retrieve the SkinsRestorer API may fail in certain scenarios
log.writeError("failed to retrieve SkinsRestorer API -- defaulting to Mojang", e);
}
}
return super.getAuthenticProfileProvider();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,37 @@
package de.pdinklag.mcstats.bukkit;

import java.util.Optional;
import java.util.UUID;

import de.pdinklag.mcstats.AccountType;
import de.pdinklag.mcstats.Player;
import de.pdinklag.mcstats.PlayerProfile;
import de.pdinklag.mcstats.PlayerProfileProvider;
import net.skinsrestorer.api.SkinsRestorerAPI;
import net.skinsrestorer.api.model.MojangProfileResponse;
import net.skinsrestorer.api.property.IProperty;
import net.skinsrestorer.api.PropertyUtils;
import net.skinsrestorer.api.SkinsRestorer;
import net.skinsrestorer.api.SkinsRestorerProvider;
import net.skinsrestorer.api.property.SkinProperty;

public class SkinsRestorerProfileProvider implements PlayerProfileProvider {
private final SkinsRestorerAPI api;
private final SkinsRestorer api;

public SkinsRestorerProfileProvider() {
api = SkinsRestorerAPI.getApi();
api = SkinsRestorerProvider.get();
}

@Override
public PlayerProfile getPlayerProfile(Player player) {
final PlayerProfile currentProfile = player.getProfile();
if (currentProfile.hasName()) {
final String skinName = api.getSkinName(currentProfile.getName());
if (skinName != null) {
final IProperty skinData = api.getSkinData(skinName);
if (skinData != null) {
final String skin = api.getSkinTextureUrlStripped(skinData);
return new PlayerProfile(currentProfile.getName(), skin, System.currentTimeMillis());
}
}
}

if (player.getAccountType().maybeMojangAccount()) {
final IProperty skinsRestorerProfile = api.getProfile(player.getUuid());
if (skinsRestorerProfile != null) {
player.setAccountType(AccountType.MOJANG);
final MojangProfileResponse mojangProfile = api.getSkinProfileData(skinsRestorerProfile);
return new PlayerProfile(mojangProfile.getProfileName(),
mojangProfile.getTextures().getSKIN().getStrippedUrl(), System.currentTimeMillis());
} else {
player.setAccountType(AccountType.OFFLINE);
}
final Optional<SkinProperty> skinProperty = api.getPlayerStorage()
.getSkinOfPlayer(UUID.fromString(player.getUuid()));
if (skinProperty.isPresent()) {
player.setAccountType(AccountType.MOJANG);
return new PlayerProfile(
PropertyUtils.getSkinProfileData(skinProperty.get()).getProfileName(),
PropertyUtils.getSkinTextureUrlStripped(skinProperty.get()),
System.currentTimeMillis());
} else if (player.getAccountType().maybeMojangAccount()) {
player.setAccountType(AccountType.OFFLINE);
}
return currentProfile;
return player.getProfile();
}
}
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.3.0
3.3.1