Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dev.nandi0813.practice.manager.matchhistory;

import lombok.Getter;
import lombok.Setter;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
* Per-player match history model. Analog to {@code Profile}, owning its own
* {@link MatchHistoryFile} for persistence and caching the recent entries.
* <p>
* Entries are kept newest-first, capped at {@link #MAX_HISTORY}.
*/
@Getter
@Setter
public class MatchHistory {

private static final int MAX_HISTORY = 5;

private final UUID uuid;
private final MatchHistoryFile file;
private final List<MatchHistoryEntry> matches = new ArrayList<>();

public MatchHistory(UUID uuid) {
this.uuid = uuid;
this.file = new MatchHistoryFile(this);
}

/**
* Loads YAML matches into the cache. Returns the loaded matches (newest-first).
*/
public List<MatchHistoryEntry> load() {
file.getData();
return matches;
}

/**
* Adds a match to the front of the cache (newest-first), keeping the size within the cap.
*/
public void add(MatchHistoryEntry match) {
matches.addFirst(match);
if (matches.size() > MAX_HISTORY) matches.subList(MAX_HISTORY, matches.size()).clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,93 +19,102 @@ public class MatchHistoryFile extends ConfigFile {
private static final int MAX_HISTORY = 5;
private static final String ROOT = "matches";

private final UUID playerUuid;
private final MatchHistory matchHistory;

public MatchHistoryFile(UUID playerUuid) {
super("/match-history/", playerUuid.toString().toLowerCase());
this.playerUuid = playerUuid;
public MatchHistoryFile(MatchHistory matchHistory) {
super("/match-history/", matchHistory.getUuid().toString().toLowerCase());
this.matchHistory = matchHistory;

saveFile();
reloadFile();
}

@Override
public void setData() {
saveFile();
}

/**
* Saves a new match and prunes entries beyond the cap.
* Returns the assigned id.
*/
public int saveMatch(MatchHistoryEntry entry) {
int nextId = getNextId();
setMatch(entry, nextId);
pruneOldMatches();
saveFile();
return nextId;
}

private void setMatch(MatchHistoryEntry entry, int matchId) {
String match = ROOT + "." + matchId;

config.set(match + ".player_uuid", entry.getPlayerUuid().toString());
config.set(match + ".opponent_uuid", entry.getOpponentUuid().toString());
config.set(match + ".player_name", entry.getPlayerName());
config.set(match + ".opponent_name", entry.getOpponentName());
config.set(match + ".kit_name", entry.getKitName());
config.set(match + ".arena_name", entry.getArenaName());
config.set(match + ".player_score", entry.getPlayerScore());
config.set(match + ".opponent_score", entry.getOpponentScore());
config.set(match + ".player_final_health", entry.getPlayerFinalHealth());
config.set(match + ".opponent_final_health", entry.getOpponentFinalHealth());
config.set(match + ".winner_uuid", entry.getWinnerUuid() != null ? entry.getWinnerUuid().toString() : "");
config.set(match + ".match_duration", entry.getMatchDuration());
config.set(match + ".played_at", entry.getPlayedAt());
}

@Override
public void getData() {
// Data is loaded on demand via loadEntries()
loadMatches();
}

/**
* Loads all stored entries for this player, ordered newest-first.
*/
public List<MatchHistoryEntry> loadEntries() {
List<MatchHistoryEntry> result = new ArrayList<>();
private void loadMatches() {
matchHistory.getMatches().clear();

ConfigurationSection root = config.getConfigurationSection(ROOT);
if (root == null) return result;
if (root == null) return;

for (String key : root.getKeys(false)) {
ConfigurationSection section = root.getConfigurationSection(key);
if (section == null) continue;

try {
UUID pUuid = UUID.fromString(section.getString("player_uuid", playerUuid.toString()));
UUID oppUuid = UUID.fromString(section.getString("opponent_uuid", "00000000-0000-0000-0000-000000000000"));
String winStr = section.getString("winner_uuid", null);
UUID winnerUuid = (winStr != null && !winStr.isEmpty()) ? UUID.fromString(winStr) : null;

result.add(new MatchHistoryEntry(
Integer.parseInt(key),
pUuid,
oppUuid,
section.getString("player_name", "Unknown"),
section.getString("opponent_name", "Unknown"),
section.getString("kit_name", "Unknown"),
section.getString("arena_name", "Unknown"),
section.getInt("player_score", 0),
section.getInt("opponent_score", 0),
section.getDouble("player_final_health", 0.0),
section.getDouble("opponent_final_health", 0.0),
winnerUuid,
section.getInt("match_duration", 0),
section.getLong("played_at", System.currentTimeMillis())
));
} catch (Exception e) {
Common.sendConsoleMMMessage("<yellow>[MatchHistory] Skipping corrupt entry " + key
+ " for " + playerUuid + ": " + e.getMessage());
MatchHistoryEntry entry = loadMatch(key);
if (entry != null) {
matchHistory.getMatches().add(entry);
}
}

result.sort((a, b) -> Integer.compare(b.getMatchId(), a.getMatchId()));
return result;
matchHistory.getMatches().sort((a, b) -> Integer.compare(b.getMatchId(), a.getMatchId()));
}

/**
* Saves a new entry and prunes entries beyond the cap.
* Returns the assigned id.
*/
public int saveEntry(MatchHistoryEntry entry) {
int nextId = getNextId();
String path = ROOT + "." + nextId;

config.set(path + ".player_uuid", entry.getPlayerUuid().toString());
config.set(path + ".opponent_uuid", entry.getOpponentUuid().toString());
config.set(path + ".player_name", entry.getPlayerName());
config.set(path + ".opponent_name", entry.getOpponentName());
config.set(path + ".kit_name", entry.getKitName());
config.set(path + ".arena_name", entry.getArenaName());
config.set(path + ".player_score", entry.getPlayerScore());
config.set(path + ".opponent_score", entry.getOpponentScore());
config.set(path + ".player_final_health", entry.getPlayerFinalHealth());
config.set(path + ".opponent_final_health", entry.getOpponentFinalHealth());
config.set(path + ".winner_uuid", entry.getWinnerUuid() != null ? entry.getWinnerUuid().toString() : "");
config.set(path + ".match_duration", entry.getMatchDuration());
config.set(path + ".played_at", entry.getPlayedAt());

pruneOldEntries();
saveFile();
return nextId;
private MatchHistoryEntry loadMatch(String matchId) {
String match = ROOT + "." + matchId;

try {
return new MatchHistoryEntry(
Integer.parseInt(matchId),
UUID.fromString(config.getString(match + ".player_uuid", matchHistory.getUuid().toString())),
UUID.fromString(config.getString(match + ".opponent_uuid", "00000000-0000-0000-0000-000000000000")),
config.getString(match + ".player_name", "Unknown"),
config.getString(match + ".opponent_name", "Unknown"),
config.getString(match + ".kit_name", "Unknown"),
config.getString(match + ".arena_name", "Unknown"),
config.getInt(match + ".player_score", 0),
config.getInt(match + ".opponent_score", 0),
config.getDouble(match + ".player_final_health", 0.0),
config.getDouble(match + ".opponent_final_health", 0.0),
getWinnerUuid(match),
config.getInt(match + ".match_duration", 0),
config.getLong(match + ".played_at", System.currentTimeMillis())
);
} catch (Exception e) {
Common.sendConsoleMMMessage("<yellow>[MatchHistory] Skipping corrupt entry " + matchId
+ " for " + matchHistory.getUuid() + ": " + e.getMessage());
return null;
}
}

private UUID getWinnerUuid(String match) {
String winnerUuid = config.getString(match + ".winner_uuid");
return (winnerUuid != null && !winnerUuid.isEmpty()) ? UUID.fromString(winnerUuid) : null;
}

private int getNextId() {
Expand All @@ -119,7 +128,7 @@ private int getNextId() {
return max + 1;
}

private void pruneOldEntries() {
private void pruneOldMatches() {
ConfigurationSection root = config.getConfigurationSection(ROOT);
if (root == null) return;

Expand All @@ -135,4 +144,4 @@ private void pruneOldEntries() {
config.set(ROOT + "." + id, null);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@

/**
* Central manager for match history.
*
* <p>
* Storage strategy:
* 1. YAML files — always used (match-history/<uuid>.yml), same pattern as profiles/.
* 2. MySQL — also used when connected; logic lives in MysqlManager.
*
* <p>
* Both reads and writes are async. An in-memory cache avoids repeated disk access.
*/
public class MatchHistoryManager implements Listener {
Expand All @@ -38,7 +38,15 @@ public static MatchHistoryManager getInstance() {
private static final int MAX_HISTORY = 5;

/** In-memory cache: player UUID → last MAX_HISTORY entries, newest-first. */
private final Map<UUID, List<MatchHistoryEntry>> cache = new ConcurrentHashMap<>();
private final Map<UUID, MatchHistory> matchHistories = new ConcurrentHashMap<>();

/**
* Returns the cached per-player {@link MatchHistory}, creating one on demand.
* Analogous to {@code ProfileManager#getProfile(UUID)}.
*/
public MatchHistory getMatchHistory(UUID uuid) {
return matchHistories.computeIfAbsent(uuid, MatchHistory::new);
}

private MatchHistoryManager() {
Bukkit.getPluginManager().registerEvents(this, ZonePractice.getInstance());
Expand Down Expand Up @@ -82,8 +90,8 @@ public void saveMatchAsync(UUID playerUuid, UUID opponentUuid,
kitName, arenaName, opponentScore, playerScore,
opponentFinalHealth, playerFinalHealth, winnerUuid, matchDuration, now);

addToCache(playerUuid, finalPlayerPov);
addToCache(opponentUuid, finalOpponentPov);
getMatchHistory(playerUuid).add(finalPlayerPov);
getMatchHistory(opponentUuid).add(finalOpponentPov);

if (MysqlManager.isConnected(false)) {
MysqlManager.saveMatchHistoryAsync(
Expand All @@ -99,46 +107,33 @@ public void saveMatchAsync(UUID playerUuid, UUID opponentUuid,
* Returns from cache if available; otherwise reads YAML (and MySQL as fallback if YAML empty).
*/
public CompletableFuture<List<MatchHistoryEntry>> loadHistoryAsync(UUID playerUuid) {
if (cache.containsKey(playerUuid)) {
return CompletableFuture.completedFuture(new ArrayList<>(cache.get(playerUuid)));
MatchHistory history = matchHistories.get(playerUuid);
if (history != null && !history.getMatches().isEmpty()) {
return CompletableFuture.completedFuture(new ArrayList<>(history.getMatches()));
}

return CompletableFuture.supplyAsync(() -> {
List<MatchHistoryEntry> entries = loadFromYaml(playerUuid);
MatchHistory loaded = getMatchHistory(playerUuid);
List<MatchHistoryEntry> entries = loaded.load();

if (entries.isEmpty() && MysqlManager.isConnected(false)) {
entries = MysqlManager.loadMatchHistorySync(playerUuid, MAX_HISTORY);
loaded.getMatches().addAll(entries);
}

cache.put(playerUuid, new ArrayList<>(entries));
return entries;
});
}

private int saveToYaml(UUID uuid, MatchHistoryEntry entry) {
try {
return new MatchHistoryFile(uuid).saveEntry(entry);
return getMatchHistory(uuid).getFile().saveMatch(entry);
} catch (Exception e) {
Common.sendConsoleMMMessage("<red>[MatchHistory] YAML save error for " + uuid + ": " + e.getMessage());
return -1;
}
}

private List<MatchHistoryEntry> loadFromYaml(UUID uuid) {
try {
return new MatchHistoryFile(uuid).loadEntries();
} catch (Exception e) {
Common.sendConsoleMMMessage("<red>[MatchHistory] YAML load error for " + uuid + ": " + e.getMessage());
return Collections.emptyList();
}
}

private void addToCache(UUID uuid, MatchHistoryEntry entry) {
List<MatchHistoryEntry> list = cache.computeIfAbsent(uuid, k -> new ArrayList<>());
list.add(0, entry); // prepend = newest first
if (list.size() > MAX_HISTORY) list.subList(MAX_HISTORY, list.size()).clear();
}

@EventHandler(priority = EventPriority.LOW)
public void onMatchEnd(MatchEndEvent e) {
if (!(e.getMatch() instanceof Duel duel)) {
Expand Down
Loading