forked from SteamWar/SteamWar
Add Advancement
Add Advancements Add AdvancementsManager Add SelectAdvancementTabPacket Add item.json Update ConnectionListener
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* 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 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.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class Advancement {
|
||||
|
||||
{
|
||||
Advancements.all.add(this);
|
||||
}
|
||||
|
||||
protected final Map<SteamwarUser, Advancement.Data> data = 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public abstract boolean hidden(Data data);
|
||||
}
|
||||
|
||||
@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 static final JsonObject ITEMS = new Gson().fromJson(new BufferedReader(new InputStreamReader(Advancement.class.getResourceAsStream("/de/steamwar/velocitycore/advancements/item.json"))), JsonObject.class);
|
||||
|
||||
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.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,85 @@
|
||||
/*
|
||||
* 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 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<>();
|
||||
|
||||
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,
|
||||
0f
|
||||
),
|
||||
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,
|
||||
0f
|
||||
),
|
||||
Advancement.HidePolicy.PREVIOUS_UNFINISHED,
|
||||
Math.min(neededPlayTime, 100),
|
||||
user -> {
|
||||
double playtime = user.getOnlinetime();
|
||||
playtime += Instant.now().getEpochSecond() - Storage.sessions.get(Chatter.of(user).getPlayer()).toInstant().getEpochSecond();
|
||||
playtime /= 60d * 60d;
|
||||
|
||||
if (playtime > neededPlayTime) return Math.min(neededPlayTime, 100);
|
||||
return (int) (playtime / (neededPlayTime / 100));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.network.ProtocolVersion;
|
||||
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.VelocityCore;
|
||||
import de.steamwar.velocitycore.listeners.BasicListener;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Linked
|
||||
public class AdvancementsManager extends BasicListener {
|
||||
|
||||
private static SelectAdvancementTabPacket selectAdvancementTabPacket;
|
||||
|
||||
static {
|
||||
selectAdvancementTabPacket = new SelectAdvancementTabPacket(Optional.of("steamwar:advancements/root"));
|
||||
|
||||
try {
|
||||
StateRegistry.PacketRegistry.ProtocolRegistry registry = StateRegistry.PLAY.getProtocolRegistry(ProtocolUtils.Direction.CLIENTBOUND, ProtocolVersion.MINECRAFT_1_21_6);
|
||||
Field field = StateRegistry.PacketRegistry.ProtocolRegistry.class.getDeclaredField("packetClassToId");
|
||||
field.setAccessible(true);
|
||||
Object2IntMap<Class<? extends MinecraftPacket>> map = (Object2IntMap) field.get(registry);
|
||||
map.put(SelectAdvancementTabPacket.class, 0x4E);
|
||||
map.put(Advancement.Packet.class, 0x7B);
|
||||
} catch (Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(priority = -1000)
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
((ConnectedPlayer) event.getPlayer()).getConnection().write(selectAdvancementTabPacket);
|
||||
for (Advancement advancement : Advancements.getAll()) {
|
||||
advancement.get(SteamwarUser.get(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onDisconnect(DisconnectEvent event) {
|
||||
for (Advancement advancement : Advancements.getAll()) {
|
||||
advancement.data.remove(SteamwarUser.get(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user