add rotation support
This commit is contained in:
parent
5094c033a3
commit
897c1e5975
12
build.gradle
12
build.gradle
|
@ -20,18 +20,6 @@ repositories {
|
|||
mavenCentral()
|
||||
}
|
||||
|
||||
loom {
|
||||
splitEnvironmentSourceSets()
|
||||
|
||||
mods {
|
||||
"modid" {
|
||||
sourceSet sourceSets.main
|
||||
sourceSet sourceSets.client
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// To change the versions see the gradle.properties file
|
||||
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||
|
|
|
@ -9,7 +9,7 @@ loader_version=0.14.21
|
|||
fabric_version=0.84.0+1.20.1
|
||||
|
||||
#Mod properties
|
||||
mod_version = 0.9.2
|
||||
mod_version = 0.9.4
|
||||
maven_group = quimufu.structure_item
|
||||
archives_base_name = structure_item
|
||||
|
||||
|
|
|
@ -83,6 +83,7 @@ public class MyItem extends Item {
|
|||
if (i == 4) {
|
||||
texts.add(Text.translatable("item.structure_item.item.tooltip.blacklist.more",
|
||||
Text.literal(String.valueOf(bl.size() - i))));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +97,15 @@ public class MyItem extends Item {
|
|||
} else {
|
||||
texts.add(Text.translatable("item.structure_item.item.tooltip.doNotPlaceEntities"));
|
||||
}
|
||||
if (tag.contains("rotate", NbtElement.STRING_TYPE)) {
|
||||
texts.add(Text.translatable("item.structure_item.item.tooltip.dynamic.rotation"));
|
||||
texts.add(Text.translatable("item.structure_item.item.tooltip.dynamic.rotation.value",
|
||||
Text.translatable("item.structure_item.item.tooltip.dynamic.dir." + tag.getString("rotate"))));
|
||||
} else {
|
||||
texts.add(Text.translatable("item.structure_item.item.tooltip.fixed.rotation"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,15 +175,40 @@ public class MyItem extends Item {
|
|||
}
|
||||
StructureTemplate x = xOpt.get();
|
||||
|
||||
BlockRotation rotation = BlockRotation.NONE;
|
||||
if (tag.contains("rotate", NbtElement.STRING_TYPE)) {
|
||||
String defaultOrientation = tag.getString("rotate");
|
||||
|
||||
Direction defaultDir = Direction.byName(defaultOrientation);
|
||||
Direction currentDir = c.getSide();
|
||||
|
||||
if (defaultDir == null) {
|
||||
Text message =
|
||||
Text.translatable("items.structure.spawner.invalid.rotate",
|
||||
Text.literal(defaultOrientation));
|
||||
sendPlayerChat(player, message);
|
||||
} else {
|
||||
rotation = getRotationBetween(defaultDir, currentDir);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BlockPos loc = c.getBlockPos().offset(c.getSide());
|
||||
if (tag.contains("offset", 10)) {
|
||||
BlockPos offset = NbtHelper.toBlockPos(tag.getCompound("offset"));
|
||||
loc = loc.add(offset);
|
||||
|
||||
Vec3i size = x.getSize();
|
||||
switch (rotation) {
|
||||
case CLOCKWISE_90 -> loc = loc.add(new Vec3i(size.getX()-1, 0, 0));
|
||||
case CLOCKWISE_180 -> loc = loc.add(new Vec3i(size.getX()-1, 0, size.getZ()-1));
|
||||
case COUNTERCLOCKWISE_90 -> loc = loc.add(new Vec3i(0, 0, size.getZ()-1));
|
||||
}
|
||||
} else if (tag.contains("offsetV2", 10)) {
|
||||
Direction direction = c.getSide().getOpposite();
|
||||
try {
|
||||
StructureOffsetSettings offset = StructureOffsetSettings.ofTag(tag.getCompound("offsetV2"));
|
||||
offset.setRotation(rotation);
|
||||
Vec3i size = x.getSize();
|
||||
loc = loc.add(offset.getEffective(direction, size));
|
||||
} catch (Exception e) {
|
||||
|
@ -185,11 +220,12 @@ public class MyItem extends Item {
|
|||
} else {
|
||||
Direction direction = c.getSide().getOpposite();
|
||||
StructureOffsetSettings offset = StructureOffsetSettings.dynamic();
|
||||
offset.setRotation(rotation);
|
||||
Vec3i size = x.getSize();
|
||||
loc = loc.add(offset.getEffective(direction, size));
|
||||
}
|
||||
|
||||
MyPlacementSettings ps = (new MyPlacementSettings());
|
||||
MyPlacementSettings ps = new MyPlacementSettings();
|
||||
if (tag.contains("replaceEntities", 99)) {
|
||||
ps.setReplaceEntities(tag.getBoolean("replaceEntities"));
|
||||
}
|
||||
|
@ -213,15 +249,17 @@ public class MyItem extends Item {
|
|||
}
|
||||
ps.forbidOverwrite(blacklist);
|
||||
}
|
||||
|
||||
|
||||
ps.setWorld(c.getWorld())
|
||||
.setSize(x.getSize())
|
||||
.setMirror(BlockMirror.NONE)
|
||||
.setRotation(BlockRotation.NONE);
|
||||
.setRotation(rotation);
|
||||
boolean success = false;
|
||||
try {
|
||||
if (x.place((ServerWorld) c.getWorld(), loc, loc, ps, c.getWorld().getRandom(), 2))
|
||||
if (x.place((ServerWorld) c.getWorld(), loc, BlockPos.ORIGIN, ps, c.getWorld().getRandom(), 2))
|
||||
success = true;
|
||||
} catch (NullPointerException ignored) {
|
||||
} catch (PlacementNotAllowedException ignored) {
|
||||
}
|
||||
if (success) {
|
||||
c.getStack().decrement(1);
|
||||
|
@ -235,6 +273,22 @@ public class MyItem extends Item {
|
|||
return ActionResult.FAIL;
|
||||
}
|
||||
|
||||
private BlockRotation getRotationBetween(Direction from, Direction to) {
|
||||
if (to == Direction.DOWN || to == Direction.UP) {
|
||||
return BlockRotation.NONE;
|
||||
}
|
||||
if (from.getOpposite() == to) {
|
||||
return BlockRotation.CLOCKWISE_180;
|
||||
}
|
||||
if (from.rotateYClockwise() == to) {
|
||||
return BlockRotation.CLOCKWISE_90;
|
||||
}
|
||||
if (from.rotateYCounterclockwise() == to) {
|
||||
return BlockRotation.COUNTERCLOCKWISE_90;
|
||||
}
|
||||
return BlockRotation.NONE;
|
||||
}
|
||||
|
||||
private static void sendPlayer(ServerPlayerEntity player, Text message) {
|
||||
if (player == null)
|
||||
return;
|
||||
|
|
|
@ -4,12 +4,18 @@ import com.google.common.collect.Lists;
|
|||
import net.minecraft.block.Block;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.structure.StructurePlacementData;
|
||||
import net.minecraft.structure.StructureTemplate;
|
||||
import net.minecraft.structure.processor.BlockIgnoreStructureProcessor;
|
||||
import net.minecraft.structure.processor.StructureProcessor;
|
||||
import net.minecraft.structure.processor.StructureProcessorType;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Box;
|
||||
import net.minecraft.util.math.Vec3i;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraft.world.WorldView;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -20,6 +26,11 @@ public class MyPlacementSettings extends StructurePlacementData {
|
|||
private Vec3i size;
|
||||
private boolean replaceEntities = true;
|
||||
|
||||
public MyPlacementSettings() {
|
||||
addProcessor(BlockIgnoreStructureProcessor.IGNORE_STRUCTURE_BLOCKS);
|
||||
addProcessor(new CheckingStructureProcess());
|
||||
}
|
||||
|
||||
public void forbidOverwrite(List<Block> blocks) {
|
||||
if (blocks.size() == 0) {
|
||||
blacklist = null;
|
||||
|
@ -38,69 +49,6 @@ public class MyPlacementSettings extends StructurePlacementData {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StructureTemplate.PalettedBlockInfoList getRandomBlockInfos(List<StructureTemplate.PalettedBlockInfoList> blocks, BlockPos pos) {
|
||||
if (world == null || pos == null || size == null) {
|
||||
return super.getRandomBlockInfos(blocks, pos);
|
||||
}
|
||||
|
||||
List<StructureTemplate.PalettedBlockInfoList> eligibleStructures;
|
||||
eligibleStructures = getEligibleStructures(blocks, pos);
|
||||
if (eligibleStructures.size() == 0)
|
||||
return null;
|
||||
StructureTemplate.PalettedBlockInfoList randomBlockInfos = super.getRandomBlockInfos(eligibleStructures, pos);
|
||||
List<StructureTemplate.StructureBlockInfo> locs = randomBlockInfos.getAll();
|
||||
if (!locs.isEmpty()) {
|
||||
List<Entity> entitiesWithinAABB = world.getNonSpectatingEntities(Entity.class, new Box(pos,pos.add(size)));
|
||||
for (StructureTemplate.StructureBlockInfo blockInfo : locs) {
|
||||
BlockPos posToClean = blockInfo.pos().add(pos);
|
||||
for (Entity e : entitiesWithinAABB) {
|
||||
if (!(e instanceof PlayerEntity) && e.getBoundingBox().intersects(new Box(posToClean))) {
|
||||
e.remove(Entity.RemovalReason.DISCARDED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return randomBlockInfos;
|
||||
}
|
||||
|
||||
private List<StructureTemplate.PalettedBlockInfoList> getEligibleStructures(List<StructureTemplate.PalettedBlockInfoList> blocks, BlockPos pos) {
|
||||
List<StructureTemplate.PalettedBlockInfoList> eligibleStructures = new ArrayList<>();
|
||||
if (blacklist == null && shouldReplaceEntities()) {
|
||||
eligibleStructures = blocks;
|
||||
} else {
|
||||
for (StructureTemplate.PalettedBlockInfoList struct : blocks) {
|
||||
if (isValid(struct, pos)) {
|
||||
eligibleStructures.add(struct);
|
||||
}
|
||||
}
|
||||
}
|
||||
return eligibleStructures;
|
||||
}
|
||||
|
||||
private boolean isValid(StructureTemplate.PalettedBlockInfoList struct, BlockPos pos) {
|
||||
List<? extends Entity> entitiesWithinAABB;
|
||||
if (shouldReplaceEntities()) {
|
||||
entitiesWithinAABB = world.getNonSpectatingEntities(PlayerEntity.class, new Box(pos, pos.add(size)));
|
||||
} else {
|
||||
entitiesWithinAABB = world.getNonSpectatingEntities(Entity.class, new Box(pos, pos.add(size)));
|
||||
}
|
||||
for (StructureTemplate.StructureBlockInfo bi : struct.getAll()) {
|
||||
BlockPos posToCheck = bi.pos().add(pos);
|
||||
if (World.isValid(posToCheck)) {
|
||||
for (Entity e : entitiesWithinAABB) {
|
||||
if (e.getBoundingBox().intersects(new Box(posToCheck))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Block blockToCheck = world.getBlockState(posToCheck).getBlock();
|
||||
if(blacklist.contains(blockToCheck))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean shouldReplaceEntities() {
|
||||
return replaceEntities;
|
||||
}
|
||||
|
@ -108,4 +56,38 @@ public class MyPlacementSettings extends StructurePlacementData {
|
|||
public void setReplaceEntities(boolean replaceEntities) {
|
||||
this.replaceEntities = replaceEntities;
|
||||
}
|
||||
|
||||
public class CheckingStructureProcess extends StructureProcessor {
|
||||
|
||||
List<? extends Entity> entitiesWithinAABB;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public StructureTemplate.StructureBlockInfo process(WorldView world, BlockPos pos, BlockPos pivot, StructureTemplate.StructureBlockInfo originalBlockInfo, StructureTemplate.StructureBlockInfo currentBlockInfo, StructurePlacementData data) {
|
||||
if (entitiesWithinAABB == null) {
|
||||
if (shouldReplaceEntities()) {
|
||||
entitiesWithinAABB = ((ServerWorld) world).getNonSpectatingEntities(PlayerEntity.class, new Box(pos.subtract(size), pos.add(size)));
|
||||
} else {
|
||||
entitiesWithinAABB = ((ServerWorld) world).getNonSpectatingEntities(Entity.class, new Box(pos.subtract(size), pos.add(size)));
|
||||
}
|
||||
}
|
||||
BlockPos posToCheck;
|
||||
if (currentBlockInfo != null && World.isValid(posToCheck = currentBlockInfo.pos())) {
|
||||
for (Entity e : entitiesWithinAABB) {
|
||||
if (e.getBoundingBox().intersects(new Box(posToCheck))) {
|
||||
throw new PlacementNotAllowedException();
|
||||
}
|
||||
}
|
||||
Block blockToCheck = world.getBlockState(posToCheck).getBlock();
|
||||
if (blacklist.contains(blockToCheck))
|
||||
throw new PlacementNotAllowedException();
|
||||
}
|
||||
return currentBlockInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StructureProcessorType<?> getType() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
package quimufu.structure_item;
|
||||
|
||||
public class PlacementNotAllowedException extends RuntimeException{
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package quimufu.structure_item;
|
||||
|
||||
import net.minecraft.nbt.NbtCompound;
|
||||
import net.minecraft.util.BlockRotation;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
import net.minecraft.util.math.Direction;
|
||||
import net.minecraft.util.math.MathHelper;
|
||||
|
@ -23,6 +24,7 @@ public class StructureOffsetSettings {
|
|||
return args[(int) (Math.round(args[0]) + 1L)];
|
||||
}
|
||||
};
|
||||
private BlockRotation rotation;
|
||||
|
||||
public static StructureOffsetSettings ofTag(NbtCompound offsetTag) {
|
||||
StructureOffsetSettings settings = new StructureOffsetSettings();
|
||||
|
@ -83,30 +85,96 @@ public class StructureOffsetSettings {
|
|||
|
||||
private Vec3i getDirectionalOffset(Direction direction, Vec3i size) {
|
||||
BlockPos loc = new BlockPos(0, 0, 0);
|
||||
switch (direction) {
|
||||
case WEST -> {
|
||||
loc = loc.offset(Direction.NORTH, size.getZ() / 2);
|
||||
loc = loc.offset(Direction.WEST, size.getX() - 1);
|
||||
}
|
||||
case EAST -> //positive x
|
||||
loc = loc.offset(Direction.NORTH, size.getZ() / 2);
|
||||
case NORTH -> {
|
||||
loc = loc.offset(Direction.NORTH, size.getZ() - 1);
|
||||
loc = loc.offset(Direction.WEST, size.getX() / 2);
|
||||
}
|
||||
case SOUTH -> //positive z
|
||||
loc = loc.offset(Direction.WEST, size.getX() / 2);
|
||||
case UP -> { //positive y
|
||||
loc = loc.offset(Direction.NORTH, size.getZ() / 2);
|
||||
loc = loc.offset(Direction.WEST, size.getX() / 2);
|
||||
loc = loc.offset(Direction.UP);
|
||||
}
|
||||
case DOWN -> {
|
||||
loc = loc.offset(Direction.NORTH, size.getZ() / 2);
|
||||
loc = loc.offset(Direction.WEST, size.getX() / 2);
|
||||
loc = loc.offset(Direction.DOWN, size.getY());
|
||||
}
|
||||
switch (rotation) {
|
||||
case NONE:
|
||||
switch (direction) {
|
||||
case WEST:
|
||||
loc = loc.offset(Direction.NORTH, size.getZ() / 2);
|
||||
return loc.offset(Direction.WEST, size.getX() - 1);
|
||||
case EAST://positive x
|
||||
return loc.offset(Direction.NORTH, size.getZ() / 2);
|
||||
case NORTH:
|
||||
loc = loc.offset(Direction.NORTH, size.getZ() - 1);
|
||||
return loc.offset(Direction.WEST, size.getX() / 2);
|
||||
case SOUTH://positive z
|
||||
return loc.offset(Direction.WEST, size.getX() / 2);
|
||||
case UP://positive y
|
||||
loc = loc.offset(Direction.NORTH, size.getZ() / 2);
|
||||
loc = loc.offset(Direction.WEST, size.getX() / 2);
|
||||
return loc.offset(Direction.UP);
|
||||
case DOWN:
|
||||
loc = loc.offset(Direction.NORTH, size.getZ() / 2);
|
||||
loc = loc.offset(Direction.WEST, size.getX() / 2);
|
||||
return loc.offset(Direction.DOWN, size.getY());
|
||||
}
|
||||
case CLOCKWISE_90:
|
||||
switch (direction) {
|
||||
case WEST:
|
||||
return loc.offset(Direction.NORTH, size.getX() / 2);
|
||||
case EAST://positive x
|
||||
loc = loc.offset(Direction.NORTH, size.getX() / 2);
|
||||
return loc.offset(Direction.EAST, size.getZ() - 1);
|
||||
case NORTH:
|
||||
loc = loc.offset(Direction.NORTH, size.getX() - 1);
|
||||
return loc.offset(Direction.EAST, size.getZ() / 2);
|
||||
case SOUTH://positive z
|
||||
return loc.offset(Direction.EAST, size.getZ() / 2);
|
||||
case UP://positive y
|
||||
loc = loc.offset(Direction.NORTH, size.getX() / 2);
|
||||
loc = loc.offset(Direction.EAST, size.getZ() / 2);
|
||||
return loc.offset(Direction.UP);
|
||||
case DOWN:
|
||||
loc = loc.offset(Direction.NORTH, size.getX() / 2);
|
||||
loc = loc.offset(Direction.EAST, size.getZ() / 2);
|
||||
return loc.offset(Direction.DOWN, size.getY());
|
||||
}
|
||||
case CLOCKWISE_180:
|
||||
switch (direction) {
|
||||
case WEST:
|
||||
return loc.offset(Direction.SOUTH, size.getX() / 2);
|
||||
case EAST://positive x
|
||||
loc = loc.offset(Direction.SOUTH, size.getZ() / 2);
|
||||
return loc.offset(Direction.EAST, size.getX() - 1);
|
||||
case NORTH:
|
||||
return loc.offset(Direction.EAST, size.getX() / 2);
|
||||
case SOUTH://positive z
|
||||
loc = loc.offset(Direction.EAST, size.getX() / 2);
|
||||
return loc.offset(Direction.SOUTH, size.getZ() - 1);
|
||||
case UP://positive y
|
||||
loc = loc.offset(Direction.SOUTH, size.getZ() / 2);
|
||||
loc = loc.offset(Direction.EAST, size.getX() / 2);
|
||||
return loc.offset(Direction.UP);
|
||||
case DOWN:
|
||||
loc = loc.offset(Direction.SOUTH, size.getZ() / 2);
|
||||
loc = loc.offset(Direction.EAST, size.getX() / 2);
|
||||
return loc.offset(Direction.DOWN, size.getY());
|
||||
}
|
||||
case COUNTERCLOCKWISE_90:
|
||||
switch (direction) {
|
||||
case WEST:
|
||||
loc = loc.offset(Direction.SOUTH, size.getX() / 2);
|
||||
return loc.offset(Direction.WEST, size.getZ() - 1);
|
||||
case EAST://positive x
|
||||
return loc.offset(Direction.SOUTH, size.getX() / 2);
|
||||
case NORTH:
|
||||
return loc.offset(Direction.WEST, size.getZ() / 2);
|
||||
case SOUTH://positive z
|
||||
loc = loc.offset(Direction.SOUTH, size.getX() - 1);
|
||||
return loc.offset(Direction.WEST, size.getZ() / 2);
|
||||
case UP://positive y
|
||||
loc = loc.offset(Direction.SOUTH, size.getX() / 2);
|
||||
loc = loc.offset(Direction.WEST, size.getZ() / 2);
|
||||
return loc.offset(Direction.UP);
|
||||
case DOWN:
|
||||
loc = loc.offset(Direction.SOUTH, size.getX() / 2);
|
||||
loc = loc.offset(Direction.WEST, size.getZ() / 2);
|
||||
return loc.offset(Direction.DOWN, size.getY());
|
||||
}
|
||||
}
|
||||
return loc;
|
||||
}
|
||||
|
||||
public void setRotation(BlockRotation rotation) {
|
||||
this.rotation = rotation;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
"items.structure.spawner.structure.nonexistent": "Structure: %s does not exist",
|
||||
"items.structure.spawner.invalid.block": "Block %s invalid",
|
||||
"items.structure.spawner.invalid.offsetV2": "offsetV2 tag %s invalid: %s",
|
||||
"items.structure.spawner.invalid.rotate": "rotate tag invalid direction: %s",
|
||||
"items.structure.spawner.no.tag": "Item has no NBT tag",
|
||||
"items.structure.spawner.no.structure": "Item has no String \"structure\": tag",
|
||||
"item.structure_item.item": "Structure Spawner",
|
||||
|
@ -12,7 +13,7 @@
|
|||
"item.structure_item.item.tooltip.structure": "Places down: ",
|
||||
"item.structure_item.item.tooltip.allowed.on": "Can be placed on: ",
|
||||
"item.structure_item.item.tooltip.fixed.offset": "Offset:",
|
||||
"item.structure_item.item.tooltip.v2.offset": "Advanced Offset:",
|
||||
"item.structure_item.item.tooltip.v2.offset": "Advanced offset:",
|
||||
"item.structure_item.item.tooltip.xyz":" x: %s y: %s z: %s",
|
||||
"item.structure_item.item.tooltip.xFuncOff":" x: %s",
|
||||
"item.structure_item.item.tooltip.yFuncOff":" y: %s",
|
||||
|
@ -21,7 +22,14 @@
|
|||
"item.structure_item.item.tooltip.blacklist": "Blacklist:",
|
||||
"item.structure_item.item.tooltip.blacklist.more": " And %s more...",
|
||||
"item.structure_item.item.tooltip.replaceEntities": "Deletes Entities in the way",
|
||||
"item.structure_item.item.tooltip.doNotReplaceEntities": "Doesn't allow placement with Entities in the way",
|
||||
"item.structure_item.item.tooltip.doNotReplaceEntities": "Doesn't allow placement with entities in the way",
|
||||
"item.structure_item.item.tooltip.placeEntities": "Places contained Entities",
|
||||
"item.structure_item.item.tooltip.doNotPlaceEntities": "Doesn't place contained Entities"
|
||||
"item.structure_item.item.tooltip.doNotPlaceEntities": "Doesn't place contained entities",
|
||||
"item.structure_item.item.tooltip.dynamic.rotation": "Rotated dynamically",
|
||||
"item.structure_item.item.tooltip.dynamic.rotation.value": "Player is standing %s of structure ",
|
||||
"item.structure_item.item.tooltip.dynamic.dir.north": "north",
|
||||
"item.structure_item.item.tooltip.dynamic.dir.south": "south",
|
||||
"item.structure_item.item.tooltip.dynamic.dir.east": "east",
|
||||
"item.structure_item.item.tooltip.dynamic.dir.west": "west",
|
||||
"item.structure_item.item.tooltip.fixed.rotation": "Fixed rotation"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user