|
| 1 | +package net.earthcomputer.clientcommands.command; |
| 2 | + |
| 3 | +import com.mojang.blaze3d.platform.NativeImage; |
| 4 | +import com.mojang.brigadier.Command; |
| 5 | +import com.mojang.brigadier.CommandDispatcher; |
| 6 | +import com.mojang.brigadier.exceptions.CommandSyntaxException; |
| 7 | +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; |
| 8 | +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; |
| 9 | +import net.minecraft.ChatFormatting; |
| 10 | +import net.minecraft.client.Screenshot; |
| 11 | +import net.minecraft.client.multiplayer.ClientLevel; |
| 12 | +import net.minecraft.core.BlockPos; |
| 13 | +import net.minecraft.core.Direction; |
| 14 | +import net.minecraft.core.component.DataComponents; |
| 15 | +import net.minecraft.network.chat.ClickEvent; |
| 16 | +import net.minecraft.network.chat.Component; |
| 17 | +import net.minecraft.world.entity.Entity; |
| 18 | +import net.minecraft.world.entity.LivingEntity; |
| 19 | +import net.minecraft.world.entity.decoration.ItemFrame; |
| 20 | +import net.minecraft.world.item.ItemStack; |
| 21 | +import net.minecraft.world.item.MapItem; |
| 22 | +import net.minecraft.world.level.material.MapColor; |
| 23 | +import net.minecraft.world.level.saveddata.maps.MapItemSavedData; |
| 24 | +import org.joml.Vector2i; |
| 25 | +import org.joml.Vector4i; |
| 26 | + |
| 27 | +import java.io.File; |
| 28 | +import java.io.IOException; |
| 29 | +import java.util.ArrayDeque; |
| 30 | +import java.util.Arrays; |
| 31 | +import java.util.Deque; |
| 32 | +import java.util.HashMap; |
| 33 | +import java.util.HashSet; |
| 34 | +import java.util.List; |
| 35 | +import java.util.Map; |
| 36 | +import java.util.Set; |
| 37 | +import java.util.function.Function; |
| 38 | +import java.util.stream.Collectors; |
| 39 | +import java.util.stream.StreamSupport; |
| 40 | + |
| 41 | +import static com.mojang.brigadier.arguments.IntegerArgumentType.*; |
| 42 | +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.*; |
| 43 | + |
| 44 | +public class MapCommand { |
| 45 | + |
| 46 | + private static final SimpleCommandExceptionType NO_HELD_MAP_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cmap.noHeldMap")); |
| 47 | + private static final SimpleCommandExceptionType FAILED_SAVE_EXCEPTION = new SimpleCommandExceptionType(Component.translatable("commands.cmap.failedSave")); |
| 48 | + |
| 49 | + public static void register(CommandDispatcher<FabricClientCommandSource> dispatcher) { |
| 50 | + dispatcher.register(literal("cmap") |
| 51 | + .then(literal("export") |
| 52 | + .executes(ctx -> exportMap(ctx.getSource(), 1)) |
| 53 | + .then(argument("scale", integer(1, 64)) |
| 54 | + .executes(ctx -> exportMap(ctx.getSource(), getInteger(ctx, "scale"))) |
| 55 | + ) |
| 56 | + ) |
| 57 | + ); |
| 58 | + } |
| 59 | + |
| 60 | + private static int exportMap(FabricClientCommandSource source, int scale) throws CommandSyntaxException { |
| 61 | + MapItemSavedData data = fromHand(source.getPlayer()); |
| 62 | + if (data != null) { |
| 63 | + return exportMap(source, new MapInfo[][] { { new MapInfo(data, 0) } }, scale, false); |
| 64 | + } |
| 65 | + |
| 66 | + MapInfo[][] worldData = fromWorld(source.getClient().crosshairPickEntity, source.getClient().level, source.getPlayer().getDirection()); |
| 67 | + if (worldData != null) { |
| 68 | + return exportMap(source, worldData, scale, true); |
| 69 | + } |
| 70 | + |
| 71 | + throw NO_HELD_MAP_EXCEPTION.create(); |
| 72 | + } |
| 73 | + |
| 74 | + private static MapItemSavedData fromHand(LivingEntity entity) { |
| 75 | + ItemStack item = entity.getMainHandItem(); |
| 76 | + if (item.has(DataComponents.MAP_ID)) { |
| 77 | + return MapItem.getSavedData(item, entity.level()); |
| 78 | + } |
| 79 | + |
| 80 | + item = entity.getOffhandItem(); |
| 81 | + if (item.has(DataComponents.MAP_ID)) { |
| 82 | + return MapItem.getSavedData(item, entity.level()); |
| 83 | + } |
| 84 | + |
| 85 | + return null; |
| 86 | + } |
| 87 | + |
| 88 | + private static MapInfo[][] fromWorld(Entity entity, ClientLevel level, Direction facing) { |
| 89 | + if (!(entity instanceof ItemFrame frame)) { |
| 90 | + return null; |
| 91 | + } |
| 92 | + |
| 93 | + ItemStack item = frame.getItem(); |
| 94 | + Direction direction = frame.getDirection(); |
| 95 | + if (!item.has(DataComponents.MAP_ID)) { |
| 96 | + return null; |
| 97 | + } |
| 98 | + |
| 99 | + // determine axis directions |
| 100 | + Direction xAxis = switch (direction) { |
| 101 | + case NORTH, SOUTH, EAST, WEST -> Direction.DOWN; |
| 102 | + case UP -> facing.getOpposite(); |
| 103 | + case DOWN -> facing; |
| 104 | + }; |
| 105 | + |
| 106 | + Direction yAxis = switch (direction) { |
| 107 | + case NORTH -> Direction.WEST; |
| 108 | + case EAST -> Direction.NORTH; |
| 109 | + case SOUTH -> Direction.EAST; |
| 110 | + case WEST -> Direction.SOUTH; |
| 111 | + case UP -> Direction.from2DDataValue((xAxis.get2DDataValue() + 3) % 4); |
| 112 | + case DOWN -> Direction.from2DDataValue((xAxis.get2DDataValue() + 1) % 4); |
| 113 | + }; |
| 114 | + |
| 115 | + // collect item frames |
| 116 | + Map<BlockPos, ItemFrame> frames = StreamSupport.stream(level.entitiesForRendering().spliterator(), true) |
| 117 | + .filter(e -> e instanceof ItemFrame itemFrame && itemFrame.getDirection() == direction) |
| 118 | + .map(ItemFrame.class::cast) |
| 119 | + .collect(Collectors.toMap(ItemFrame::getPos, Function.identity())); |
| 120 | + |
| 121 | + // dfs |
| 122 | + BlockPos initialPos = frame.getPos(); |
| 123 | + |
| 124 | + Map<Vector2i, MapInfo> positionsAndData = new HashMap<>(); |
| 125 | + Set<BlockPos> visited = new HashSet<>(); |
| 126 | + Deque<BlockPos> toVisit = new ArrayDeque<>(); |
| 127 | + toVisit.addLast(frame.getPos()); |
| 128 | + |
| 129 | + while (!toVisit.isEmpty()) { |
| 130 | + BlockPos pos = toVisit.removeFirst(); |
| 131 | + if (!visited.add(pos)) { |
| 132 | + continue; |
| 133 | + } |
| 134 | + |
| 135 | + ItemFrame frameAtPos = frames.get(pos); |
| 136 | + if (frameAtPos == null) { |
| 137 | + continue; |
| 138 | + } |
| 139 | + |
| 140 | + MapItemSavedData savedData = MapItem.getSavedData(frameAtPos.getItem(), level); |
| 141 | + |
| 142 | + // compute position |
| 143 | + BlockPos relPos = pos.subtract(initialPos); |
| 144 | + Vector2i position = new Vector2i( |
| 145 | + relPos.get(xAxis.getAxis()) * xAxis.getAxisDirection().getStep(), |
| 146 | + relPos.get(yAxis.getAxis()) * yAxis.getAxisDirection().getStep() |
| 147 | + ); |
| 148 | + |
| 149 | + // offset rotation to match with map orientation |
| 150 | + int rotationOffset = switch (xAxis) { |
| 151 | + case WEST -> 3; |
| 152 | + case EAST -> 1; |
| 153 | + case SOUTH -> direction == Direction.UP ? 0 : 2; |
| 154 | + case NORTH, UP -> direction == Direction.UP ? 2 : 0; |
| 155 | + case DOWN -> 0; |
| 156 | + }; |
| 157 | + |
| 158 | + if (savedData != null) { |
| 159 | + positionsAndData.put(position, new MapInfo(savedData, frameAtPos.getRotation() + rotationOffset)); |
| 160 | + } |
| 161 | + |
| 162 | + // add adjacent to search |
| 163 | + List<BlockPos> adjacent = switch (direction.getAxis()) { |
| 164 | + case X -> List.of(pos.north(), pos.south(), pos.above(), pos.below()); |
| 165 | + case Y -> List.of(pos.north(), pos.south(), pos.east(), pos.west()); |
| 166 | + case Z -> List.of(pos.east(), pos.west(), pos.above(), pos.below()); |
| 167 | + }; |
| 168 | + |
| 169 | + toVisit.addAll(adjacent); |
| 170 | + } |
| 171 | + |
| 172 | + // determine bounds |
| 173 | + // x = minX, y = minY, z = maxX, w = maxY |
| 174 | + Vector4i bounds = positionsAndData.keySet().stream().reduce(new Vector4i(0), |
| 175 | + (bound, value) -> new Vector4i( |
| 176 | + Math.min(bound.x, value.x), |
| 177 | + Math.min(bound.y, value.y), |
| 178 | + Math.max(bound.z, value.x), |
| 179 | + Math.max(bound.w, value.y)), |
| 180 | + (a, b) -> new Vector4i( |
| 181 | + Math.min(a.x, b.x), |
| 182 | + Math.min(a.y, b.y), |
| 183 | + Math.max(a.z, b.z), |
| 184 | + Math.max(a.w, b.w))); |
| 185 | + |
| 186 | + int width = Math.abs(bounds.z - bounds.x) + 1; |
| 187 | + int height = Math.abs(bounds.w - bounds.y) + 1; |
| 188 | + |
| 189 | + // create 2d array |
| 190 | + MapInfo[][] data = new MapInfo[width][height]; |
| 191 | + for (Map.Entry<Vector2i, MapInfo> entry : positionsAndData.entrySet()) { |
| 192 | + data[entry.getKey().x - bounds.x][entry.getKey().y - bounds.y] = entry.getValue(); |
| 193 | + } |
| 194 | + |
| 195 | + return data; |
| 196 | + } |
| 197 | + |
| 198 | + // current impl will break horribly if MapItem.IMAGE_WIDTH != MapItem.IMAGE_HEIGHT |
| 199 | + private static int exportMap(FabricClientCommandSource source, MapInfo[][] data, int scale, boolean world) throws CommandSyntaxException { |
| 200 | + int height = data.length * MapItem.IMAGE_HEIGHT; |
| 201 | + int width = Arrays.stream(data).mapToInt(a -> a.length).max().orElseThrow() * MapItem.IMAGE_WIDTH; |
| 202 | + |
| 203 | + // create image |
| 204 | + NativeImage image = new NativeImage(NativeImage.Format.RGBA, width, height, true); |
| 205 | + for (int y = 0; y < data.length; y++) { |
| 206 | + for (int x = 0; x < data[y].length; x++) { |
| 207 | + MapInfo savedData = data[y][x]; |
| 208 | + if (savedData == null) { |
| 209 | + continue; |
| 210 | + } |
| 211 | + |
| 212 | + drawMapWithOffsets(image, x * MapItem.IMAGE_WIDTH * scale, y * MapItem.IMAGE_HEIGHT * scale, scale, savedData); |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + // save to screenshot dir as if it was a screenshot |
| 217 | + File screenshotDir = new File(source.getClient().gameDirectory, Screenshot.SCREENSHOT_DIR); |
| 218 | + if (!screenshotDir.exists() && !screenshotDir.mkdirs()) { |
| 219 | + throw FAILED_SAVE_EXCEPTION.create(); |
| 220 | + } |
| 221 | + |
| 222 | + File imageFile = Screenshot.getFile(screenshotDir); |
| 223 | + try { |
| 224 | + image.writeToFile(imageFile); |
| 225 | + } catch (IOException e) { |
| 226 | + throw FAILED_SAVE_EXCEPTION.create(); |
| 227 | + } |
| 228 | + |
| 229 | + source.sendFeedback(Component.translatable(world ? "commands.cmap.success.world" : "commands.cmap.success.hand", Component.literal(imageFile.getName()) |
| 230 | + .withStyle(ChatFormatting.UNDERLINE) |
| 231 | + .withStyle(s -> s.withClickEvent(new ClickEvent.OpenFile(imageFile.getAbsoluteFile()))))); |
| 232 | + |
| 233 | + return Command.SINGLE_SUCCESS; |
| 234 | + } |
| 235 | + |
| 236 | + private static void drawMapWithOffsets(NativeImage image, int offsetX, int offsetY, int scale, MapInfo info) { |
| 237 | + for (int y = 0; y < info.getHeight(); y++) { |
| 238 | + for (int x = 0; x < info.getWidth(); x++) { |
| 239 | + int color = MapColor.getColorFromPackedId(info.getMapColor(x, y)); |
| 240 | + |
| 241 | + for (int i = 0; i < scale; i++) { |
| 242 | + for (int j = 0; j < scale; j++) { |
| 243 | + image.setPixel((x * scale + i) + offsetX, (y * scale + j) + offsetY, color); |
| 244 | + } |
| 245 | + } |
| 246 | + } |
| 247 | + } |
| 248 | + } |
| 249 | + |
| 250 | + private record MapInfo(MapItemSavedData data, int rotation) { |
| 251 | + |
| 252 | + @SuppressWarnings({"UnnecessaryLocalVariable", "SuspiciousNameCombination"}) |
| 253 | + public byte getMapColor(int x, int y) { |
| 254 | + switch (rotation % 4) { |
| 255 | + case 0 -> { |
| 256 | + return data.colors[x + y * MapItem.IMAGE_WIDTH]; |
| 257 | + } |
| 258 | + // 90 clockwise |
| 259 | + case 1 -> { |
| 260 | + int newX = y; |
| 261 | + int newY = MapItem.IMAGE_WIDTH - 1 - x; |
| 262 | + return data.colors[newX + newY * MapItem.IMAGE_WIDTH]; |
| 263 | + } |
| 264 | + // 180 |
| 265 | + case 2 -> { |
| 266 | + int newX = MapItem.IMAGE_WIDTH - 1 - x; |
| 267 | + int newY = MapItem.IMAGE_WIDTH - 1 - y; |
| 268 | + return data.colors[newX + newY * MapItem.IMAGE_WIDTH]; |
| 269 | + } |
| 270 | + // 270 |
| 271 | + case 3 -> { |
| 272 | + int newX = MapItem.IMAGE_WIDTH - 1 - y; |
| 273 | + int newY = x; |
| 274 | + return data.colors[newX + newY * MapItem.IMAGE_WIDTH]; |
| 275 | + } |
| 276 | + default -> throw new IllegalStateException("unreachable"); |
| 277 | + } |
| 278 | + } |
| 279 | + |
| 280 | + @SuppressWarnings("SuspiciousNameCombination") |
| 281 | + public int getWidth() { |
| 282 | + if (rotation % 2 == 0) { |
| 283 | + return MapItem.IMAGE_WIDTH; |
| 284 | + } else { |
| 285 | + return MapItem.IMAGE_HEIGHT; |
| 286 | + } |
| 287 | + } |
| 288 | + |
| 289 | + @SuppressWarnings("SuspiciousNameCombination") |
| 290 | + public int getHeight() { |
| 291 | + if (rotation % 2 == 0) { |
| 292 | + return MapItem.IMAGE_HEIGHT; |
| 293 | + } else { |
| 294 | + return MapItem.IMAGE_WIDTH; |
| 295 | + } |
| 296 | + } |
| 297 | + } |
| 298 | +} |
0 commit comments