Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2024-12-25 15:50:14 +01:00
Fix powder snow and fish buckets (#2437)
Dieser Commit ist enthalten in:
Ursprung
20b183ddda
Commit
3eb73a5634
@ -26,14 +26,27 @@
|
|||||||
package org.geysermc.connector.entity.living.animal;
|
package org.geysermc.connector.entity.living.animal;
|
||||||
|
|
||||||
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
|
import com.github.steveice10.mc.protocol.data.game.entity.metadata.EntityMetadata;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.nukkitx.math.vector.Vector3f;
|
import com.nukkitx.math.vector.Vector3f;
|
||||||
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
import com.nukkitx.protocol.bedrock.data.entity.EntityData;
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntList;
|
||||||
import org.geysermc.connector.entity.living.AbstractFishEntity;
|
import org.geysermc.connector.entity.living.AbstractFishEntity;
|
||||||
import org.geysermc.connector.entity.type.EntityType;
|
import org.geysermc.connector.entity.type.EntityType;
|
||||||
import org.geysermc.connector.network.session.GeyserSession;
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class TropicalFishEntity extends AbstractFishEntity {
|
public class TropicalFishEntity extends AbstractFishEntity {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of variant numbers that are given special names
|
||||||
|
* The index of the variant in this list is used as part of the locale key
|
||||||
|
*/
|
||||||
|
private static final IntList PREDEFINED_VARIANTS = IntList.of(117506305, 117899265, 185008129, 117441793, 118161664, 65536, 50726144, 67764993, 234882305, 67110144, 117441025, 16778497, 101253888, 50660352, 918529, 235340288, 918273, 67108865, 917504, 459008, 67699456, 67371009);
|
||||||
|
|
||||||
|
private static final List<String> VARIANT_NAMES = ImmutableList.of("kob", "sunstreak", "snooper", "dasher", "brinely", "spotty", "flopper", "stripey", "glitter", "blockfish", "betty", "clayfish");
|
||||||
|
private static final List<String> COLOR_NAMES = ImmutableList.of("white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray", "light_gray", "cyan", "purple", "blue", "brown", "green", "red", "black");
|
||||||
|
|
||||||
public TropicalFishEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
public TropicalFishEntity(long entityId, long geyserId, EntityType entityType, Vector3f position, Vector3f motion, Vector3f rotation) {
|
||||||
super(entityId, geyserId, entityType, position, motion, rotation);
|
super(entityId, geyserId, entityType, position, motion, rotation);
|
||||||
}
|
}
|
||||||
@ -43,11 +56,48 @@ public class TropicalFishEntity extends AbstractFishEntity {
|
|||||||
if (entityMetadata.getId() == 17) {
|
if (entityMetadata.getId() == 17) {
|
||||||
int varNumber = (int) entityMetadata.getValue();
|
int varNumber = (int) entityMetadata.getValue();
|
||||||
|
|
||||||
metadata.put(EntityData.VARIANT, varNumber & 0xFF); // Shape 0-1
|
metadata.put(EntityData.VARIANT, getShape(varNumber)); // Shape 0-1
|
||||||
metadata.put(EntityData.MARK_VARIANT, (varNumber >> 8) & 0xFF); // Pattern 0-5
|
metadata.put(EntityData.MARK_VARIANT, getPattern(varNumber)); // Pattern 0-5
|
||||||
metadata.put(EntityData.COLOR, (byte) ((varNumber >> 16) & 0xFF)); // Base color 0-15
|
metadata.put(EntityData.COLOR, getBaseColor(varNumber)); // Base color 0-15
|
||||||
metadata.put(EntityData.COLOR_2, (byte) ((varNumber >> 24) & 0xFF)); // Pattern color 0-15
|
metadata.put(EntityData.COLOR_2, getPatternColor(varNumber)); // Pattern color 0-15
|
||||||
}
|
}
|
||||||
super.updateBedrockMetadata(entityMetadata, session);
|
super.updateBedrockMetadata(entityMetadata, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getShape(int variant) {
|
||||||
|
return Math.min(variant & 0xFF, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getPattern(int variant) {
|
||||||
|
return Math.min((variant >> 8) & 0xFF, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte getBaseColor(int variant) {
|
||||||
|
byte color = (byte) ((variant >> 16) & 0xFF);
|
||||||
|
if (!(0 <= color && color <= 15)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte getPatternColor(int variant) {
|
||||||
|
byte color = (byte) ((variant >> 24) & 0xFF);
|
||||||
|
if (!(0 <= color && color <= 15)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getVariantName(int variant) {
|
||||||
|
int id = 6 * getShape(variant) + getPattern(variant);
|
||||||
|
return VARIANT_NAMES.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getColorName(byte colorId) {
|
||||||
|
return COLOR_NAMES.get(colorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getPredefinedId(int variant) {
|
||||||
|
return PREDEFINED_VARIANTS.indexOf(variant);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,6 @@ import com.github.steveice10.mc.protocol.packet.ingame.client.player.ClientPlaye
|
|||||||
import com.nukkitx.math.vector.Vector3f;
|
import com.nukkitx.math.vector.Vector3f;
|
||||||
import com.nukkitx.math.vector.Vector3i;
|
import com.nukkitx.math.vector.Vector3i;
|
||||||
import com.nukkitx.protocol.bedrock.data.LevelEventType;
|
import com.nukkitx.protocol.bedrock.data.LevelEventType;
|
||||||
import com.nukkitx.protocol.bedrock.data.entity.EntityFlags;
|
|
||||||
import com.nukkitx.protocol.bedrock.data.inventory.*;
|
import com.nukkitx.protocol.bedrock.data.inventory.*;
|
||||||
import com.nukkitx.protocol.bedrock.packet.*;
|
import com.nukkitx.protocol.bedrock.packet.*;
|
||||||
import org.geysermc.connector.entity.CommandBlockMinecartEntity;
|
import org.geysermc.connector.entity.CommandBlockMinecartEntity;
|
||||||
@ -149,7 +148,6 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||||||
*/
|
*/
|
||||||
// CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch
|
// CraftBukkit+ check - see https://github.com/PaperMC/Paper/blob/458db6206daae76327a64f4e2a17b67a7e38b426/Spigot-Server-Patches/0532-Move-range-check-for-block-placing-up.patch
|
||||||
Vector3f playerPosition = session.getPlayerEntity().getPosition();
|
Vector3f playerPosition = session.getPlayerEntity().getPosition();
|
||||||
EntityFlags flags = session.getPlayerEntity().getMetadata().getFlags();
|
|
||||||
|
|
||||||
// Adjust position for current eye height
|
// Adjust position for current eye height
|
||||||
switch (session.getPose()) {
|
switch (session.getPose()) {
|
||||||
@ -210,15 +208,18 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||||||
if (session.getItemMappings().getBoatIds().contains(packet.getItemInHand().getId())) {
|
if (session.getItemMappings().getBoatIds().contains(packet.getItemInHand().getId())) {
|
||||||
ClientPlayerUseItemPacket itemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
|
ClientPlayerUseItemPacket itemPacket = new ClientPlayerUseItemPacket(Hand.MAIN_HAND);
|
||||||
session.sendDownstreamPacket(itemPacket);
|
session.sendDownstreamPacket(itemPacket);
|
||||||
}
|
} else if (session.getItemMappings().getBucketIds().contains(packet.getItemInHand().getId())) {
|
||||||
// Check actions, otherwise buckets may be activated when block inventories are accessed
|
|
||||||
else if (session.getItemMappings().getBucketIds().contains(packet.getItemInHand().getId())) {
|
|
||||||
// Let the server decide if the bucket item should change, not the client, and revert the changes the client made
|
// Let the server decide if the bucket item should change, not the client, and revert the changes the client made
|
||||||
InventorySlotPacket slotPacket = new InventorySlotPacket();
|
InventorySlotPacket slotPacket = new InventorySlotPacket();
|
||||||
slotPacket.setContainerId(ContainerId.INVENTORY);
|
slotPacket.setContainerId(ContainerId.INVENTORY);
|
||||||
slotPacket.setSlot(packet.getHotbarSlot());
|
slotPacket.setSlot(packet.getHotbarSlot());
|
||||||
slotPacket.setItem(packet.getItemInHand());
|
slotPacket.setItem(packet.getItemInHand());
|
||||||
session.sendUpstreamPacket(slotPacket);
|
session.sendUpstreamPacket(slotPacket);
|
||||||
|
// Don't send ClientPlayerUseItemPacket for powder snow buckets
|
||||||
|
if (packet.getItemInHand().getId() != session.getItemMappings().getStoredItems().powderSnowBucket().getBedrockId()) {
|
||||||
|
// Special check for crafting tables since clients don't send BLOCK_INTERACT when interacting
|
||||||
|
int blockState = session.getConnector().getWorldManager().getBlockAt(session, packet.getBlockPosition());
|
||||||
|
if (session.isSneaking() || blockState != BlockRegistries.JAVA_IDENTIFIERS.get("minecraft:crafting_table")) {
|
||||||
// Delay the interaction in case the client doesn't intend to actually use the bucket
|
// Delay the interaction in case the client doesn't intend to actually use the bucket
|
||||||
// See BedrockActionTranslator.java
|
// See BedrockActionTranslator.java
|
||||||
session.setBucketScheduledFuture(session.getConnector().getGeneralThreadPool().schedule(() -> {
|
session.setBucketScheduledFuture(session.getConnector().getGeneralThreadPool().schedule(() -> {
|
||||||
@ -227,6 +228,8 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
|
|||||||
}, 5, TimeUnit.MILLISECONDS));
|
}, 5, TimeUnit.MILLISECONDS));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (packet.getActions().isEmpty()) {
|
if (packet.getActions().isEmpty()) {
|
||||||
if (session.getOpPermissionLevel() >= 2 && session.getGameMode() == GameMode.CREATIVE) {
|
if (session.getOpPermissionLevel() >= 2 && session.getGameMode() == GameMode.CREATIVE) {
|
||||||
|
@ -46,6 +46,7 @@ public class StoredItemMappings {
|
|||||||
private final ItemMapping fishingRod;
|
private final ItemMapping fishingRod;
|
||||||
private final ItemMapping lodestoneCompass;
|
private final ItemMapping lodestoneCompass;
|
||||||
private final ItemMapping milkBucket;
|
private final ItemMapping milkBucket;
|
||||||
|
private final ItemMapping powderSnowBucket;
|
||||||
private final ItemMapping egg;
|
private final ItemMapping egg;
|
||||||
private final ItemMapping shield;
|
private final ItemMapping shield;
|
||||||
private final ItemMapping wheat;
|
private final ItemMapping wheat;
|
||||||
@ -60,6 +61,7 @@ public class StoredItemMappings {
|
|||||||
this.fishingRod = load(itemMappings, "fishing_rod");
|
this.fishingRod = load(itemMappings, "fishing_rod");
|
||||||
this.lodestoneCompass = load(itemMappings, "lodestone_compass");
|
this.lodestoneCompass = load(itemMappings, "lodestone_compass");
|
||||||
this.milkBucket = load(itemMappings, "milk_bucket");
|
this.milkBucket = load(itemMappings, "milk_bucket");
|
||||||
|
this.powderSnowBucket = load(itemMappings, "powder_snow_bucket");
|
||||||
this.egg = load(itemMappings, "egg");
|
this.egg = load(itemMappings, "egg");
|
||||||
this.shield = load(itemMappings, "shield");
|
this.shield = load(itemMappings, "shield");
|
||||||
this.wheat = load(itemMappings, "wheat");
|
this.wheat = load(itemMappings, "wheat");
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2019-2021 GeyserMC. http://geysermc.org
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
* @author GeyserMC
|
||||||
|
* @link https://github.com/GeyserMC/Geyser
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.geysermc.connector.network.translators.item.translators.nbt;
|
||||||
|
|
||||||
|
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import net.kyori.adventure.text.format.Style;
|
||||||
|
import net.kyori.adventure.text.format.TextDecoration;
|
||||||
|
import org.geysermc.connector.entity.living.animal.TropicalFishEntity;
|
||||||
|
import org.geysermc.connector.network.session.GeyserSession;
|
||||||
|
import org.geysermc.connector.network.translators.ItemRemapper;
|
||||||
|
import org.geysermc.connector.network.translators.chat.MessageTranslator;
|
||||||
|
import org.geysermc.connector.network.translators.item.NbtItemStackTranslator;
|
||||||
|
import org.geysermc.connector.registry.type.ItemMapping;
|
||||||
|
import org.geysermc.connector.utils.LocaleUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@ItemRemapper
|
||||||
|
public class TropicalFishBucketTranslator extends NbtItemStackTranslator {
|
||||||
|
|
||||||
|
private static final Style LORE_STYLE = Style.style(NamedTextColor.GRAY, TextDecoration.ITALIC);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void translateToBedrock(GeyserSession session, CompoundTag itemTag, ItemMapping mapping) {
|
||||||
|
// Prevent name from appearing as "Bucket of"
|
||||||
|
itemTag.put(new ByteTag("AppendCustomName", (byte) 1));
|
||||||
|
itemTag.put(new StringTag("CustomName", LocaleUtils.getLocaleString("entity.minecraft.tropical_fish", session.getLocale())));
|
||||||
|
// Add Java's client side lore tag
|
||||||
|
Tag bucketVariantTag = itemTag.get("BucketVariantTag");
|
||||||
|
if (bucketVariantTag instanceof IntTag) {
|
||||||
|
CompoundTag displayTag = itemTag.get("display");
|
||||||
|
if (displayTag == null) {
|
||||||
|
displayTag = new CompoundTag("display");
|
||||||
|
itemTag.put(displayTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Tag> lore = new ArrayList<>();
|
||||||
|
|
||||||
|
int varNumber = ((IntTag) bucketVariantTag).getValue();
|
||||||
|
int predefinedVariantId = TropicalFishEntity.getPredefinedId(varNumber);
|
||||||
|
if (predefinedVariantId != -1) {
|
||||||
|
Component tooltip = Component.translatable("entity.minecraft.tropical_fish.predefined." + predefinedVariantId, LORE_STYLE);
|
||||||
|
lore.add(0, new StringTag("", MessageTranslator.convertMessage(tooltip, session.getLocale())));
|
||||||
|
} else {
|
||||||
|
Component typeTooltip = Component.translatable("entity.minecraft.tropical_fish.type." + TropicalFishEntity.getVariantName(varNumber), LORE_STYLE);
|
||||||
|
lore.add(0, new StringTag("", MessageTranslator.convertMessage(typeTooltip, session.getLocale())));
|
||||||
|
|
||||||
|
byte baseColor = TropicalFishEntity.getBaseColor(varNumber);
|
||||||
|
byte patternColor = TropicalFishEntity.getPatternColor(varNumber);
|
||||||
|
Component colorTooltip = Component.translatable("color.minecraft." + TropicalFishEntity.getColorName(baseColor), LORE_STYLE);
|
||||||
|
if (baseColor != patternColor) {
|
||||||
|
colorTooltip = colorTooltip.append(Component.text(", ", LORE_STYLE))
|
||||||
|
.append(Component.translatable("color.minecraft." + TropicalFishEntity.getColorName(patternColor), LORE_STYLE));
|
||||||
|
}
|
||||||
|
lore.add(1, new StringTag("", MessageTranslator.convertMessage(colorTooltip, session.getLocale())));
|
||||||
|
}
|
||||||
|
|
||||||
|
ListTag loreTag = displayTag.get("Lore");
|
||||||
|
if (loreTag != null) {
|
||||||
|
lore.addAll(loreTag.getValue());
|
||||||
|
}
|
||||||
|
displayTag.put(new ListTag("Lore", lore));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean acceptItem(ItemMapping mapping) {
|
||||||
|
return mapping.getJavaIdentifier().equals("minecraft:tropical_fish_bucket");
|
||||||
|
}
|
||||||
|
}
|
@ -32,12 +32,14 @@ import org.geysermc.connector.network.session.GeyserSession;
|
|||||||
import org.geysermc.connector.network.translators.sound.BlockSoundInteractionHandler;
|
import org.geysermc.connector.network.translators.sound.BlockSoundInteractionHandler;
|
||||||
import org.geysermc.connector.network.translators.sound.SoundHandler;
|
import org.geysermc.connector.network.translators.sound.SoundHandler;
|
||||||
|
|
||||||
@SoundHandler(items = "bucket")
|
@SoundHandler(items = "bucket", ignoreSneakingWhileHolding = true)
|
||||||
public class BucketSoundInteractionHandler implements BlockSoundInteractionHandler {
|
public class BucketSoundInteractionHandler implements BlockSoundInteractionHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleInteraction(GeyserSession session, Vector3f position, String identifier) {
|
public void handleInteraction(GeyserSession session, Vector3f position, String identifier) {
|
||||||
if (session.getBucketScheduledFuture() == null) return; // No bucket was really interacted with
|
if (session.getBucketScheduledFuture() == null) {
|
||||||
|
return; // No bucket was really interacted with
|
||||||
|
}
|
||||||
String handItemIdentifier = session.getPlayerInventory().getItemInHand().getMapping(session).getJavaIdentifier();
|
String handItemIdentifier = session.getPlayerInventory().getItemInHand().getMapping(session).getJavaIdentifier();
|
||||||
LevelSoundEventPacket soundEventPacket = new LevelSoundEventPacket();
|
LevelSoundEventPacket soundEventPacket = new LevelSoundEventPacket();
|
||||||
soundEventPacket.setPosition(position);
|
soundEventPacket.setPosition(position);
|
||||||
@ -52,22 +54,31 @@ public class BucketSoundInteractionHandler implements BlockSoundInteractionHandl
|
|||||||
soundEvent = SoundEvent.BUCKET_FILL_WATER;
|
soundEvent = SoundEvent.BUCKET_FILL_WATER;
|
||||||
} else if (identifier.contains("lava[")) {
|
} else if (identifier.contains("lava[")) {
|
||||||
soundEvent = SoundEvent.BUCKET_FILL_LAVA;
|
soundEvent = SoundEvent.BUCKET_FILL_LAVA;
|
||||||
|
} else if (identifier.contains("powder_snow")) {
|
||||||
|
soundEvent = SoundEvent.BUCKET_FILL_POWDER_SNOW;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "minecraft:lava_bucket":
|
case "minecraft:lava_bucket":
|
||||||
soundEvent = SoundEvent.BUCKET_EMPTY_LAVA;
|
soundEvent = SoundEvent.BUCKET_EMPTY_LAVA;
|
||||||
break;
|
break;
|
||||||
case "minecraft:fish_bucket":
|
case "minecraft:axolotl_bucket":
|
||||||
|
case "minecraft:cod_bucket":
|
||||||
|
case "minecraft:salmon_bucket":
|
||||||
|
case "minecraft:pufferfish_bucket":
|
||||||
|
case "minecraft:tropical_fish_bucket":
|
||||||
soundEvent = SoundEvent.BUCKET_EMPTY_FISH;
|
soundEvent = SoundEvent.BUCKET_EMPTY_FISH;
|
||||||
break;
|
break;
|
||||||
case "minecraft:water_bucket":
|
case "minecraft:water_bucket":
|
||||||
soundEvent = SoundEvent.BUCKET_EMPTY_WATER;
|
soundEvent = SoundEvent.BUCKET_EMPTY_WATER;
|
||||||
break;
|
break;
|
||||||
|
case "minecraft:powder_snow_bucket":
|
||||||
|
soundEvent = SoundEvent.BUCKET_EMPTY_POWDER_SNOW;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if (soundEvent != null) {
|
if (soundEvent != null) {
|
||||||
soundEventPacket.setSound(soundEvent);
|
soundEventPacket.setSound(soundEvent);
|
||||||
session.sendUpstreamPacket(soundEventPacket);
|
session.sendUpstreamPacket(soundEventPacket);
|
||||||
}
|
|
||||||
session.setBucketScheduledFuture(null);
|
session.setBucketScheduledFuture(null);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit f109d34a343da0ade6132661839b893859680d91
|
Subproject commit 2efdb453e4e76992d63824b5c8b551bebec67b71
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren