Skip to content

Commit 1367d4b

Browse files
authored
Allow registering player-based formulas to stats match module (#1499)
Signed-off-by: chatt <[email protected]>
1 parent d20769c commit 1367d4b

File tree

11 files changed

+192
-51
lines changed

11 files changed

+192
-51
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());

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
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.Builtin.ASSISTS;
4+
import static tc.oc.pgm.stats.StatType.Builtin.DEATHS;
5+
import static tc.oc.pgm.stats.StatType.Builtin.KILLS;
6+
import static tc.oc.pgm.stats.StatType.Builtin.KILL_DEATH_RATIO;
7+
import static tc.oc.pgm.stats.StatType.Builtin.KILL_STREAK;
88

99
import java.time.Duration;
1010
import java.time.Instant;
@@ -189,7 +189,7 @@ public Component getBasicStatsMessage() {
189189

190190
// Getters, both raw stats and some handy calculations
191191
@Override
192-
public Number getStat(StatType type) {
192+
public Number getStat(StatType.Builtin type) {
193193
return switch (type) {
194194
case KILLS -> kills;
195195
case DEATHS -> deaths;

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ public interface StatHolder {
1111
JoinConfiguration PIPE = separator(text(" | "));
1212
JoinConfiguration SPACES = separator(text(" "));
1313

14-
Number getStat(StatType type);
14+
Number getStat(StatType.Builtin type);
1515

16-
default Component pipeSeparated(StatType... types) {
16+
default Component pipeSeparated(StatType.Builtin... types) {
1717
return getComponent(PIPE, types);
1818
}
1919

20-
default Component spaceSeparated(StatType... types) {
20+
default Component spaceSeparated(StatType.Builtin... types) {
2121
return getComponent(SPACES, types);
2222
}
2323

24-
default Component getComponent(JoinConfiguration joining, StatType... types) {
24+
default Component getComponent(JoinConfiguration joining, StatType.Builtin... types) {
2525
Component[] children = new Component[types.length];
2626
for (int i = 0; i < types.length; i++) children[i] = types[i].component(this);
2727
return join(joining, children);
Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,82 @@
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 net.kyori.adventure.text.format.TextColor;
13+
import tc.oc.pgm.api.player.MatchPlayer;
14+
import tc.oc.pgm.util.math.Formula;
1015

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";
16+
public sealed interface StatType<I> {
17+
Component makeNumber(Number number);
18+
19+
Component component(Component valueComponent);
20+
21+
Component component(I input);
22+
23+
enum Builtin implements StatType<StatHolder> {
24+
KILLS,
25+
DEATHS,
26+
ASSISTS,
27+
KILL_STREAK,
28+
BEST_KILL_STREAK,
29+
KILL_DEATH_RATIO,
30+
LONGEST_BOW_SHOT {
31+
private final String blocks = key + ".blocks";
32+
33+
@Override
34+
public Component makeNumber(Number number) {
35+
return translatable(blocks, number(number, YELLOW));
36+
}
37+
},
38+
DAMAGE {
39+
@Override
40+
public Component makeNumber(Number number) {
41+
return damageComponent(number.doubleValue(), GREEN);
42+
}
43+
};
44+
45+
public final String key = "match.stats.type." + name().toLowerCase(Locale.ROOT);
2046

2147
@Override
2248
public Component makeNumber(Number number) {
23-
return translatable(blocks, number(number, YELLOW));
49+
return number(number, this == DEATHS ? RED : GREEN);
2450
}
25-
},
26-
DAMAGE {
51+
2752
@Override
28-
public Component makeNumber(Number number) {
29-
return damageComponent(number.doubleValue(), GREEN);
53+
public Component component(StatHolder stats) {
54+
return translatable(key, makeNumber(stats.getStat(this)));
3055
}
31-
};
32-
33-
public final String key = "match.stats.type." + name().toLowerCase(Locale.ROOT);
34-
public static final StatType[] VALUES = values();
3556

36-
public Component makeNumber(Number number) {
37-
return number(number, this == DEATHS ? RED : GREEN);
57+
@Override
58+
public Component component(Component valueComponent) {
59+
return translatable(key, valueComponent);
60+
}
3861
}
3962

40-
public Component component(StatHolder stats) {
41-
return translatable(key, makeNumber(stats.getStat(this)));
63+
record OfFormula(Component name, Formula<MatchPlayer> formula, TextColor color)
64+
implements StatType<MatchPlayer> {
65+
private static final String KEY = "match.stats.type.generic";
66+
67+
@Override
68+
public Component makeNumber(Number number) {
69+
return number(number).color(color);
70+
}
71+
72+
@Override
73+
public Component component(MatchPlayer player) {
74+
return translatable(KEY, name, makeNumber(formula.apply(player)));
75+
}
76+
77+
@Override
78+
public Component component(Component valueComponent) {
79+
return translatable(KEY, name, valueComponent);
80+
}
4281
}
4382
}

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

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ public class StatsMatchModule implements MatchModule, Listener {
9393
private final Match match;
9494
private final Map<UUID, PlayerStats> allPlayerStats = new HashMap<>();
9595
private final Table<Team, UUID, PlayerStats> stats = HashBasedTable.create();
96+
private final List<StatType.OfFormula> formulaStats;
9697

9798
private final boolean verboseStats = PGM.get().getConfiguration().showVerboseStats();
9899
private final Duration showAfter = PGM.get().getConfiguration().showStatsAfter();
@@ -102,8 +103,9 @@ public class StatsMatchModule implements MatchModule, Listener {
102103

103104
private List<MenuItem> teams;
104105

105-
public StatsMatchModule(Match match) {
106+
public StatsMatchModule(Match match, List<StatType.OfFormula> formulaStats) {
106107
this.match = match;
108+
this.formulaStats = formulaStats;
107109
}
108110

109111
public Map<UUID, PlayerStats> getStats() {
@@ -290,14 +292,20 @@ public void onStatsDisplay(MatchStatsEvent event) {
290292

291293
// Gather aggregated player stats from this match
292294
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<>()));
295+
stats.add(new AggStat<>(StatType.Builtin.KILLS, 0, new HashSet<>()));
296+
stats.add(new AggStat<>(StatType.Builtin.DEATHS, 0, new HashSet<>()));
297+
stats.add(new AggStat<>(StatType.Builtin.ASSISTS, 0, new HashSet<>()));
298+
stats.add(new AggStat<>(StatType.Builtin.BEST_KILL_STREAK, 0, new HashSet<>()));
299+
stats.add(new AggStat<>(StatType.Builtin.LONGEST_BOW_SHOT, 0, new HashSet<>()));
300+
if (verboseStats) stats.add(new AggStat<>(StatType.Builtin.DAMAGE, 0d, new HashSet<>()));
301+
for (StatType.OfFormula formulaStat : formulaStats) {
302+
stats.add(new AggStat<>(formulaStat, 0d, new HashSet<>()));
303+
}
299304

300-
allPlayerStats.forEach((uuid, s) -> stats.replaceAll(stat -> stat.track(uuid, s)));
305+
allPlayerStats.forEach((uuid, s) -> {
306+
MatchPlayer player = match.getPlayer(uuid);
307+
stats.replaceAll(stat -> stat.track(uuid, player, s));
308+
});
301309

302310
var best = stats.stream()
303311
.filter(agg -> agg.value().doubleValue() > 0)
@@ -353,10 +361,13 @@ private Component getMessage(AggStat<?> agg, boolean best, boolean own) {
353361
who = who.append((RenderableComponent) v -> {
354362
if (!(v instanceof Player p)) return empty();
355363
if (agg.players.contains(p.getUniqueId()) || hasNoStats(p.getUniqueId())) return empty();
356-
var number = agg.type.makeNumber(getGlobalPlayerStat(p.getUniqueId()).getStat(agg.type));
364+
var value = getStatValue(
365+
agg.type, match.getPlayer(p.getUniqueId()), getGlobalPlayerStat(p.getUniqueId()));
366+
if (value == null) return empty();
367+
var number = agg.type.makeNumber(value);
357368
return !best ? number : text(" ").append(translatable("match.stats.you.short", number));
358369
});
359-
return translatable(agg.type.key, who);
370+
return agg.type.component(who);
360371
}
361372

362373
private Component credit(Set<UUID> players) {
@@ -465,13 +476,23 @@ public Team getPrimaryTeam(UUID uuid, boolean includeObservers) {
465476

466477
private record AggStat<T extends Number & Comparable<T>>(
467478
StatType type, T value, Set<UUID> players) {
468-
public AggStat<T> track(UUID uuid, StatHolder stat) {
469-
T newVal = (T) stat.getStat(type);
479+
public AggStat<T> track(UUID uuid, MatchPlayer player, StatHolder stat) {
480+
T newVal = (T) getStatValue(type, player, stat);
481+
if (newVal == null) return this;
470482
int cmp = value.compareTo(newVal);
471483
if (cmp > 0) return this;
472484
if (cmp < 0) players.clear();
473485
players.add(uuid);
474486
return cmp == 0 ? this : new AggStat<>(type, newVal, players);
475487
}
476488
}
489+
490+
private static Number getStatValue(StatType<?> statType, MatchPlayer player, StatHolder stat) {
491+
return switch (statType) {
492+
case StatType.Builtin builtin -> stat.getStat(builtin);
493+
case StatType.OfFormula formulaStats -> player != null
494+
? formulaStats.formula().apply(player)
495+
: null;
496+
};
497+
}
477498
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package tc.oc.pgm.stats;
2+
3+
import com.google.common.collect.ImmutableList;
4+
import java.util.Collection;
5+
import java.util.logging.Logger;
6+
import net.kyori.adventure.text.Component;
7+
import net.kyori.adventure.text.format.NamedTextColor;
8+
import net.kyori.adventure.text.format.TextColor;
9+
import org.jdom2.Document;
10+
import org.jdom2.Element;
11+
import org.jetbrains.annotations.Nullable;
12+
import tc.oc.pgm.api.map.MapModule;
13+
import tc.oc.pgm.api.map.factory.MapFactory;
14+
import tc.oc.pgm.api.map.factory.MapModuleFactory;
15+
import tc.oc.pgm.api.match.Match;
16+
import tc.oc.pgm.api.module.exception.ModuleLoadException;
17+
import tc.oc.pgm.api.player.MatchPlayer;
18+
import tc.oc.pgm.util.math.Formula;
19+
import tc.oc.pgm.util.xml.InvalidXMLException;
20+
import tc.oc.pgm.util.xml.XMLFluentParser;
21+
import tc.oc.pgm.util.xml.XMLUtils;
22+
import tc.oc.pgm.variables.VariablesModule;
23+
24+
public class StatsModule implements MapModule<StatsMatchModule> {
25+
private final ImmutableList<StatType.OfFormula> formulaStats;
26+
27+
private StatsModule(ImmutableList<StatType.OfFormula> formulaStats) {
28+
this.formulaStats = formulaStats;
29+
}
30+
31+
@Override
32+
public @Nullable StatsMatchModule createMatchModule(Match match) throws ModuleLoadException {
33+
return new StatsMatchModule(match, formulaStats);
34+
}
35+
36+
public static class Factory implements MapModuleFactory<StatsModule> {
37+
38+
@Override
39+
public Collection<Class<? extends MapModule<?>>> getWeakDependencies() {
40+
return ImmutableList.of(VariablesModule.class);
41+
}
42+
43+
@Override
44+
public StatsModule parse(MapFactory factory, Logger logger, Document doc)
45+
throws InvalidXMLException {
46+
XMLFluentParser parser = factory.getParser();
47+
ImmutableList.Builder<StatType.OfFormula> formulaStats = new ImmutableList.Builder<>();
48+
for (Element el : XMLUtils.flattenElements(doc.getRootElement(), "stats", "stat")) {
49+
Component name = parser.component(el, "name").required();
50+
TextColor color = parser.textColor(el, "color").optional(NamedTextColor.GREEN);
51+
Formula<MatchPlayer> formulaDef =
52+
parser.formula(MatchPlayer.class, el, "value").required();
53+
formulaStats.add(new StatType.OfFormula(name, formulaDef, color));
54+
}
55+
return new StatsModule(formulaStats.build());
56+
}
57+
}
58+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public TeamStats(Collection<PlayerStats> playerStats) {
3535
}
3636

3737
@Override
38-
public Number getStat(StatType type) {
38+
public Number getStat(StatType.Builtin type) {
3939
return switch (type) {
4040
case KILLS -> teamKills;
4141
case DEATHS -> teamDeaths;

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.Builtin.*;
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;

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import static net.kyori.adventure.text.Component.text;
44
import static net.kyori.adventure.text.Component.translatable;
55
import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
6-
import static tc.oc.pgm.stats.StatType.DEATHS;
7-
import static tc.oc.pgm.stats.StatType.KILLS;
8-
import static tc.oc.pgm.stats.StatType.KILL_DEATH_RATIO;
6+
import static tc.oc.pgm.stats.StatType.Builtin.DEATHS;
7+
import static tc.oc.pgm.stats.StatType.Builtin.KILLS;
8+
import static tc.oc.pgm.stats.StatType.Builtin.KILL_DEATH_RATIO;
99
import static tc.oc.pgm.stats.StatsMatchModule.damageComponent;
1010
import static tc.oc.pgm.util.nms.PlayerUtils.PLAYER_UTILS;
1111
import static tc.oc.pgm.util.text.NumberComponent.number;

core/src/main/java/tc/oc/pgm/util/xml/XMLFluentParser.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package tc.oc.pgm.util.xml;
22

33
import java.time.Duration;
4+
import net.kyori.adventure.text.Component;
5+
import net.kyori.adventure.text.format.TextColor;
46
import org.bukkit.Material;
57
import org.bukkit.util.BlockVector;
68
import org.bukkit.util.Vector;
@@ -18,6 +20,7 @@
1820
import tc.oc.pgm.regions.RegionParser;
1921
import tc.oc.pgm.util.math.Formula;
2022
import tc.oc.pgm.util.text.TextException;
23+
import tc.oc.pgm.util.text.TextFormatter;
2124
import tc.oc.pgm.util.text.TextParser;
2225
import tc.oc.pgm.util.xml.parsers.BoolBuilder;
2326
import tc.oc.pgm.util.xml.parsers.Builder;
@@ -137,6 +140,24 @@ public ItemBuilder item(Element el, String... prop) {
137140
return new ItemBuilder(kits, el, prop);
138141
}
139142

143+
public Builder.Generic<Component> component(Element el, String... prop) {
144+
return new Builder.Generic<Component>(el, prop) {
145+
@Override
146+
protected Component parse(Node node) throws InvalidXMLException {
147+
return XMLUtils.parseFormattedText(node);
148+
}
149+
};
150+
}
151+
152+
public Builder.Generic<TextColor> textColor(Element el, String... prop) {
153+
return new Builder.Generic<TextColor>(el, prop) {
154+
@Override
155+
protected TextColor parse(Node node) throws InvalidXMLException {
156+
return TextFormatter.convert(XMLUtils.parseChatColor(node));
157+
}
158+
};
159+
}
160+
140161
public Builder.Generic<Kit> kit(Element el, String... prop) {
141162
return new Builder.Generic<>(el, prop) {
142163
@Override

0 commit comments

Comments
 (0)