Skip to content

Commit a99d1ac

Browse files
committed
cmap v2, electric boogaloo
1 parent bc4008c commit a99d1ac

File tree

4 files changed

+307
-0
lines changed

4 files changed

+307
-0
lines changed

src/main/java/net/earthcomputer/clientcommands/ClientCommands.java

+1
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public static void registerCommands(CommandDispatcher<FabricClientCommandSource>
159159
KitCommand.register(dispatcher);
160160
ListenCommand.register(dispatcher);
161161
LookCommand.register(dispatcher);
162+
MapCommand.register(dispatcher);
162163
MinesweeperCommand.register(dispatcher);
163164
MoteCommand.register(dispatcher);
164165
NoteCommand.register(dispatcher);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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+
}

src/main/resources/assets/clientcommands/lang/en_us.json

+5
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@
198198
"commands.clisten.sentPacket": "Sent the following packet: %s",
199199
"commands.clisten.unknownPacket": "Unknown packet %s",
200200

201+
"commands.cmap.noHeldMap": "You are not holding or looking at a map",
202+
"commands.cmap.failedSave": "Failed to save map to file",
203+
"commands.cmap.success.hand": "Saved held map successfully as %s",
204+
"commands.cmap.success.world": "Saved target map successfully as %s",
205+
201206
"commands.cminesweeper.tooManyMines": "Too many mines, must be between 0 and 9 less than than the amount of total tiles",
202207

203208
"commands.cparticle.success": "Displaying particle %s",

src/main/resources/clientcommands.aw

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ accessible field net/minecraft/network/PacketDecoder protocolInfo Lnet/minecraft
3333
accessible field net/minecraft/network/PacketEncoder protocolInfo Lnet/minecraft/network/ProtocolInfo;
3434
accessible field net/minecraft/network/codec/IdDispatchCodec toId Lit/unimi/dsi/fastutil/objects/Object2IntMap;
3535

36+
# cmap
37+
accessible method net/minecraft/client/Screenshot getFile (Ljava/io/File;)Ljava/io/File;
38+
3639
# cpermissionlevel
3740
accessible method net/minecraft/client/player/LocalPlayer getPermissionLevel ()I
3841

0 commit comments

Comments
 (0)