Merge pull request 'Velocity core/ advancements' (#429) from VelocityCore/Advancements into main

Reviewed-on: SteamWar/SteamWar#429
This commit is contained in:
2026-06-17 10:24:21 +02:00
9 changed files with 940 additions and 0 deletions
@@ -86,6 +86,24 @@ class CheckedSchematic(id: EntityID<CompositeID>) : CompositeEntity(id) {
useDb { useDb {
find { (CheckedSchematicTable.nodeOwner eq owner.id) and (CheckedSchematicTable.seen eq false) }.orderBy(CheckedSchematicTable.endTime to SortOrder.DESC).toList() find { (CheckedSchematicTable.nodeOwner eq owner.id) and (CheckedSchematicTable.seen eq false) }.orderBy(CheckedSchematicTable.endTime to SortOrder.DESC).toList()
} }
@JvmStatic
fun countAccepted(owner: SteamwarUser) =
useDb {
find { (CheckedSchematicTable.nodeOwner eq owner.id) and (CheckedSchematicTable.declineReason eq "freigegeben") }.count()
}
@JvmStatic
fun countAccepted(owner: SteamwarUser, type: String) =
useDb {
find { (CheckedSchematicTable.nodeOwner eq owner.id) and (CheckedSchematicTable.declineReason eq "freigegeben") and (CheckedSchematicTable.nodeType like "$type%") }.count()
}
@JvmStatic
fun countChecked(validator: SteamwarUser) =
useDb {
find { CheckedSchematicTable.validator eq validator.id }.count()
}
} }
val node by CheckedSchematicTable.nodeId.transform({ it?.let { EntityID(it, SchematicNodeTable) } }, { it?.value }) val node by CheckedSchematicTable.nodeId.transform({ it?.let { EntityID(it, SchematicNodeTable) } }, { it?.value })
@@ -130,6 +130,43 @@ class EventFight(id: EntityID<Int>) : IntEntity(id), Comparable<EventFight> {
} }
) )
} }
@JvmStatic
fun countEventFights(fighter: SteamwarUser) =
useDb {
exec(
"SELECT COUNT(DISTINCT F.FightID) AS FightCount FROM FightPlayer INNER JOIN Fight F on FightPlayer.FightID = F.FightID INNER JOIN EventFight EF on F.FightID = EF.Fight WHERE UserID = ?",
args = listOf(IntegerColumnType() to fighter.id.value)
) {
if (it.next()) {
it.getLong("FightCount")
} else {
0
}
}
?: 0
}
@JvmStatic
fun countPlacement(fighter: SteamwarUser, placement: Int) =
useDb {
exec(
"""
SELECT COUNT(DISTINCT EventFight.EventID) AS PlacementCount FROM TeamTeilnahme
INNER JOIN EventFight ON EventFight.EventID = TeamTeilnahme.EventID
INNER JOIN FightPlayer ON FightPlayer.FightID = EventFight.Fight
WHERE (IF(FightPlayer.Team = 1, EventFight.TeamBlue, EventFight.TeamRed)) = TeamTeilnahme.TeamID AND UserID = ? AND Placement = ?
""".trimIndent(),
args = listOf(IntegerColumnType() to fighter.id.value, IntegerColumnType() to placement)
) {
if (it.next()) {
it.getInt("PlacementCount")
} else {
0
}
}
?: 0
}
} }
val fightID by EventFightTable.id.transform({ EntityID(it, EventFightTable) }, { it.value }) val fightID by EventFightTable.id.transform({ EntityID(it, EventFightTable) }, { it.value })
@@ -20,9 +20,12 @@
package de.steamwar.sql package de.steamwar.sql
import de.steamwar.sql.internal.useDb import de.steamwar.sql.internal.useDb
import org.jetbrains.exposed.v1.core.IntegerColumnType
import org.jetbrains.exposed.v1.core.VarCharColumnType
import org.jetbrains.exposed.v1.core.dao.id.CompositeID import org.jetbrains.exposed.v1.core.dao.id.CompositeID
import org.jetbrains.exposed.v1.core.dao.id.CompositeIdTable import org.jetbrains.exposed.v1.core.dao.id.CompositeIdTable
import org.jetbrains.exposed.v1.core.dao.id.EntityID import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.eq
import org.jetbrains.exposed.v1.core.inList import org.jetbrains.exposed.v1.core.inList
import org.jetbrains.exposed.v1.dao.CompositeEntity import org.jetbrains.exposed.v1.dao.CompositeEntity
import org.jetbrains.exposed.v1.dao.CompositeEntityClass import org.jetbrains.exposed.v1.dao.CompositeEntityClass
@@ -69,6 +72,28 @@ class FightPlayer(id: EntityID<CompositeID>) : CompositeEntity(id) {
useDb { useDb {
find { FightPlayerTable.fightId inList fightIds.toList() }.toList() find { FightPlayerTable.fightId inList fightIds.toList() }.toList()
} }
@JvmStatic
fun countFights(userId: Int) =
useDb {
find { FightPlayerTable.userId eq userId }.count()
}
@JvmStatic
fun countFights(userId: Int, type: String) =
useDb {
exec(
"SELECT COUNT(*) AS FightCount FROM FightPlayer INNER JOIN Fight F on FightPlayer.FightID = F.FightID WHERE UserID = ? AND GameMode LIKE ?",
args = listOf(IntegerColumnType() to userId, VarCharColumnType() to "$type%")
) {
if (it.next()) {
it.getInt("FightCount")
} else {
0
}
}
?: 0
}
} }
val fightID by FightPlayerTable.fightId.transform({ EntityID(it, FightTable) }, { it.value }) val fightID by FightPlayerTable.fightId.transform({ EntityID(it, FightTable) }, { it.value })
@@ -0,0 +1,43 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.velocitycore.advancements;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
public class Items {
/**
* Loaded from https://github.com/retrooper/packetevents/blob/2.0/mappings/registries/item.json
*/
public static final JsonObject values;
static {
try {
values = new Gson().fromJson(new BufferedReader(new InputStreamReader(URI.create("https://raw.githubusercontent.com/retrooper/packetevents/refs/heads/2.0/mappings/registries/item.json").toURL().openConnection().getInputStream())), JsonObject.class);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,340 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.velocitycore.advancements;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import de.steamwar.messages.Chatter;
import de.steamwar.sql.SteamwarUser;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
@RequiredArgsConstructor
public class Advancement {
protected static final Map<SteamwarUser, Map<Advancement.Value.Key, Advancement.Value>> values = new HashMap<>();
{
Advancements.all.add(this);
}
protected final Map<SteamwarUser, Advancement.Data> data = new HashMap<>();
protected final Map<SteamwarUser, Advancement.Value> value = new HashMap<>();
public Advancement.Data get(SteamwarUser user) {
return get(user, Data::new);
}
public Advancement.Data get(SteamwarUser user, BiFunction<Advancement, SteamwarUser, Data> function) {
if (data.containsKey(user)) return data.get(user);
return function.apply(this, user);
}
private final String identifier;
private final Optional<Advancement> parent;
private final Display display;
private final HidePolicy hidePolicy;
private final int total;
private final Function<SteamwarUser, Integer> progressCalculator;
@Override
public String toString() {
StringBuilder st = new StringBuilder();
st.append("Advancement(");
parent.ifPresent(advancement -> st.append(advancement.identifier).append("<-"));
st.append(identifier);
st.append(", total=").append(total);
st.append(")");
return st.toString();
}
@RequiredArgsConstructor
@AllArgsConstructor
public static class Display {
private final Component title;
private final Component description;
private final String item;
private final FrameType frameType;
private Optional<String> background = Optional.empty();
private final float xCoord;
private final float yCoord;
public enum FrameType {
TASK,
CHALLENGE,
GOAL
}
}
public enum HidePolicy {
NEVER {
@Override
public boolean hidden(Data data) {
return false;
}
},
NO_PROGRESS {
@Override
public boolean hidden(Data data) {
return data.progress == 0;
}
},
PREVIOUS_UNFINISHED {
@Override
public boolean hidden(Data data) {
if (data.advancement.parent.isPresent()) {
Advancement parent = data.advancement.parent.get();
Advancement.Data parentData = parent.get(data.user);
return parentData.progress != parentData.advancement.total;
} else {
return false;
}
}
},
WITH_PREVIOUS {
@Override
public boolean hidden(Data data) {
if (data.advancement.parent.isPresent()) {
Advancement parent = data.advancement.parent.get();
Advancement.Data parentData = parent.get(data.user);
return parentData.hidden;
} else {
return false;
}
}
},
;
public abstract boolean hidden(Data data);
}
public static class Value<T extends Number> {
@AllArgsConstructor
public static class Key<T extends Number> {
public static final List<Key> keys = new ArrayList<>();
{
keys.add(this);
}
private final Function<SteamwarUser, T> valueFunction;
private Advancement.Value get(SteamwarUser user) {
Key self = this;
return values.computeIfAbsent(user, __ -> new HashMap<>()).computeIfAbsent(self, __ -> {
Value data = new Advancement.Value();
data.update(user, self);
return data;
});
}
public Function<SteamwarUser, Integer> max(int neededValue) {
return user -> {
double value = get(user).value.doubleValue();
if (value > neededValue) return Math.min(neededValue, 100);
return (int) (value / Math.max(neededValue / 100.0, 1));
};
}
public Function<SteamwarUser, Integer> reached(int neededValue) {
return user -> {
double value = get(user).value.doubleValue();
return value >= neededValue ? 1 : 0;
};
}
}
@Getter
private T value;
public void update(SteamwarUser user, Key<T> key) {
this.value = key.valueFunction.apply(user);
}
}
@ToString
public static class Data {
private final Advancement advancement;
private final SteamwarUser user;
private int progress;
private boolean showToast = true;
private boolean hidden = false;
public Data(Advancement advancement, SteamwarUser user) {
this.advancement = advancement;
advancement.data.put(user, this);
this.user = user;
this.progress = advancement.progressCalculator.apply(user);
checkHidden();
checkFinished();
new Packet(this, showToast).send();
}
public Data(Advancement advancement, SteamwarUser user, int progress) {
this.advancement = advancement;
advancement.data.put(user, this);
this.user = user;
this.progress = progress;
checkHidden();
checkFinished();
new Packet(this, showToast).send();
}
public void update() {
this.progress = advancement.progressCalculator.apply(user);
checkHidden();
new Packet(this, showToast).send();
// Update Advancements that have this as parent
Advancements.getAll()
.stream()
.filter(advancement -> advancement.parent.filter(value -> value == this.advancement).isPresent())
.map(advancement -> advancement.get(user))
.forEach(Advancement.Data::update);
checkFinished();
}
private void checkHidden() {
hidden = advancement.hidePolicy.hidden(this);
}
private void checkFinished() {
if (progress == advancement.total) {
showToast = false;
}
}
private void encodeAdvancement(ByteBuf byteBuf, ProtocolVersion protocolVersion, boolean showToast) {
ProtocolUtils.writeString(byteBuf, advancement.identifier);
if (advancement.parent.isPresent()) {
byteBuf.writeBoolean(true);
ProtocolUtils.writeString(byteBuf, advancement.parent.get().identifier);
} else {
byteBuf.writeBoolean(false);
}
{ // Display
byteBuf.writeBoolean(true);
new ComponentHolder(protocolVersion, advancement.display.title).write(byteBuf);
new ComponentHolder(protocolVersion, advancement.display.description).write(byteBuf);
{ // Slot
ProtocolUtils.writeVarInt(byteBuf, 1);
int itemId = Items.values
.get(protocolVersion.name().replace("MINECRAFT_", "V_"))
.getAsJsonObject()
.get(advancement.display.item)
.getAsInt();
ProtocolUtils.writeVarInt(byteBuf, itemId);
ProtocolUtils.writeVarInt(byteBuf, 0);
ProtocolUtils.writeVarInt(byteBuf, 0);
}
ProtocolUtils.writeVarInt(byteBuf, advancement.display.frameType.ordinal());
if (advancement.display.background.isPresent()) {
byteBuf.writeInt(0x01 | (showToast ? 0x02 : 0x00) | (hidden ? 0x04 : 0x00));
ProtocolUtils.writeString(byteBuf, advancement.display.background.get());
} else {
byteBuf.writeInt((showToast ? 0x02 : 0x00) | (hidden ? 0x04 : 0x00));
}
byteBuf.writeFloat(advancement.display.xCoord);
byteBuf.writeFloat(advancement.display.yCoord);
}
ProtocolUtils.writeVarInt(byteBuf, advancement.total);
for (int i = 0; i < advancement.total; i++) {
ProtocolUtils.writeVarInt(byteBuf, 1);
ProtocolUtils.writeString(byteBuf, advancement.identifier + "_" + i);
}
byteBuf.writeBoolean(false); // No Telemetry
}
private void encodeProgress(ByteBuf byteBuf) {
ProtocolUtils.writeString(byteBuf, this.advancement.identifier);
ProtocolUtils.writeVarInt(byteBuf, advancement.total);
for (int i = 0; i < advancement.total; i++) {
ProtocolUtils.writeString(byteBuf, advancement.identifier + "_" + i);
if (i == advancement.total - 1 && advancement.total == progress) {
byteBuf.writeBoolean(true);
byteBuf.writeLong(new Date().getTime());
} else if (i < progress) {
byteBuf.writeBoolean(true);
byteBuf.writeLong(0);
} else {
byteBuf.writeBoolean(false);
}
}
}
}
protected record Packet(Data data, boolean showToast) implements MinecraftPacket {
public void send() {
Player player = Chatter.of(data.user).getPlayer();
((ConnectedPlayer) player).getConnection().write(this);
}
@Override
public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException();
}
@Override
public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
byteBuf.writeBoolean(false); // Clear
if (!data.hidden) {
ProtocolUtils.writeVarInt(byteBuf, 1);
data.encodeAdvancement(byteBuf, protocolVersion, showToast);
ProtocolUtils.writeVarInt(byteBuf, 0); // No Advancements to remove
ProtocolUtils.writeVarInt(byteBuf, 1);
data.encodeProgress(byteBuf);
} else {
ProtocolUtils.writeVarInt(byteBuf, 0); // No Advancements to update
ProtocolUtils.writeVarInt(byteBuf, 1);
ProtocolUtils.writeString(byteBuf, data.advancement.identifier);
ProtocolUtils.writeVarInt(byteBuf, 0); // No Advancements Progress to update
}
byteBuf.writeBoolean(true); // Show Advancements
}
@Override
public boolean handle(MinecraftSessionHandler minecraftSessionHandler) {
return false;
}
}
}
@@ -0,0 +1,315 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.velocitycore.advancements;
import de.steamwar.messages.Chatter;
import de.steamwar.persistent.Storage;
import de.steamwar.sql.CheckedSchematic;
import de.steamwar.sql.EventFight;
import de.steamwar.sql.FightPlayer;
import lombok.Getter;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.text.Component;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@UtilityClass
public class Advancements {
@Getter
static final List<Advancement> all = new ArrayList<>();
@Getter
private static final List<Advancement> playtime = new ArrayList<>();
public static final Advancement.Value.Key<Double> PLAY_TIME_KEY = new Advancement.Value.Key<>(user -> {
double playtime = user.getOnlinetime();
playtime += Instant.now().getEpochSecond() - Storage.sessions.get(Chatter.of(user).getPlayer()).toInstant().getEpochSecond();
playtime /= 60d * 60d;
return playtime;
});
public static final Advancement.Value.Key<Long> FIGHT_COUNT = new Advancement.Value.Key<>(user -> {
return FightPlayer.countFights(user.getId());
});
public static final Advancement.Value.Key<Integer> FIGHT_COUNT_WAR_GEAR = new Advancement.Value.Key<>(user -> {
return FightPlayer.countFights(user.getId(), "WarGear");
});
public static final Advancement.Value.Key<Integer> FIGHT_COUNT_MINI_WAR_GEAR = new Advancement.Value.Key<>(user -> {
return FightPlayer.countFights(user.getId(), "MiniWarGear");
});
public static final Advancement.Value.Key<Integer> FIGHT_COUNT_WAR_SHIP = new Advancement.Value.Key<>(user -> {
return FightPlayer.countFights(user.getId(), "WarShip");
});
public static final Advancement.Value.Key<Long> EVENT_FIGHT_COUNT = new Advancement.Value.Key<>(user -> {
return EventFight.countEventFights(user);
});
public static final Advancement.Value.Key<Integer> EVENT_FIGHT_FIRST_PLACE_COUNT = new Advancement.Value.Key<>(user -> {
return EventFight.countPlacement(user, 1);
});
public static final Advancement.Value.Key<Integer> EVENT_FIGHT_SECOND_PLACE_COUNT = new Advancement.Value.Key<>(user -> {
return EventFight.countPlacement(user, 2);
});
public static final Advancement.Value.Key<Integer> EVENT_FIGHT_THIRDPLACE_COUNT = new Advancement.Value.Key<>(user -> {
return EventFight.countPlacement(user, 3);
});
public static final Advancement.Value.Key<Long> CHECKED_SCHEMATIC_COUNT = new Advancement.Value.Key<>(user -> {
return CheckedSchematic.countChecked(user);
});
public static final Advancement.Value.Key<Long> ACCEPTED_SCHEMATIC_COUNT = new Advancement.Value.Key<>(user -> {
return CheckedSchematic.countAccepted(user);
});
public static final Advancement.Value.Key<Long> ACCEPTED_SCHEMATIC_COUNT_WAR_GEAR = new Advancement.Value.Key<>(user -> {
return CheckedSchematic.countAccepted(user, "WarGear");
});
public static final Advancement.Value.Key<Long> ACCEPTED_SCHEMATIC_COUNT_MINI_WAR_GEAR = new Advancement.Value.Key<>(user -> {
return CheckedSchematic.countAccepted(user, "MiniWarGear");
});
public static final Advancement.Value.Key<Long> ACCEPTED_SCHEMATIC_COUNT_WAR_SHIP = new Advancement.Value.Key<>(user -> {
return CheckedSchematic.countAccepted(user, "WarShip");
});
public static final Advancement ROOT = new Advancement(
"steamwar:advancements/root",
Optional.empty(),
new Advancement.Display(
Component.text("SteamWar"),
Component.text("Join SteamWar for the first time!"),
"cactus_flower",
Advancement.Display.FrameType.CHALLENGE,
Optional.of("minecraft:gui/advancements/backgrounds/adventure"),
0f,
3f
),
Advancement.HidePolicy.NEVER,
1,
user -> 1
);
static {
Advancement previous = ROOT;
int[] playTimes = new int[]{1, 10, 100, 500, 1000, 2500, 5000, 7500, 10000, 15000, 20000};
for (int i = 0; i < playTimes.length; i++) {
int neededPlayTime = playTimes[i];
previous = new Advancement(
"steamwar:advancements/playtime_" + neededPlayTime + "_hour",
Optional.of(previous),
new Advancement.Display(
Component.text("Play " + neededPlayTime + " Hour" + (neededPlayTime > 1 ? "s" : "")),
Component.text("Play " + neededPlayTime + " hour" + (neededPlayTime > 1 ? "s" : "") + " on SteamWar"),
"clock",
Advancement.Display.FrameType.TASK,
i + 1f,
3f
),
Advancement.HidePolicy.PREVIOUS_UNFINISHED,
Math.min(neededPlayTime, 100),
PLAY_TIME_KEY.max(neededPlayTime)
);
playtime.add(previous);
}
}
static {
Advancement previous = ROOT;
int[] fightCounts = new int[]{1, 10, 50, 100, 200, 500, 1000, 2500, 5000, 7500, 10000, 15000, 20000};
for (int i = 0; i < fightCounts.length; i++) {
int fightCount = fightCounts[i];
previous = new Advancement(
"steamwar:advancements/fights_" + fightCount,
Optional.of(previous),
new Advancement.Display(
Component.text(fightCount + " Fight" + (fightCount > 1 ? "s" : "")),
Component.text(fightCount + " Fight" + (fightCount > 1 ? "s" : "")),
"iron_sword",
Advancement.Display.FrameType.TASK,
i + 1f,
4f
),
Advancement.HidePolicy.PREVIOUS_UNFINISHED,
Math.min(fightCount, 100),
FIGHT_COUNT.max(fightCount)
);
if (i == 0) {
fightsPerType(previous, 5f, "WarGear", FIGHT_COUNT_WAR_GEAR, "stone_bricks");
fightsPerType(previous, 6f, "MiniWarGear", FIGHT_COUNT_MINI_WAR_GEAR, "stone_brick_slab");
fightsPerType(previous, 7f, "WarShip", FIGHT_COUNT_WAR_SHIP, "dark_oak_boat");
}
}
}
private static void fightsPerType(Advancement previous, float yCoord, String type, Advancement.Value.Key<Integer> typeKey, String item) {
int[] fightCounts = new int[]{1, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000};
for (int i = 0; i < fightCounts.length; i++) {
int fightCount = fightCounts[i];
previous = new Advancement(
"steamwar:advancements/fights_" + type + "_" + fightCount,
Optional.of(previous),
new Advancement.Display(
Component.text(type + " " + fightCount + " Fight" + (fightCount > 1 ? "s" : "")),
Component.text(type + " " + fightCount + " Fight" + (fightCount > 1 ? "s" : "")),
item,
Advancement.Display.FrameType.TASK,
i + 2f,
yCoord
),
i == 0 ? Advancement.HidePolicy.WITH_PREVIOUS : Advancement.HidePolicy.PREVIOUS_UNFINISHED,
Math.min(fightCount, 100),
typeKey.max(fightCount)
);
}
}
static {
Advancement previous = ROOT;
int[] eventFightCounts = new int[]{1, 5, 10, 15, 25, 50, 100, 150, 200, 250};
for (int i = 0; i < eventFightCounts.length; i++) {
int eventFightCount = eventFightCounts[i];
previous = new Advancement(
"steamwar:advancements/event_fights_" + eventFightCount,
Optional.of(previous),
new Advancement.Display(
Component.text(eventFightCount + " Event-Fight" + (eventFightCount > 1 ? "s" : "")),
Component.text(eventFightCount + " Event-Fight" + (eventFightCount > 1 ? "s" : "")),
"golden_sword",
Advancement.Display.FrameType.TASK,
i + 1f,
8f
),
Advancement.HidePolicy.PREVIOUS_UNFINISHED,
Math.min(eventFightCount, 100),
EVENT_FIGHT_COUNT.max(eventFightCount)
);
if (i == 0) {
placementsCounts(previous, 9f, 1, "gold_block", EVENT_FIGHT_FIRST_PLACE_COUNT, Advancement.Display.FrameType.CHALLENGE);
placementsCounts(previous, 10f, 2, "iron_block", EVENT_FIGHT_SECOND_PLACE_COUNT, Advancement.Display.FrameType.GOAL);
placementsCounts(previous, 11f, 3, "copper_block", EVENT_FIGHT_THIRDPLACE_COUNT, Advancement.Display.FrameType.TASK);
}
}
}
private static void placementsCounts(Advancement previous, float yCoord, int placement, String item, Advancement.Value.Key<Integer> typeKey, Advancement.Display.FrameType frameType) {
for (int placementCount = 1; placementCount <= 10; placementCount++) {
int finalPlacementCount = placementCount;
previous = new Advancement(
"steamwar:advancements/event_placement_" + placement + "_" + placementCount,
Optional.of(previous),
new Advancement.Display(
Component.text(placementCount + "x " + placement + ". Place in Event"),
Component.text(""),
item,
frameType,
2f + (placementCount - 1f),
yCoord
),
placementCount == 1 ? Advancement.HidePolicy.WITH_PREVIOUS : Advancement.HidePolicy.PREVIOUS_UNFINISHED,
1,
typeKey.reached(placementCount)
);
}
}
static {
Advancement previous = ROOT;
int[] checkedCounts = new int[]{1, 10, 100, 250, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000};
for (int i = 0; i < checkedCounts.length; i++) {
int checkedCount = checkedCounts[i];
previous = new Advancement(
"steamwar:advancements/checked_" + checkedCount,
Optional.of(previous),
new Advancement.Display(
Component.text(checkedCount + " Check Session" + (checkedCount > 1 ? "s" : "")),
Component.text(checkedCount + " Check Session" + (checkedCount > 1 ? "s" : "")),
"paper",
Advancement.Display.FrameType.TASK,
i + 1f,
0f
),
i == 0 ? Advancement.HidePolicy.NO_PROGRESS : Advancement.HidePolicy.PREVIOUS_UNFINISHED,
Math.min(checkedCount, 100),
CHECKED_SCHEMATIC_COUNT.max(checkedCount)
);
}
}
static {
Advancement previous = ROOT;
int[] acceptedCounts = new int[]{1, 5, 10, 15, 25, 50, 100, 150, 200, 250, 500, 750, 1000};
for (int i = 0; i < acceptedCounts.length; i++) {
int acceptedCount = acceptedCounts[i];
previous = new Advancement(
"steamwar:advancements/accepted_" + acceptedCount,
Optional.of(previous),
new Advancement.Display(
Component.text(acceptedCount + " Accepted Schematic" + (acceptedCount > 1 ? "s" : "")),
Component.text(acceptedCount + " Accepted Schematic" + (acceptedCount > 1 ? "s" : "")),
"cauldron",
Advancement.Display.FrameType.TASK,
i + 1f,
2f
),
Advancement.HidePolicy.PREVIOUS_UNFINISHED,
Math.min(acceptedCount, 100),
ACCEPTED_SCHEMATIC_COUNT.max(acceptedCount)
);
if (i == 0) {
acceptedPerType(previous, 2f, "WarGear", ACCEPTED_SCHEMATIC_COUNT_WAR_GEAR, "end_stone_bricks");
acceptedPerType(previous, 3f, "MiniWarGear", ACCEPTED_SCHEMATIC_COUNT_MINI_WAR_GEAR, "end_stone_brick_slab");
acceptedPerType(previous, 4f, "WarShip", ACCEPTED_SCHEMATIC_COUNT_WAR_SHIP, "oak_boat");
}
}
}
private static void acceptedPerType(Advancement previous, float xCoord, String type, Advancement.Value.Key<Long> typeKey, String item) {
new Advancement(
"steamwar:advancements/accepted_" + type,
Optional.of(previous),
new Advancement.Display(
Component.text(type + " Accepted"),
Component.text(""),
item,
Advancement.Display.FrameType.GOAL,
xCoord,
1f
),
Advancement.HidePolicy.WITH_PREVIOUS,
1,
typeKey.reached(1)
);
}
}
@@ -0,0 +1,99 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.velocitycore.advancements;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.DisconnectEvent;
import com.velocitypowered.api.event.connection.PostLoginEvent;
import com.velocitypowered.api.event.player.ServerPostConnectEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import de.steamwar.linkage.Linked;
import de.steamwar.sql.SteamwarUser;
import de.steamwar.velocitycore.listeners.BasicListener;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import java.lang.reflect.Field;
import java.util.Optional;
@Linked
public class AdvancementsManager extends BasicListener {
private static SelectAdvancementTabPacket selectAdvancementTabPacket;
static {
selectAdvancementTabPacket = new SelectAdvancementTabPacket(Optional.of("steamwar:advancements/root"));
registerPacketId(ProtocolVersion.MINECRAFT_1_21_9, 0x53, 0x80);
registerPacketId(ProtocolVersion.MINECRAFT_1_21_7, 0x4E, 0x7B);
registerPacketId(ProtocolVersion.MINECRAFT_1_21_6, 0x4E, 0x7B);
registerPacketId(ProtocolVersion.MINECRAFT_1_21_5, 0x4E, 0x7B);
registerPacketId(ProtocolVersion.MINECRAFT_1_21_4, 0x4F, 0x7B);
}
private static void registerPacketId(ProtocolVersion version, int selectAdvancementTabPacket, int advancementPacket) {
try {
StateRegistry.PacketRegistry.ProtocolRegistry registry = StateRegistry.PLAY.getProtocolRegistry(ProtocolUtils.Direction.CLIENTBOUND, version);
Field field = StateRegistry.PacketRegistry.ProtocolRegistry.class.getDeclaredField("packetClassToId");
field.setAccessible(true);
Object2IntMap<Class<? extends MinecraftPacket>> map = (Object2IntMap) field.get(registry);
map.put(SelectAdvancementTabPacket.class, selectAdvancementTabPacket);
map.put(Advancement.Packet.class, advancementPacket);
} catch (Exception e) {
// Ignore
}
}
@Subscribe(priority = -1000)
public void onPostLogin(PostLoginEvent event) {
sendAdvancements(event.getPlayer());
}
@Subscribe(priority = -1000)
public void onServerPostConnect(ServerPostConnectEvent event) {
sendAdvancements(event.getPlayer());
}
private void sendAdvancements(Player player) {
// Only enable for 1.21.4 or higher
if (player.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_4)) {
return;
}
((ConnectedPlayer) player).getConnection().write(selectAdvancementTabPacket);
SteamwarUser user = SteamwarUser.get(player.getUniqueId());
for (Advancement advancement : Advancements.getAll()) {
advancement.get(user).update();
}
}
@Subscribe
public void onDisconnect(DisconnectEvent event) {
SteamwarUser user = SteamwarUser.get(event.getPlayer().getUniqueId());
for (Advancement advancement : Advancements.getAll()) {
advancement.data.remove(user);
}
Advancement.values.remove(user);
}
}
@@ -0,0 +1,55 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2026 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package de.steamwar.velocitycore.advancements;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import lombok.AllArgsConstructor;
import java.util.Optional;
@AllArgsConstructor
public class SelectAdvancementTabPacket implements MinecraftPacket {
private Optional<String> identifier;
@Override
public void decode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException("Packet is not implemented");
}
@Override
public void encode(ByteBuf byteBuf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
if (this.identifier.isPresent()) {
byteBuf.writeBoolean(true);
ProtocolUtils.writeString(byteBuf, this.identifier.get());
} else {
byteBuf.writeBoolean(false);
}
}
@Override
public boolean handle(MinecraftSessionHandler minecraftSessionHandler) {
return false;
}
}
@@ -35,6 +35,9 @@ import de.steamwar.sql.CheckedSchematic;
import de.steamwar.sql.SchematicType; import de.steamwar.sql.SchematicType;
import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.SteamwarUser;
import de.steamwar.sql.UserPerm; import de.steamwar.sql.UserPerm;
import de.steamwar.velocitycore.VelocityCore;
import de.steamwar.velocitycore.advancements.Advancement;
import de.steamwar.velocitycore.advancements.Advancements;
import de.steamwar.velocitycore.commands.*; import de.steamwar.velocitycore.commands.*;
import de.steamwar.velocitycore.discord.DiscordBot; import de.steamwar.velocitycore.discord.DiscordBot;
import de.steamwar.velocitycore.discord.util.DiscordRanks; import de.steamwar.velocitycore.discord.util.DiscordRanks;
@@ -45,6 +48,7 @@ import net.kyori.adventure.text.event.ClickEvent;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Linked @Linked
public class ConnectionListener extends BasicListener { public class ConnectionListener extends BasicListener {
@@ -102,8 +106,12 @@ public class ConnectionListener extends BasicListener {
} }
if (newPlayers.contains(player.getUniqueId())) { if (newPlayers.contains(player.getUniqueId())) {
Advancements.ROOT.get(user, (advancement, __) -> new Advancement.Data(advancement, user, 0));
Chatter.broadcast().system("JOIN_FIRST", player); Chatter.broadcast().system("JOIN_FIRST", player);
newPlayers.remove(player.getUniqueId()); newPlayers.remove(player.getUniqueId());
VelocityCore.schedule(() -> {
Advancements.ROOT.get(user).update();
}).delay(1, TimeUnit.SECONDS).schedule();
} }
if (!StreamingCommand.isNotStreaming(user)) { if (!StreamingCommand.isNotStreaming(user)) {