Skip to content

Commit 72fcaf1

Browse files
committed
feat: allow registration of player-scoped variables to stats module
Signed-off-by: chatt <[email protected]>
1 parent d20769c commit 72fcaf1

File tree

10 files changed

+224
-55
lines changed

10 files changed

+224
-55
lines changed

core/src/main/java/tc/oc/pgm/api/Modules.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
import tc.oc.pgm.spawns.SpawnModule;
124124
import tc.oc.pgm.start.StartMatchModule;
125125
import tc.oc.pgm.stats.StatsMatchModule;
126+
import tc.oc.pgm.stats.StatsModule;
126127
import tc.oc.pgm.structure.StructureMatchModule;
127128
import tc.oc.pgm.structure.StructureModule;
128129
import tc.oc.pgm.teams.TeamMatchModule;
@@ -219,7 +220,6 @@ void registerAll() {
219220
register(SoundsMatchModule.class, new SoundsMatchModule.Factory());
220221
register(ObserverToolsMatchModule.class, new ObserverToolsMatchModule.Factory());
221222
register(FireworkMatchModule.class, FireworkMatchModule::new);
222-
register(StatsMatchModule.class, StatsMatchModule::new);
223223
register(MapmakerMatchModule.class, MapmakerMatchModule::new);
224224
register(TNTRenderMatchModule.class, TNTRenderMatchModule::new);
225225
register(PlayerTimeMatchModule.class, PlayerTimeMatchModule::new);
@@ -246,6 +246,7 @@ void registerAll() {
246246
register(RegionModule.class, RegionMatchModule.class, new RegionModule.Factory());
247247
register(FilterModule.class, FilterMatchModule.class, new FilterModule.Factory());
248248
register(SpawnModule.class, SpawnMatchModule.class, new SpawnModule.Factory());
249+
register(StatsModule.class, StatsMatchModule.class, new StatsModule.Factory());
249250
register(CoreModule.class, CoreMatchModule.class, new CoreModule.Factory());
250251
register(WoolModule.class, WoolMatchModule.class, new WoolModule.Factory());
251252
register(ScoreModule.class, ScoreMatchModule.class, new ScoreModule.Factory());
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package tc.oc.pgm.stats;
2+
3+
import java.util.Map;
4+
import java.util.function.Supplier;
5+
import tc.oc.pgm.util.math.Formula;
6+
7+
public class FormulaStatHolder implements StatHolder {
8+
private final Map<Formula<?>, Supplier<Double>> stats;
9+
10+
public FormulaStatHolder(Map<Formula<?>, Supplier<Double>> stats) {
11+
this.stats = stats;
12+
}
13+
14+
@Override
15+
public Number getStat(StatType type) {
16+
return switch (type) {
17+
case StatType.BaseStatType baseStatType -> Double.NaN;
18+
case StatType.FormulaStatType formulaStatType -> stats
19+
.get(formulaStatType.formula)
20+
.get();
21+
};
22+
}
23+
}

core/src/main/java/tc/oc/pgm/stats/PlayerStats.java

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package tc.oc.pgm.stats;
22

3-
import static tc.oc.pgm.stats.StatType.ASSISTS;
4-
import static tc.oc.pgm.stats.StatType.DEATHS;
5-
import static tc.oc.pgm.stats.StatType.KILLS;
6-
import static tc.oc.pgm.stats.StatType.KILL_DEATH_RATIO;
7-
import static tc.oc.pgm.stats.StatType.KILL_STREAK;
3+
import static tc.oc.pgm.stats.StatType.BaseStatType.ASSISTS;
4+
import static tc.oc.pgm.stats.StatType.BaseStatType.DEATHS;
5+
import static tc.oc.pgm.stats.StatType.BaseStatType.FormulaStatType;
6+
import static tc.oc.pgm.stats.StatType.BaseStatType.KILLS;
7+
import static tc.oc.pgm.stats.StatType.BaseStatType.KILL_DEATH_RATIO;
8+
import static tc.oc.pgm.stats.StatType.BaseStatType.KILL_STREAK;
89

910
import java.time.Duration;
1011
import java.time.Instant;
@@ -191,14 +192,17 @@ public Component getBasicStatsMessage() {
191192
@Override
192193
public Number getStat(StatType type) {
193194
return switch (type) {
194-
case KILLS -> kills;
195-
case DEATHS -> deaths;
196-
case ASSISTS -> assists;
197-
case KILL_STREAK -> killstreak;
198-
case BEST_KILL_STREAK -> killstreakMax;
199-
case KILL_DEATH_RATIO -> getKD();
200-
case LONGEST_BOW_SHOT -> longestBowKill;
201-
case DAMAGE -> damageDone;
195+
case StatType.BaseStatType baseStatType -> switch (baseStatType) {
196+
case KILLS -> kills;
197+
case DEATHS -> deaths;
198+
case ASSISTS -> assists;
199+
case KILL_STREAK -> killstreak;
200+
case BEST_KILL_STREAK -> killstreakMax;
201+
case KILL_DEATH_RATIO -> getKD();
202+
case LONGEST_BOW_SHOT -> longestBowKill;
203+
case DAMAGE -> damageDone;
204+
};
205+
case FormulaStatType formulaStatType -> Double.NaN;
202206
};
203207
}
204208

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,87 @@
11
package tc.oc.pgm.stats;
22

33
import static net.kyori.adventure.text.Component.translatable;
4-
import static net.kyori.adventure.text.format.NamedTextColor.*;
4+
import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
5+
import static net.kyori.adventure.text.format.NamedTextColor.RED;
6+
import static net.kyori.adventure.text.format.NamedTextColor.YELLOW;
57
import static tc.oc.pgm.stats.StatsMatchModule.damageComponent;
68
import static tc.oc.pgm.util.text.NumberComponent.number;
79

810
import java.util.Locale;
911
import net.kyori.adventure.text.Component;
12+
import tc.oc.pgm.util.math.Formula;
1013

11-
public enum StatType {
12-
KILLS,
13-
DEATHS,
14-
ASSISTS,
15-
KILL_STREAK,
16-
BEST_KILL_STREAK,
17-
KILL_DEATH_RATIO,
18-
LONGEST_BOW_SHOT {
19-
private final String blocks = key + ".blocks";
14+
public sealed interface StatType {
15+
Component makeNumber(Number number);
16+
17+
Component buildMessage(Component valueComponent);
18+
19+
Component component(StatHolder stats);
20+
21+
enum BaseStatType implements StatType {
22+
KILLS,
23+
DEATHS,
24+
ASSISTS,
25+
KILL_STREAK,
26+
BEST_KILL_STREAK,
27+
KILL_DEATH_RATIO,
28+
LONGEST_BOW_SHOT {
29+
private final String blocks = key + ".blocks";
30+
31+
@Override
32+
public Component makeNumber(Number number) {
33+
return translatable(blocks, number(number, YELLOW));
34+
}
35+
},
36+
DAMAGE {
37+
@Override
38+
public Component makeNumber(Number number) {
39+
return damageComponent(number.doubleValue(), GREEN);
40+
}
41+
};
42+
43+
public final String key = "match.stats.type." + name().toLowerCase(Locale.ROOT);
2044

2145
@Override
2246
public Component makeNumber(Number number) {
23-
return translatable(blocks, number(number, YELLOW));
47+
return number(number, this == DEATHS ? RED : GREEN);
48+
}
49+
50+
@Override
51+
public Component component(StatHolder stats) {
52+
return translatable(key, makeNumber(stats.getStat(this)));
2453
}
25-
},
26-
DAMAGE {
54+
2755
@Override
28-
public Component makeNumber(Number number) {
29-
return damageComponent(number.doubleValue(), GREEN);
56+
public Component buildMessage(Component valueComponent) {
57+
return translatable(key, valueComponent);
3058
}
31-
};
59+
}
3260

33-
public final String key = "match.stats.type." + name().toLowerCase(Locale.ROOT);
34-
public static final StatType[] VALUES = values();
61+
final class FormulaStatType implements StatType {
62+
private static final String KEY = "match.stats.type.generic";
3563

36-
public Component makeNumber(Number number) {
37-
return number(number, this == DEATHS ? RED : GREEN);
38-
}
64+
public final Formula<?> formula;
65+
private final Component friendlyName;
3966

40-
public Component component(StatHolder stats) {
41-
return translatable(key, makeNumber(stats.getStat(this)));
67+
public FormulaStatType(Formula<?> formula, Component friendlyName) {
68+
this.formula = formula;
69+
this.friendlyName = friendlyName;
70+
}
71+
72+
@Override
73+
public Component makeNumber(Number number) {
74+
return number(number, GREEN);
75+
}
76+
77+
@Override
78+
public Component component(StatHolder stats) {
79+
return translatable(KEY, friendlyName, makeNumber(stats.getStat(this)));
80+
}
81+
82+
@Override
83+
public Component buildMessage(Component valueComponent) {
84+
return translatable(KEY, friendlyName, valueComponent);
85+
}
4286
}
4387
}

core/src/main/java/tc/oc/pgm/stats/StatsMatchModule.java

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Set;
2323
import java.util.UUID;
2424
import java.util.concurrent.TimeUnit;
25+
import java.util.function.Supplier;
2526
import net.kyori.adventure.text.Component;
2627
import net.kyori.adventure.text.format.NamedTextColor;
2728
import net.kyori.adventure.text.format.TextColor;
@@ -75,6 +76,7 @@
7576
import tc.oc.pgm.teams.Team;
7677
import tc.oc.pgm.tracker.TrackerMatchModule;
7778
import tc.oc.pgm.tracker.info.ProjectileInfo;
79+
import tc.oc.pgm.util.math.Formula;
7880
import tc.oc.pgm.util.named.NameStyle;
7981
import tc.oc.pgm.util.player.PlayerComponent;
8082
import tc.oc.pgm.util.text.RenderableComponent;
@@ -93,6 +95,7 @@ public class StatsMatchModule implements MatchModule, Listener {
9395
private final Match match;
9496
private final Map<UUID, PlayerStats> allPlayerStats = new HashMap<>();
9597
private final Table<Team, UUID, PlayerStats> stats = HashBasedTable.create();
98+
private final List<FormulaStat> formulaStats;
9699

97100
private final boolean verboseStats = PGM.get().getConfiguration().showVerboseStats();
98101
private final Duration showAfter = PGM.get().getConfiguration().showStatsAfter();
@@ -102,8 +105,9 @@ public class StatsMatchModule implements MatchModule, Listener {
102105

103106
private List<MenuItem> teams;
104107

105-
public StatsMatchModule(Match match) {
108+
public StatsMatchModule(Match match, List<FormulaStat> formulaStats) {
106109
this.match = match;
110+
this.formulaStats = formulaStats;
107111
}
108112

109113
public Map<UUID, PlayerStats> getStats() {
@@ -290,14 +294,28 @@ public void onStatsDisplay(MatchStatsEvent event) {
290294

291295
// Gather aggregated player stats from this match
292296
List<AggStat<?>> stats = new ArrayList<>();
293-
stats.add(new AggStat<>(StatType.KILLS, 0, new HashSet<>()));
294-
stats.add(new AggStat<>(StatType.DEATHS, 0, new HashSet<>()));
295-
stats.add(new AggStat<>(StatType.ASSISTS, 0, new HashSet<>()));
296-
stats.add(new AggStat<>(StatType.BEST_KILL_STREAK, 0, new HashSet<>()));
297-
stats.add(new AggStat<>(StatType.LONGEST_BOW_SHOT, 0, new HashSet<>()));
298-
if (verboseStats) stats.add(new AggStat<>(StatType.DAMAGE, 0d, new HashSet<>()));
297+
stats.add(new AggStat<>(StatType.BaseStatType.KILLS, 0, new HashSet<>()));
298+
stats.add(new AggStat<>(StatType.BaseStatType.DEATHS, 0, new HashSet<>()));
299+
stats.add(new AggStat<>(StatType.BaseStatType.ASSISTS, 0, new HashSet<>()));
300+
stats.add(new AggStat<>(StatType.BaseStatType.BEST_KILL_STREAK, 0, new HashSet<>()));
301+
stats.add(new AggStat<>(StatType.BaseStatType.LONGEST_BOW_SHOT, 0, new HashSet<>()));
302+
if (verboseStats) stats.add(new AggStat<>(StatType.BaseStatType.DAMAGE, 0d, new HashSet<>()));
303+
for (FormulaStat formulaStat : formulaStats) {
304+
stats.add(new AggStat<>(
305+
new StatType.FormulaStatType(formulaStat.formula, formulaStat.statDisplayName),
306+
formulaStat.defaultValue,
307+
new HashSet<>()));
308+
}
299309

300-
allPlayerStats.forEach((uuid, s) -> stats.replaceAll(stat -> stat.track(uuid, s)));
310+
for (Map.Entry<UUID, PlayerStats> playerStats : allPlayerStats.entrySet()) {
311+
FormulaStatHolder formulaStatHolder =
312+
new FormulaStatHolder(getPlayerFormulaStatSuppliers(playerStats.getKey()));
313+
StatHolder compositeStatHolder = (statType) -> switch (statType) {
314+
case StatType.BaseStatType baseStatType -> playerStats.getValue().getStat(statType);
315+
case StatType.FormulaStatType formulaStatType -> formulaStatHolder.getStat(statType);
316+
};
317+
stats.replaceAll(stat -> stat.track(playerStats.getKey(), compositeStatHolder));
318+
}
301319

302320
var best = stats.stream()
303321
.filter(agg -> agg.value().doubleValue() > 0)
@@ -356,7 +374,7 @@ private Component getMessage(AggStat<?> agg, boolean best, boolean own) {
356374
var number = agg.type.makeNumber(getGlobalPlayerStat(p.getUniqueId()).getStat(agg.type));
357375
return !best ? number : text(" ").append(translatable("match.stats.you.short", number));
358376
});
359-
return translatable(agg.type.key, who);
377+
return agg.type.buildMessage(who);
360378
}
361379

362380
private Component credit(Set<UUID> players) {
@@ -422,6 +440,18 @@ public final PlayerStats getGlobalPlayerStat(UUID uuid) {
422440
return allPlayerStats.computeIfAbsent(uuid, k -> new PlayerStats());
423441
}
424442

443+
private Map<Formula<?>, Supplier<Double>> getPlayerFormulaStatSuppliers(UUID uuid) {
444+
MatchPlayer player = match.getPlayer(uuid);
445+
if (player == null) {
446+
return Collections.emptyMap();
447+
}
448+
Map<Formula<?>, Supplier<Double>> formulaStatSuppliers = new HashMap<>();
449+
for (FormulaStat formulaStat : formulaStats) {
450+
formulaStatSuppliers.put(formulaStat.formula, () -> formulaStat.formula.apply(player));
451+
}
452+
return formulaStatSuppliers;
453+
}
454+
425455
public final PlayerStats getPlayerStat(ParticipantState player) {
426456
return getPlayerTeamStats(player.getId(), player.getParty());
427457
}
@@ -474,4 +504,7 @@ public AggStat<T> track(UUID uuid, StatHolder stat) {
474504
return cmp == 0 ? this : new AggStat<>(type, newVal, players);
475505
}
476506
}
507+
508+
public record FormulaStat(
509+
Formula<MatchPlayer> formula, Component statDisplayName, double defaultValue) {}
477510
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package tc.oc.pgm.stats;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.logging.Logger;
7+
import net.kyori.adventure.text.Component;
8+
import org.jdom2.Document;
9+
import org.jdom2.Element;
10+
import org.jetbrains.annotations.Nullable;
11+
import tc.oc.pgm.api.map.MapModule;
12+
import tc.oc.pgm.api.map.factory.MapFactory;
13+
import tc.oc.pgm.api.map.factory.MapModuleFactory;
14+
import tc.oc.pgm.api.match.Match;
15+
import tc.oc.pgm.api.module.exception.ModuleLoadException;
16+
import tc.oc.pgm.api.player.MatchPlayer;
17+
import tc.oc.pgm.util.math.Formula;
18+
import tc.oc.pgm.util.xml.InvalidXMLException;
19+
import tc.oc.pgm.util.xml.XMLFluentParser;
20+
import tc.oc.pgm.util.xml.XMLUtils;
21+
22+
public class StatsModule implements MapModule<StatsMatchModule> {
23+
private final List<StatsMatchModule.FormulaStat> formulaStats;
24+
25+
private StatsModule(List<StatsMatchModule.FormulaStat> formulaStats) {
26+
this.formulaStats = formulaStats;
27+
}
28+
29+
@Override
30+
public @Nullable StatsMatchModule createMatchModule(Match match) throws ModuleLoadException {
31+
return new StatsMatchModule(match, formulaStats);
32+
}
33+
34+
public static class Factory implements MapModuleFactory<StatsModule> {
35+
@Override
36+
public StatsModule parse(MapFactory factory, Logger logger, Document doc)
37+
throws InvalidXMLException {
38+
XMLFluentParser parser = factory.getParser();
39+
Element statsElement = XMLUtils.getUniqueChild(doc.getRootElement(), "stats");
40+
if (statsElement == null) {
41+
return new StatsModule(Collections.emptyList());
42+
} else {
43+
List<StatsMatchModule.FormulaStat> formulaStats = new ArrayList<>();
44+
for (Element el : XMLUtils.getChildren(statsElement, "stat")) {
45+
Component displayName = XMLUtils.parseFormattedText(el, "display-name");
46+
double initialStatValue =
47+
XMLUtils.parseNumber(el.getAttribute("initial"), Double.class, 0D);
48+
if (displayName == null) {
49+
throw new InvalidXMLException("Expected a display name to be present", el);
50+
}
51+
Formula<MatchPlayer> formulaDef =
52+
parser.formula(MatchPlayer.class, el, "value").required();
53+
formulaStats.add(
54+
new StatsMatchModule.FormulaStat(formulaDef, displayName, initialStatValue));
55+
}
56+
return new StatsModule(formulaStats);
57+
}
58+
}
59+
}
60+
}

core/src/main/java/tc/oc/pgm/stats/TeamStats.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ public TeamStats(Collection<PlayerStats> playerStats) {
3737
@Override
3838
public Number getStat(StatType type) {
3939
return switch (type) {
40-
case KILLS -> teamKills;
41-
case DEATHS -> teamDeaths;
42-
case KILL_DEATH_RATIO -> teamKD;
43-
case DAMAGE -> damageDone;
44-
default -> Double.NaN;
40+
case StatType.BaseStatType baseStatType -> switch (baseStatType) {
41+
case KILLS -> teamKills;
42+
case DEATHS -> teamDeaths;
43+
case KILL_DEATH_RATIO -> teamKD;
44+
case DAMAGE -> damageDone;
45+
default -> Double.NaN;
46+
};
47+
case StatType.FormulaStatType formulaStatType -> Double.NaN;
4548
};
4649
}
4750

core/src/main/java/tc/oc/pgm/stats/menu/items/PlayerStatsMenuItem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import static net.kyori.adventure.text.Component.translatable;
66
import static net.kyori.adventure.text.JoinConfiguration.separator;
77
import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
8-
import static tc.oc.pgm.stats.StatType.*;
8+
import static tc.oc.pgm.stats.StatType.BaseStatType.*;
99
import static tc.oc.pgm.stats.StatsMatchModule.damageComponent;
1010
import static tc.oc.pgm.util.nms.NMSHacks.NMS_HACKS;
1111
import static tc.oc.pgm.util.nms.PlayerUtils.PLAYER_UTILS;

0 commit comments

Comments
 (0)