diff --git a/CommonCore/SQL/src/de/steamwar/sql/CheckedSchematic.kt b/CommonCore/SQL/src/de/steamwar/sql/CheckedSchematic.kt index a7b0104c..a8da2737 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/CheckedSchematic.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/CheckedSchematic.kt @@ -86,6 +86,24 @@ class CheckedSchematic(id: EntityID) : CompositeEntity(id) { useDb { 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 }) diff --git a/CommonCore/SQL/src/de/steamwar/sql/EventFight.kt b/CommonCore/SQL/src/de/steamwar/sql/EventFight.kt index ae214915..bdbce609 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/EventFight.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/EventFight.kt @@ -130,6 +130,43 @@ class EventFight(id: EntityID) : IntEntity(id), Comparable { } ) } + + @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 }) diff --git a/CommonCore/SQL/src/de/steamwar/sql/FightPlayer.kt b/CommonCore/SQL/src/de/steamwar/sql/FightPlayer.kt index 626440fb..23ff78a0 100644 --- a/CommonCore/SQL/src/de/steamwar/sql/FightPlayer.kt +++ b/CommonCore/SQL/src/de/steamwar/sql/FightPlayer.kt @@ -20,9 +20,12 @@ package de.steamwar.sql 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.CompositeIdTable 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.dao.CompositeEntity import org.jetbrains.exposed.v1.dao.CompositeEntityClass @@ -69,6 +72,28 @@ class FightPlayer(id: EntityID) : CompositeEntity(id) { useDb { 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 }) diff --git a/VelocityCore/Dependencies/src/de/steamwar/velocitycore/advancements/Items.java b/VelocityCore/Dependencies/src/de/steamwar/velocitycore/advancements/Items.java new file mode 100644 index 00000000..617c2902 --- /dev/null +++ b/VelocityCore/Dependencies/src/de/steamwar/velocitycore/advancements/Items.java @@ -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 . + */ + +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); + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/advancements/Advancement.java b/VelocityCore/src/de/steamwar/velocitycore/advancements/Advancement.java new file mode 100644 index 00000000..b1a2befb --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/advancements/Advancement.java @@ -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 . + */ + +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> values = new HashMap<>(); + + { + Advancements.all.add(this); + } + + protected final Map data = new HashMap<>(); + protected final Map value = new HashMap<>(); + + public Advancement.Data get(SteamwarUser user) { + return get(user, Data::new); + } + + public Advancement.Data get(SteamwarUser user, BiFunction function) { + if (data.containsKey(user)) return data.get(user); + return function.apply(this, user); + } + + private final String identifier; + private final Optional parent; + private final Display display; + + private final HidePolicy hidePolicy; + private final int total; + + private final Function 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 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 { + + @AllArgsConstructor + public static class Key { + public static final List keys = new ArrayList<>(); + + { + keys.add(this); + } + + private final Function 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 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 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 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; + } + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/advancements/Advancements.java b/VelocityCore/src/de/steamwar/velocitycore/advancements/Advancements.java new file mode 100644 index 00000000..4742d266 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/advancements/Advancements.java @@ -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 . + */ + +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 all = new ArrayList<>(); + + @Getter + private static final List playtime = new ArrayList<>(); + + public static final Advancement.Value.Key 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 FIGHT_COUNT = new Advancement.Value.Key<>(user -> { + return FightPlayer.countFights(user.getId()); + }); + + public static final Advancement.Value.Key FIGHT_COUNT_WAR_GEAR = new Advancement.Value.Key<>(user -> { + return FightPlayer.countFights(user.getId(), "WarGear"); + }); + + public static final Advancement.Value.Key FIGHT_COUNT_MINI_WAR_GEAR = new Advancement.Value.Key<>(user -> { + return FightPlayer.countFights(user.getId(), "MiniWarGear"); + }); + + public static final Advancement.Value.Key FIGHT_COUNT_WAR_SHIP = new Advancement.Value.Key<>(user -> { + return FightPlayer.countFights(user.getId(), "WarShip"); + }); + + public static final Advancement.Value.Key EVENT_FIGHT_COUNT = new Advancement.Value.Key<>(user -> { + return EventFight.countEventFights(user); + }); + + public static final Advancement.Value.Key EVENT_FIGHT_FIRST_PLACE_COUNT = new Advancement.Value.Key<>(user -> { + return EventFight.countPlacement(user, 1); + }); + + public static final Advancement.Value.Key EVENT_FIGHT_SECOND_PLACE_COUNT = new Advancement.Value.Key<>(user -> { + return EventFight.countPlacement(user, 2); + }); + + public static final Advancement.Value.Key EVENT_FIGHT_THIRDPLACE_COUNT = new Advancement.Value.Key<>(user -> { + return EventFight.countPlacement(user, 3); + }); + + public static final Advancement.Value.Key CHECKED_SCHEMATIC_COUNT = new Advancement.Value.Key<>(user -> { + return CheckedSchematic.countChecked(user); + }); + + public static final Advancement.Value.Key ACCEPTED_SCHEMATIC_COUNT = new Advancement.Value.Key<>(user -> { + return CheckedSchematic.countAccepted(user); + }); + + public static final Advancement.Value.Key ACCEPTED_SCHEMATIC_COUNT_WAR_GEAR = new Advancement.Value.Key<>(user -> { + return CheckedSchematic.countAccepted(user, "WarGear"); + }); + + public static final Advancement.Value.Key ACCEPTED_SCHEMATIC_COUNT_MINI_WAR_GEAR = new Advancement.Value.Key<>(user -> { + return CheckedSchematic.countAccepted(user, "MiniWarGear"); + }); + + public static final Advancement.Value.Key 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 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 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 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) + ); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/advancements/AdvancementsManager.java b/VelocityCore/src/de/steamwar/velocitycore/advancements/AdvancementsManager.java new file mode 100644 index 00000000..14598894 --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/advancements/AdvancementsManager.java @@ -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 . + */ + +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> 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); + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/advancements/SelectAdvancementTabPacket.java b/VelocityCore/src/de/steamwar/velocitycore/advancements/SelectAdvancementTabPacket.java new file mode 100644 index 00000000..b344f62d --- /dev/null +++ b/VelocityCore/src/de/steamwar/velocitycore/advancements/SelectAdvancementTabPacket.java @@ -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 . + */ + +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 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; + } +} diff --git a/VelocityCore/src/de/steamwar/velocitycore/listeners/ConnectionListener.java b/VelocityCore/src/de/steamwar/velocitycore/listeners/ConnectionListener.java index b8bffa6e..7e39c822 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/listeners/ConnectionListener.java +++ b/VelocityCore/src/de/steamwar/velocitycore/listeners/ConnectionListener.java @@ -35,6 +35,9 @@ import de.steamwar.sql.CheckedSchematic; import de.steamwar.sql.SchematicType; import de.steamwar.sql.SteamwarUser; 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.discord.DiscordBot; 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.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; @Linked public class ConnectionListener extends BasicListener { @@ -102,8 +106,12 @@ public class ConnectionListener extends BasicListener { } if (newPlayers.contains(player.getUniqueId())) { + Advancements.ROOT.get(user, (advancement, __) -> new Advancement.Data(advancement, user, 0)); Chatter.broadcast().system("JOIN_FIRST", player); newPlayers.remove(player.getUniqueId()); + VelocityCore.schedule(() -> { + Advancements.ROOT.get(user).update(); + }).delay(1, TimeUnit.SECONDS).schedule(); } if (!StreamingCommand.isNotStreaming(user)) {