forked from ChomeNS/chipmunkmod
feat: 1.21.4 update
This commit is contained in:
parent
a9b365127f
commit
58d5af5c61
9 changed files with 15 additions and 283 deletions
build.gradlegradle.properties
src/main
19
build.gradle
19
build.gradle
|
@ -2,9 +2,6 @@ plugins {
|
||||||
id 'fabric-loom' version '1.9-SNAPSHOT'
|
id 'fabric-loom' version '1.9-SNAPSHOT'
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
|
||||||
|
|
||||||
base.archivesName = project.archives_base_name
|
base.archivesName = project.archives_base_name
|
||||||
version = project.mod_version
|
version = project.mod_version
|
||||||
group = project.maven_group
|
group = project.maven_group
|
||||||
|
@ -22,15 +19,10 @@ dependencies {
|
||||||
// Fabric API. This is technically optional, but you probably want it anyway.
|
// Fabric API. This is technically optional, but you probably want it anyway.
|
||||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||||
|
|
||||||
include(modImplementation("net.kyori:adventure-platform-fabric:5.14.1")) // for Minecraft 1.21-1.21.1
|
include(modImplementation("net.kyori:adventure-platform-fabric:6.2.0"))
|
||||||
include(implementation("net.kyori:adventure-text-serializer-gson:4.17.0"))
|
include(implementation("net.kyori:adventure-text-serializer-gson:4.18.0"))
|
||||||
include(implementation("net.kyori:adventure-text-serializer-legacy:4.17.0"))
|
include(implementation("net.kyori:adventure-text-serializer-legacy:4.18.0"))
|
||||||
include(implementation("org.luaj:luaj-jse:3.0.1"))
|
include(implementation("org.luaj:luaj-jse:3.0.1"))
|
||||||
|
|
||||||
// Uncomment the following line to enable the deprecated Fabric API modules.
|
|
||||||
// These are included in the Fabric API production distribution and allow you to update your mod to the latest modules at a later more convenient time.
|
|
||||||
|
|
||||||
// modImplementation "net.fabricmc.fabric-api:fabric-api-deprecated:${project.fabric_version}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
processResources {
|
processResources {
|
||||||
|
@ -44,10 +36,13 @@ processResources {
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
// Minecraft 1.18 (1.18-pre2) upwards uses Java 17.
|
// Minecraft 1.18 (1.18-pre2) upwards uses Java 17.
|
||||||
it.options.release = 21
|
it.options.release = 21
|
||||||
|
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
from("LICENSE") {
|
from("LICENSE") {
|
||||||
rename { "${it}_${project.archivesBaseName}"}
|
rename { "${it}_${base.archivesName}"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ org.gradle.parallel=true
|
||||||
|
|
||||||
# Fabric Properties
|
# Fabric Properties
|
||||||
# check these on https://fabricmc.net/develop
|
# check these on https://fabricmc.net/develop
|
||||||
minecraft_version=1.21.1
|
minecraft_version=1.21.4
|
||||||
yarn_mappings=1.21.1+build.3
|
yarn_mappings=1.21.4+build.2
|
||||||
loader_version=0.16.5
|
loader_version=0.16.9
|
||||||
|
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version = 1.0.0
|
mod_version = 1.0.0
|
||||||
|
@ -14,6 +14,6 @@ org.gradle.parallel=true
|
||||||
archives_base_name = chipmunkmod
|
archives_base_name = chipmunkmod
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
fabric_version=0.105.0+1.21.1
|
fabric_version=0.113.0+1.21.4
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package land.chipmunk.chipmunkmod;
|
||||||
|
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import land.chipmunk.chipmunkmod.modules.KaboomCheck;
|
import land.chipmunk.chipmunkmod.modules.KaboomCheck;
|
||||||
import land.chipmunk.chipmunkmod.modules.Players;
|
|
||||||
import land.chipmunk.chipmunkmod.modules.SelfCare;
|
import land.chipmunk.chipmunkmod.modules.SelfCare;
|
||||||
import land.chipmunk.chipmunkmod.util.gson.BlockPosTypeAdapter;
|
import land.chipmunk.chipmunkmod.util.gson.BlockPosTypeAdapter;
|
||||||
import net.fabricmc.api.ModInitializer;
|
import net.fabricmc.api.ModInitializer;
|
||||||
|
@ -45,7 +44,6 @@ public class ChipmunkMod implements ModInitializer {
|
||||||
throw new RuntimeException("Could not load the config", exception);
|
throw new RuntimeException("Could not load the config", exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
Players.INSTANCE.init();
|
|
||||||
KaboomCheck.INSTANCE.init();
|
KaboomCheck.INSTANCE.init();
|
||||||
SelfCare.INSTANCE.init();
|
SelfCare.INSTANCE.init();
|
||||||
|
|
||||||
|
|
|
@ -28,13 +28,6 @@ public class ClientConnectionMixin {
|
||||||
@Unique
|
@Unique
|
||||||
private static final Pattern CUSTOM_PITCH_PATTERN = Pattern.compile(".*\\.pitch\\.(.*)");
|
private static final Pattern CUSTOM_PITCH_PATTERN = Pattern.compile(".*\\.pitch\\.(.*)");
|
||||||
|
|
||||||
@Inject(at = @At("HEAD"), method = "disconnect", cancellable = true)
|
|
||||||
public void disconnect (Text disconnectReason, CallbackInfo ci) {
|
|
||||||
if (disconnectReason == ClientPlayNetworkHandlerAccessor.chatValidationFailedText()) {
|
|
||||||
ci.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Inject(method = "exceptionCaught", at = @At("HEAD"), cancellable = true)
|
@Inject(method = "exceptionCaught", at = @At("HEAD"), cancellable = true)
|
||||||
private void exceptionCaught (ChannelHandlerContext context, Throwable ex, CallbackInfo ci) {
|
private void exceptionCaught (ChannelHandlerContext context, Throwable ex, CallbackInfo ci) {
|
||||||
ci.cancel();
|
ci.cancel();
|
||||||
|
@ -60,7 +53,7 @@ public class ClientConnectionMixin {
|
||||||
} else if (packet instanceof PlaySoundS2CPacket t_packet) {
|
} else if (packet instanceof PlaySoundS2CPacket t_packet) {
|
||||||
final SoundEvent soundEvent = t_packet.getSound().value();
|
final SoundEvent soundEvent = t_packet.getSound().value();
|
||||||
|
|
||||||
final Identifier sound = soundEvent.getId();
|
final Identifier sound = soundEvent.id();
|
||||||
|
|
||||||
final Matcher matcher = CUSTOM_PITCH_PATTERN.matcher(sound.getPath());
|
final Matcher matcher = CUSTOM_PITCH_PATTERN.matcher(sound.getPath());
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
package land.chipmunk.chipmunkmod.mixin;
|
package land.chipmunk.chipmunkmod.mixin;
|
||||||
|
|
||||||
import net.minecraft.client.network.PlayerListEntry;
|
import net.minecraft.client.network.PlayerListEntry;
|
||||||
import net.minecraft.network.ClientConnection;
|
|
||||||
import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket;
|
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||||
import net.minecraft.text.Text;
|
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -13,9 +10,6 @@ import java.util.UUID;
|
||||||
|
|
||||||
@Mixin(net.minecraft.client.network.ClientPlayNetworkHandler.class)
|
@Mixin(net.minecraft.client.network.ClientPlayNetworkHandler.class)
|
||||||
public interface ClientPlayNetworkHandlerAccessor {
|
public interface ClientPlayNetworkHandlerAccessor {
|
||||||
@Accessor("CHAT_VALIDATION_FAILED_TEXT")
|
|
||||||
static Text chatValidationFailedText () { throw new AssertionError(); }
|
|
||||||
|
|
||||||
@Accessor("playerListEntries")
|
@Accessor("playerListEntries")
|
||||||
Map<UUID, PlayerListEntry> playerListEntries();
|
Map<UUID, PlayerListEntry> playerListEntries();
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,6 @@ import land.chipmunk.chipmunkmod.command.CommandManager;
|
||||||
import land.chipmunk.chipmunkmod.listeners.Listener;
|
import land.chipmunk.chipmunkmod.listeners.Listener;
|
||||||
import land.chipmunk.chipmunkmod.listeners.ListenerManager;
|
import land.chipmunk.chipmunkmod.listeners.ListenerManager;
|
||||||
import land.chipmunk.chipmunkmod.modules.*;
|
import land.chipmunk.chipmunkmod.modules.*;
|
||||||
import net.kyori.adventure.platform.fabric.FabricAudiences;
|
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import net.kyori.adventure.text.TextComponent;
|
|
||||||
import net.minecraft.client.MinecraftClient;
|
import net.minecraft.client.MinecraftClient;
|
||||||
import net.minecraft.command.CommandRegistryAccess;
|
import net.minecraft.command.CommandRegistryAccess;
|
||||||
import net.minecraft.network.encryption.NetworkEncryptionUtils;
|
import net.minecraft.network.encryption.NetworkEncryptionUtils;
|
||||||
|
|
|
@ -88,7 +88,7 @@ public class CustomChat {
|
||||||
try {
|
try {
|
||||||
final Matcher racistMatcher = RACIST_PATTERN.matcher(message);
|
final Matcher racistMatcher = RACIST_PATTERN.matcher(message);
|
||||||
if (racistMatcher.matches()) {
|
if (racistMatcher.matches()) {
|
||||||
player.sendMessage(Text.literal("racism bad"));
|
player.sendMessage(Text.literal("racism bad"), false);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,245 +0,0 @@
|
||||||
package land.chipmunk.chipmunkmod.modules;
|
|
||||||
|
|
||||||
import com.mojang.brigadier.Message;
|
|
||||||
import com.mojang.brigadier.suggestion.Suggestion;
|
|
||||||
import com.mojang.brigadier.suggestion.Suggestions;
|
|
||||||
import land.chipmunk.chipmunkmod.data.MutablePlayerListEntry;
|
|
||||||
import land.chipmunk.chipmunkmod.listeners.Listener;
|
|
||||||
import land.chipmunk.chipmunkmod.listeners.ListenerManager;
|
|
||||||
import land.chipmunk.chipmunkmod.mixin.ClientPlayNetworkHandlerAccessor;
|
|
||||||
import land.chipmunk.chipmunkmod.mixin.PlayerListEntryAccessor;
|
|
||||||
import net.minecraft.client.MinecraftClient;
|
|
||||||
import net.minecraft.client.network.PlayerListEntry;
|
|
||||||
import net.minecraft.network.packet.Packet;
|
|
||||||
import net.minecraft.network.packet.s2c.play.CommandSuggestionsS2CPacket;
|
|
||||||
import net.minecraft.network.packet.s2c.play.PlayerListS2CPacket;
|
|
||||||
import net.minecraft.network.packet.s2c.play.PlayerRemoveS2CPacket;
|
|
||||||
import net.minecraft.text.Text;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
import static land.chipmunk.chipmunkmod.util.ServerUtilities.serverHasCommand;
|
|
||||||
|
|
||||||
public class Players extends Listener {
|
|
||||||
public List<MutablePlayerListEntry> list = new ArrayList<>();
|
|
||||||
|
|
||||||
public static Players INSTANCE = new Players(MinecraftClient.getInstance());
|
|
||||||
|
|
||||||
private final MinecraftClient client;
|
|
||||||
|
|
||||||
public Players (MinecraftClient client) {
|
|
||||||
this.client = client;
|
|
||||||
ListenerManager.addListener(this);
|
|
||||||
|
|
||||||
TabComplete.INSTANCE.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init () {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void packetReceived (Packet<?> packet) {
|
|
||||||
if (packet instanceof PlayerListS2CPacket) packetReceived((PlayerListS2CPacket) packet);
|
|
||||||
else if (packet instanceof PlayerRemoveS2CPacket) packetReceived((PlayerRemoveS2CPacket) packet);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void packetReceived (PlayerListS2CPacket packet) {
|
|
||||||
try {
|
|
||||||
for (PlayerListS2CPacket.Action action : packet.getActions()) {
|
|
||||||
for (PlayerListS2CPacket.Entry entry : packet.getEntries()) {
|
|
||||||
if (action == PlayerListS2CPacket.Action.ADD_PLAYER) addPlayer(entry);
|
|
||||||
// else if (action == PlayerListS2CPacket.Action.INITIALIZE_CHAT) initializeChat(entry);
|
|
||||||
else if (action == PlayerListS2CPacket.Action.UPDATE_GAME_MODE) updateGamemode(entry);
|
|
||||||
// else if (action == PlayerListS2CPacket.Action.UPDATE_LISTED) updateListed(entry);
|
|
||||||
else if (action == PlayerListS2CPacket.Action.UPDATE_LATENCY) updateLatency(entry);
|
|
||||||
else if (action == PlayerListS2CPacket.Action.UPDATE_DISPLAY_NAME) updateDisplayName(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void packetReceived (PlayerRemoveS2CPacket packet) {
|
|
||||||
try {
|
|
||||||
for (UUID uuid : packet.profileIds()) {
|
|
||||||
removePlayer(uuid);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public final MutablePlayerListEntry getEntry (UUID uuid) {
|
|
||||||
try {
|
|
||||||
for (MutablePlayerListEntry candidate : list) {
|
|
||||||
if (candidate.profile.getId().equals(uuid)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final MutablePlayerListEntry getEntry (String username) {
|
|
||||||
for (MutablePlayerListEntry candidate : list) {
|
|
||||||
if (candidate.profile.getName().equals(username)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final MutablePlayerListEntry getEntry (Text displayName) {
|
|
||||||
for (MutablePlayerListEntry candidate : list) {
|
|
||||||
if (candidate.displayName != null && candidate.displayName.equals(displayName)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MutablePlayerListEntry getEntry (PlayerListS2CPacket.Entry other) {
|
|
||||||
return getEntry(other.profileId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addPlayer (PlayerListS2CPacket.Entry newEntry) {
|
|
||||||
try {
|
|
||||||
final MutablePlayerListEntry duplicate = getEntry(newEntry);
|
|
||||||
if (duplicate != null) {
|
|
||||||
removeFromPlayerList(duplicate.profile.getId());
|
|
||||||
list.remove(duplicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
final MutablePlayerListEntry entry = new MutablePlayerListEntry(newEntry);
|
|
||||||
|
|
||||||
list.add(entry);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateGamemode (PlayerListS2CPacket.Entry newEntry) {
|
|
||||||
try {
|
|
||||||
final MutablePlayerListEntry target = getEntry(newEntry);
|
|
||||||
if (target == null) return;
|
|
||||||
|
|
||||||
target.gamemode = newEntry.gameMode();
|
|
||||||
|
|
||||||
final ClientPlayNetworkHandlerAccessor accessor = ((ClientPlayNetworkHandlerAccessor) MinecraftClient.getInstance().getNetworkHandler());
|
|
||||||
|
|
||||||
if (accessor == null) return;
|
|
||||||
|
|
||||||
final PlayerListEntryAccessor entryAccessor = (PlayerListEntryAccessor) accessor.playerListEntries().get(newEntry.profileId());
|
|
||||||
|
|
||||||
entryAccessor.setGameMode(newEntry.gameMode());
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateLatency (PlayerListS2CPacket.Entry newEntry) {
|
|
||||||
final MutablePlayerListEntry target = getEntry(newEntry);
|
|
||||||
if (target == null) return;
|
|
||||||
|
|
||||||
target.latency = newEntry.latency();
|
|
||||||
|
|
||||||
final ClientPlayNetworkHandlerAccessor accessor = ((ClientPlayNetworkHandlerAccessor) MinecraftClient.getInstance().getNetworkHandler());
|
|
||||||
|
|
||||||
if (accessor == null) return;
|
|
||||||
|
|
||||||
final PlayerListEntryAccessor entryAccessor = (PlayerListEntryAccessor) accessor.playerListEntries().get(newEntry.profileId());
|
|
||||||
|
|
||||||
entryAccessor.setLatency(newEntry.latency());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDisplayName (PlayerListS2CPacket.Entry newEntry) {
|
|
||||||
final MutablePlayerListEntry target = getEntry(newEntry);
|
|
||||||
if (target == null) return;
|
|
||||||
|
|
||||||
target.displayName = newEntry.displayName();
|
|
||||||
|
|
||||||
final ClientPlayNetworkHandlerAccessor accessor = ((ClientPlayNetworkHandlerAccessor) MinecraftClient.getInstance().getNetworkHandler());
|
|
||||||
|
|
||||||
if (accessor == null) return;
|
|
||||||
|
|
||||||
accessor.playerListEntries().get(newEntry.profileId()).setDisplayName(newEntry.displayName());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removePlayer (UUID uuid) {
|
|
||||||
try {
|
|
||||||
final MutablePlayerListEntry target = getEntry(uuid);
|
|
||||||
if (target == null) return;
|
|
||||||
|
|
||||||
if (!serverHasCommand("scoreboard")) {
|
|
||||||
removeFromPlayerList(uuid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final CompletableFuture<CommandSuggestionsS2CPacket> future = TabComplete.INSTANCE.complete("/scoreboard players add ");
|
|
||||||
|
|
||||||
if (future == null) return;
|
|
||||||
|
|
||||||
future.thenApply(packet -> {
|
|
||||||
final Suggestions matches = packet.getSuggestions();
|
|
||||||
final String username = target.profile.getName();
|
|
||||||
|
|
||||||
for (int i = 0; i < matches.getList().size(); i++) {
|
|
||||||
final Suggestion suggestion = matches.getList().get(i);
|
|
||||||
|
|
||||||
final Message tooltip = suggestion.getTooltip();
|
|
||||||
if (tooltip != null || !suggestion.getText().equals(username)) continue;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.remove(target);
|
|
||||||
|
|
||||||
removeFromPlayerList(uuid);
|
|
||||||
|
|
||||||
for (MutablePlayerListEntry entry : list) {
|
|
||||||
if (!entry.profile.getId().equals(uuid)) continue;
|
|
||||||
|
|
||||||
addToPlayerList(new PlayerListEntry(entry.profile, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addToPlayerList (PlayerListEntry entry) {
|
|
||||||
client.getSocialInteractionsManager().setPlayerOnline(entry);
|
|
||||||
|
|
||||||
final ClientPlayNetworkHandlerAccessor accessor = ((ClientPlayNetworkHandlerAccessor) MinecraftClient.getInstance().getNetworkHandler());
|
|
||||||
|
|
||||||
if (accessor == null) return;
|
|
||||||
|
|
||||||
accessor.playerListEntries().put(entry.getProfile().getId(), entry);
|
|
||||||
|
|
||||||
accessor.listedPlayerListEntries().add(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeFromPlayerList (UUID uuid) {
|
|
||||||
client.getSocialInteractionsManager().setPlayerOffline(uuid);
|
|
||||||
|
|
||||||
final ClientPlayNetworkHandlerAccessor accessor = ((ClientPlayNetworkHandlerAccessor) MinecraftClient.getInstance().getNetworkHandler());
|
|
||||||
|
|
||||||
if (accessor == null) return;
|
|
||||||
|
|
||||||
final PlayerListEntry playerListEntry = accessor.playerListEntries().remove(uuid);
|
|
||||||
|
|
||||||
if (playerListEntry != null) {
|
|
||||||
accessor.listedPlayerListEntries().remove(playerListEntry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -30,9 +30,9 @@
|
||||||
],
|
],
|
||||||
|
|
||||||
"depends": {
|
"depends": {
|
||||||
"fabricloader": ">=0.16.5",
|
"fabricloader": ">=0.16.9",
|
||||||
"fabric-api": "*",
|
"fabric-api": "*",
|
||||||
"minecraft": ">=1.21",
|
"minecraft": "1.21.4",
|
||||||
"java": ">=21"
|
"java": ">=21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue