parent
621c006590
commit
961e4bc375
8 changed files with 290 additions and 39 deletions
7
DEVELOPMENT.md
Normal file
7
DEVELOPMENT.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
To see Minecraft source code: https://git.m724.eu/Minecon724/fabric-template-minimal-source
|
||||||
|
|
||||||
|
To setup NMS:
|
||||||
|
1. Download BuildTools, move it into an empty directory and open terminal
|
||||||
|
2. ```
|
||||||
|
java -jar BuildTools.jar --rev 1.21.1 --remapped
|
||||||
|
```
|
66
pom.xml
66
pom.xml
|
@ -12,6 +12,7 @@
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<project.spigot.version>1.21.1-R0.1-SNAPSHOT</project.spigot.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -54,6 +55,39 @@
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</plugin>
|
||||||
<!-- end of this -->
|
<!-- end of this -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>net.md-5</groupId>
|
||||||
|
<artifactId>specialsource-maven-plugin</artifactId>
|
||||||
|
<version>2.0.3</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>remap</goal>
|
||||||
|
</goals>
|
||||||
|
<id>remap-obf</id>
|
||||||
|
<configuration>
|
||||||
|
<srgIn>org.spigotmc:minecraft-server:${project.spigot.version}:txt:maps-mojang</srgIn>
|
||||||
|
<reverse>true</reverse>
|
||||||
|
<remappedDependencies>org.spigotmc:spigot:${project.spigot.version}:jar:remapped-mojang</remappedDependencies>
|
||||||
|
<remappedArtifactAttached>true</remappedArtifactAttached>
|
||||||
|
<remappedClassifierName>remapped-obf-temp-dont-use</remappedClassifierName>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>remap</goal>
|
||||||
|
</goals>
|
||||||
|
<id>remap-spigot</id>
|
||||||
|
<configuration>
|
||||||
|
<inputFile>${project.build.directory}/${project.artifactId}-${project.version}-remapped-obf-temp-dont-use.jar</inputFile>
|
||||||
|
<srgIn>org.spigotmc:minecraft-server:${project.spigot.version}:csrg:maps-spigot</srgIn>
|
||||||
|
<remappedDependencies>org.spigotmc:spigot:${project.spigot.version}:jar:remapped-obf</remappedDependencies>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
@ -66,13 +100,37 @@
|
||||||
<id>dmulloy2-repo</id>
|
<id>dmulloy2-repo</id>
|
||||||
<url>https://repo.dmulloy2.net/repository/public/</url>
|
<url>https://repo.dmulloy2.net/repository/public/</url>
|
||||||
</repository>
|
</repository>
|
||||||
|
<!-- this repo has no ipv6 so keep that in mind -->
|
||||||
|
<repository>
|
||||||
|
<id>maxhenkel-repo</id>
|
||||||
|
<url>https://maven.maxhenkel.de/repository/public</url>
|
||||||
|
</repository>
|
||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.spigotmc</groupId>
|
<groupId>org.spigotmc</groupId>
|
||||||
<artifactId>spigot-api</artifactId>
|
<artifactId>spigot-api</artifactId>
|
||||||
<version>1.21.1-R0.1-SNAPSHOT</version>
|
<version>${project.spigot.version}</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.spigotmc</groupId>
|
||||||
|
<artifactId>spigot</artifactId>
|
||||||
|
<version>${project.spigot.version}</version>
|
||||||
|
<classifier>remapped-mojang</classifier>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.comphenix.protocol</groupId>
|
||||||
|
<artifactId>ProtocolLib</artifactId>
|
||||||
|
<version>5.3.0</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.maxhenkel.voicechat</groupId>
|
||||||
|
<artifactId>voicechat-api</artifactId>
|
||||||
|
<version>2.5.0</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
@ -81,11 +139,5 @@
|
||||||
<version>24.1.0</version>
|
<version>24.1.0</version>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>com.comphenix.protocol</groupId>
|
|
||||||
<artifactId>ProtocolLib</artifactId>
|
|
||||||
<version>5.3.0</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
|
@ -3,11 +3,14 @@ package eu.m724.tweaks;
|
||||||
import eu.m724.tweaks.chat.ChatCommands;
|
import eu.m724.tweaks.chat.ChatCommands;
|
||||||
import eu.m724.tweaks.chat.ChatManager;
|
import eu.m724.tweaks.chat.ChatManager;
|
||||||
import eu.m724.tweaks.door.DoorListener;
|
import eu.m724.tweaks.door.DoorListener;
|
||||||
|
import eu.m724.tweaks.motd.MotdListener;
|
||||||
import eu.m724.tweaks.ping.F3NameListener;
|
import eu.m724.tweaks.ping.F3NameListener;
|
||||||
import eu.m724.tweaks.ping.PingChecker;
|
import eu.m724.tweaks.ping.PingChecker;
|
||||||
import eu.m724.tweaks.ping.PingCommands;
|
import eu.m724.tweaks.ping.PingCommands;
|
||||||
|
import eu.m724.tweaks.player.MusicPlayer;
|
||||||
import org.bukkit.plugin.java.JavaPlugin;
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class TweaksPlugin extends JavaPlugin {
|
public class TweaksPlugin extends JavaPlugin {
|
||||||
|
@ -26,5 +29,18 @@ public class TweaksPlugin extends JavaPlugin {
|
||||||
new PingChecker(this).init();
|
new PingChecker(this).init();
|
||||||
Objects.requireNonNull(getCommand("ping")).setExecutor(new PingCommands());
|
Objects.requireNonNull(getCommand("ping")).setExecutor(new PingCommands());
|
||||||
Objects.requireNonNull(getCommand("dkick")).setExecutor(new PingCommands());
|
Objects.requireNonNull(getCommand("dkick")).setExecutor(new PingCommands());
|
||||||
|
|
||||||
|
if (getServer().getPluginManager().getPlugin("voicechat") != null) {
|
||||||
|
new MusicPlayer(this).init();
|
||||||
|
} else {
|
||||||
|
getLogger().warning("To use voice extensions, install \"Simple Voice Chat\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
new MotdListener("example").init(this);
|
||||||
|
} catch (IOException e) {
|
||||||
|
getLogger().severe("Failed to initialize MOTD extension");
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
87
src/main/java/eu/m724/tweaks/motd/MotdListener.java
Normal file
87
src/main/java/eu/m724/tweaks/motd/MotdListener.java
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package eu.m724.tweaks.motd;
|
||||||
|
|
||||||
|
import com.comphenix.protocol.PacketType;
|
||||||
|
import com.comphenix.protocol.ProtocolLibrary;
|
||||||
|
import com.comphenix.protocol.events.*;
|
||||||
|
import com.comphenix.protocol.reflect.StructureModifier;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.chat.ComponentSerializer;
|
||||||
|
import net.minecraft.SharedConstants;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.network.protocol.status.ServerStatus;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
|
public class MotdListener {
|
||||||
|
private final String motdSetName;
|
||||||
|
|
||||||
|
private Component[] motds;
|
||||||
|
|
||||||
|
public MotdListener(String motdSetName) {
|
||||||
|
this.motdSetName = motdSetName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(Plugin plugin) throws IOException {
|
||||||
|
File motdSetsFile = new File(plugin.getDataFolder() + "/motd sets/" + motdSetName + ".txt");
|
||||||
|
|
||||||
|
// if the directory didn't exist create example motd sets
|
||||||
|
if (motdSetsFile.getParentFile().mkdirs()) {
|
||||||
|
plugin.saveResource("motd sets/" + motdSetName + ".txt", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
String fileContent = Files.readString(motdSetsFile.toPath());
|
||||||
|
// MOTDs are split with an empty line
|
||||||
|
motds = Arrays.stream(fileContent.split("\n\n"))
|
||||||
|
.map(s -> {
|
||||||
|
JsonElement json = ComponentSerializer.toJson(TextComponent.fromLegacy(s.strip()));
|
||||||
|
return Component.Serializer.fromJson(json, RegistryAccess.EMPTY);
|
||||||
|
})
|
||||||
|
.toArray(Component[]::new);
|
||||||
|
|
||||||
|
plugin.getLogger().info("Loaded %d MOTDs".formatted(motds.length));
|
||||||
|
|
||||||
|
registerListener(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerListener(Plugin plugin) {
|
||||||
|
ProtocolLibrary.getProtocolManager().addPacketListener(new PacketAdapter(
|
||||||
|
plugin,
|
||||||
|
ListenerPriority.NORMAL,
|
||||||
|
PacketType.Status.Server.SERVER_INFO
|
||||||
|
) {
|
||||||
|
@Override
|
||||||
|
public void onPacketSending(PacketEvent event) {
|
||||||
|
PacketContainer packet = event.getPacket();
|
||||||
|
|
||||||
|
Component motd = motds[ThreadLocalRandom.current().nextInt(motds.length)];
|
||||||
|
|
||||||
|
ServerStatus serverStatus = (ServerStatus) packet.getStructures().read(0).getHandle();
|
||||||
|
|
||||||
|
/* this:
|
||||||
|
* removes server mod prefix (Paper, Spigot, any brand)
|
||||||
|
* hides players
|
||||||
|
*/
|
||||||
|
ServerStatus newStatus = new ServerStatus(
|
||||||
|
motd,
|
||||||
|
Optional.empty(),
|
||||||
|
Optional.of(new ServerStatus.Version(
|
||||||
|
SharedConstants.getCurrentVersion().getName(),
|
||||||
|
SharedConstants.getProtocolVersion()
|
||||||
|
)),
|
||||||
|
serverStatus.favicon(),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
packet.getStructures().write(0, new InternalStructure(newStatus, new StructureModifier<>(ServerStatus.class)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,26 +4,16 @@ import com.comphenix.protocol.PacketType;
|
||||||
import com.comphenix.protocol.ProtocolLibrary;
|
import com.comphenix.protocol.ProtocolLibrary;
|
||||||
import com.comphenix.protocol.events.*;
|
import com.comphenix.protocol.events.*;
|
||||||
import com.comphenix.protocol.reflect.StructureModifier;
|
import com.comphenix.protocol.reflect.StructureModifier;
|
||||||
|
import net.minecraft.network.protocol.common.custom.BrandPayload;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
import org.bukkit.plugin.Plugin;
|
import org.bukkit.plugin.Plugin;
|
||||||
import org.bukkit.scheduler.BukkitRunnable;
|
import org.bukkit.scheduler.BukkitRunnable;
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
|
|
||||||
public class F3NameListener {
|
public class F3NameListener {
|
||||||
private final Plugin plugin;
|
private final Plugin plugin;
|
||||||
private final Class<?> brandPayloadClass;
|
|
||||||
private final Constructor<?> brandPayloadConstructor;
|
|
||||||
|
|
||||||
public F3NameListener(Plugin plugin) {
|
public F3NameListener(Plugin plugin) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
try {
|
|
||||||
this.brandPayloadClass = Class.forName("net.minecraft.network.protocol.common.custom.BrandPayload");
|
|
||||||
this.brandPayloadConstructor = brandPayloadClass.getConstructor(String.class);
|
|
||||||
} catch (ClassNotFoundException | NoSuchMethodException e) {
|
|
||||||
throw new BrandPayloadReflectionException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
|
@ -57,34 +47,27 @@ public class F3NameListener {
|
||||||
) {
|
) {
|
||||||
@Override
|
@Override
|
||||||
public void onPacketSending(PacketEvent event) {
|
public void onPacketSending(PacketEvent event) {
|
||||||
try {
|
|
||||||
PacketContainer packet = event.getPacket();
|
PacketContainer packet = event.getPacket();
|
||||||
Object brandPayload = brandPayloadConstructor.newInstance("wait");
|
|
||||||
InternalStructure structure = new InternalStructure(brandPayload, new StructureModifier<>(brandPayloadClass));
|
InternalStructure structure = new InternalStructure(
|
||||||
|
new BrandPayload("wait"),
|
||||||
|
new StructureModifier<>(BrandPayload.class)
|
||||||
|
);
|
||||||
|
|
||||||
packet.getStructures().write(0, structure);
|
packet.getStructures().write(0, structure);
|
||||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
|
||||||
throw new BrandPayloadReflectionException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changeBrand(Player player, String brand) {
|
private void changeBrand(Player player, String brand) {
|
||||||
PacketContainer packet = new PacketContainer(PacketType.Play.Server.CUSTOM_PAYLOAD);
|
PacketContainer packet = new PacketContainer(PacketType.Play.Server.CUSTOM_PAYLOAD);
|
||||||
Object brandPayload;
|
|
||||||
try {
|
InternalStructure structure = new InternalStructure(
|
||||||
brandPayload = brandPayloadConstructor.newInstance(brand);
|
new BrandPayload(brand),
|
||||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
new StructureModifier<>(BrandPayload.class)
|
||||||
throw new BrandPayloadReflectionException(e);
|
);
|
||||||
}
|
|
||||||
InternalStructure structure = new InternalStructure(brandPayload, new StructureModifier<>(brandPayloadClass));
|
|
||||||
packet.getStructures().write(0, structure);
|
packet.getStructures().write(0, structure);
|
||||||
ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet);
|
ProtocolLibrary.getProtocolManager().sendServerPacket(player, packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class BrandPayloadReflectionException extends RuntimeException {
|
|
||||||
public BrandPayloadReflectionException(Exception e) {
|
|
||||||
super(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
70
src/main/java/eu/m724/tweaks/player/MusicPlayer.java
Normal file
70
src/main/java/eu/m724/tweaks/player/MusicPlayer.java
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package eu.m724.tweaks.player;
|
||||||
|
|
||||||
|
import de.maxhenkel.voicechat.api.BukkitVoicechatService;
|
||||||
|
import de.maxhenkel.voicechat.api.VoicechatServerApi;
|
||||||
|
import de.maxhenkel.voicechat.api.VolumeCategory;
|
||||||
|
import de.maxhenkel.voicechat.api.audiochannel.AudioPlayer;
|
||||||
|
import de.maxhenkel.voicechat.api.audiochannel.EntityAudioChannel;
|
||||||
|
import de.maxhenkel.voicechat.api.opus.OpusEncoderMode;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
|
||||||
|
import javax.sound.sampled.AudioInputStream;
|
||||||
|
import javax.sound.sampled.AudioSystem;
|
||||||
|
import javax.sound.sampled.UnsupportedAudioFileException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class MusicPlayer {
|
||||||
|
private static final String PLAYER_CATEGORY = "music_player";
|
||||||
|
|
||||||
|
private VoicechatServerApi voicechat = null;
|
||||||
|
|
||||||
|
private final Plugin plugin;
|
||||||
|
|
||||||
|
public MusicPlayer(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
BukkitVoicechatService service = plugin.getServer().getServicesManager().load(BukkitVoicechatService.class);
|
||||||
|
service.registerPlugin(new MyVoicechatPlugin(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
void unlock(VoicechatServerApi voicechat) {
|
||||||
|
VolumeCategory category = voicechat.volumeCategoryBuilder()
|
||||||
|
.setId(PLAYER_CATEGORY)
|
||||||
|
.setName("Music players")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
voicechat.registerVolumeCategory(category);
|
||||||
|
|
||||||
|
this.voicechat = voicechat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void create(Player player) {
|
||||||
|
UUID channelID = UUID.randomUUID();
|
||||||
|
EntityAudioChannel channel = voicechat.createEntityAudioChannel(channelID, voicechat.fromEntity(player));
|
||||||
|
|
||||||
|
channel.setCategory(PLAYER_CATEGORY);
|
||||||
|
channel.setDistance(10);
|
||||||
|
|
||||||
|
short[] arr;
|
||||||
|
try {
|
||||||
|
AudioInputStream audio = AudioSystem.getAudioInputStream(plugin.getResource("music.flac"));
|
||||||
|
int samples = (int) (audio.available() / audio.getFrameLength());
|
||||||
|
arr = new short[samples];
|
||||||
|
for (int i=0; i<audio.available(); i++) {
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (UnsupportedAudioFileException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioPlayer audioPlayer = voicechat.createAudioPlayer(channel, voicechat.createEncoder(OpusEncoderMode.AUDIO), arr);
|
||||||
|
audioPlayer.startPlaying();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
35
src/main/java/eu/m724/tweaks/player/MyVoicechatPlugin.java
Normal file
35
src/main/java/eu/m724/tweaks/player/MyVoicechatPlugin.java
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package eu.m724.tweaks.player;
|
||||||
|
|
||||||
|
import de.maxhenkel.voicechat.api.VoicechatApi;
|
||||||
|
import de.maxhenkel.voicechat.api.VoicechatPlugin;
|
||||||
|
import de.maxhenkel.voicechat.api.VoicechatServerApi;
|
||||||
|
import de.maxhenkel.voicechat.api.events.EventRegistration;
|
||||||
|
import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent;
|
||||||
|
|
||||||
|
public class MyVoicechatPlugin implements VoicechatPlugin {
|
||||||
|
private final MusicPlayer musicPlayer;
|
||||||
|
|
||||||
|
MyVoicechatPlugin(MusicPlayer musicPlayer) {
|
||||||
|
this.musicPlayer = musicPlayer;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getPluginId() {
|
||||||
|
return "tweaks724";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(VoicechatApi api) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerEvents(EventRegistration registration) {
|
||||||
|
registration.registerEvent(VoicechatServerStartedEvent.class, this::onServerStarted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onServerStarted(VoicechatServerStartedEvent event) {
|
||||||
|
VoicechatServerApi api = event.getVoicechat();
|
||||||
|
musicPlayer.unlock(api);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ version: ${project.version}
|
||||||
main: eu.m724.tweaks.TweaksPlugin
|
main: eu.m724.tweaks.TweaksPlugin
|
||||||
api-version: 1.21.1
|
api-version: 1.21.1
|
||||||
depend: [ProtocolLib]
|
depend: [ProtocolLib]
|
||||||
|
softdepend: [voicechat]
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
chat:
|
chat:
|
||||||
|
|
Loading…
Reference in a new issue