diff --git a/src/main/java/org/geysermc/packgenerator/GeyserMappingsGenerator.java b/src/main/java/org/geysermc/packgenerator/GeyserMappingsGenerator.java index 88ed7a9..1be947e 100644 --- a/src/main/java/org/geysermc/packgenerator/GeyserMappingsGenerator.java +++ b/src/main/java/org/geysermc/packgenerator/GeyserMappingsGenerator.java @@ -27,6 +27,7 @@ public class GeyserMappingsGenerator implements ClientModInitializer { private final List pendingPackCommands = new ArrayList<>(); private boolean waitingOnItem = false; + private boolean waitingOnClear = false; @Override public void onInitializeClient() { @@ -50,7 +51,7 @@ public class GeyserMappingsGenerator implements ClientModInitializer { .then(ClientCommandManager.literal("map") .executes(context -> { ItemStack heldItem = context.getSource().getPlayer().getMainHandItem(); - PackManager.getInstance().map(heldItem, true); + PackManager.getInstance().map(heldItem); context.getSource().sendFeedback(Component.literal("Added held item to Geyser mappings")); return 0; }) @@ -59,7 +60,7 @@ public class GeyserMappingsGenerator implements ClientModInitializer { .executes(context -> { int mapped = 0; for (ItemStack stack : context.getSource().getPlayer().getInventory()) { - if (PackManager.getInstance().map(stack, false)) { + if (PackManager.getInstance().map(stack)) { mapped++; } } @@ -69,10 +70,8 @@ public class GeyserMappingsGenerator implements ClientModInitializer { ) .then(ClientCommandManager.literal("finish") .executes(context -> { - try { - PackManager.getInstance().finish(); - } catch (IOException exception) { - throw new SimpleCommandExceptionType(Component.literal(exception.getMessage())).create(); + if (!PackManager.getInstance().finish()) { + throw new SimpleCommandExceptionType(Component.literal("Errors occurred whilst trying to write the pack to disk!")).create(); } context.getSource().sendFeedback(Component.literal("Wrote pack to disk")); return 0; @@ -80,34 +79,39 @@ public class GeyserMappingsGenerator implements ClientModInitializer { ) .then(ClientCommandManager.literal("auto") .then(ClientCommandManager.argument("namespace", StringArgumentType.word()) - .executes(context -> { - String namespace = StringArgumentType.getString(context, "namespace"); + .then(ClientCommandManager.argument("path", StringArgumentType.string()) + .executes(context -> { + String namespace = StringArgumentType.getString(context, "namespace"); + String path = StringArgumentType.getString(context, "path"); - // Duplicated code, this is just to try this out - try { - PackManager.getInstance().startPack(namespace); - } catch (IOException exception) { - throw new SimpleCommandExceptionType(Component.literal(exception.getMessage())).create(); - } - context.getSource().sendFeedback(Component.literal("Created pack with name " + namespace)); + // Duplicated code, this is just to try this out + try { + PackManager.getInstance().startPack(namespace); + } catch (IOException exception) { + throw new SimpleCommandExceptionType(Component.literal(exception.getMessage())).create(); + } + context.getSource().sendFeedback(Component.literal("Created pack with name " + namespace)); - // hack - CommandContext suggestionsContext = new CommandContext<>(null, - "loot give @s loot " + namespace + ":", - null, null, null, null, null, null, null, false); - context.getSource().getClient().getConnection().getSuggestionsProvider() - .customSuggestion(suggestionsContext) - .whenComplete((suggestions, throwable) -> pendingPackCommands.addAll(suggestions.getList().stream().map(Suggestion::getText).toList())); - return 0; - }) + // hack + CommandContext suggestionsContext = new CommandContext<>(null, + "loot give @s loot " + namespace + ":" + path, + null, null, null, null, null, null, null, false); + context.getSource().getClient().getConnection().getSuggestionsProvider() + .customSuggestion(suggestionsContext) + .whenComplete((suggestions, throwable) -> pendingPackCommands.addAll(suggestions.getList().stream().map(Suggestion::getText).toList())); + return 0; + }) + ) ) ) ); }); ClientTickEvents.START_CLIENT_TICK.register(client -> { - if (!pendingPackCommands.isEmpty() || waitingOnItem) { - if (!waitingOnItem) { + if (!pendingPackCommands.isEmpty() || waitingOnItem || waitingOnClear) { + if (waitingOnClear && client.player.getInventory().isEmpty()) { + waitingOnClear = false; + } else if (!waitingOnItem) { String command = pendingPackCommands.removeFirst(); client.getConnection().send(new ServerboundChatCommandPacket("loot give @s loot " + command)); waitingOnItem = true; @@ -116,7 +120,7 @@ public class GeyserMappingsGenerator implements ClientModInitializer { int mapped = 0; for (ItemStack stack : client.player.getInventory()) { try { - if (PackManager.getInstance().map(stack, false)) { + if (PackManager.getInstance().map(stack)) { mapped++; } } catch (Exception exception) { @@ -130,11 +134,16 @@ public class GeyserMappingsGenerator implements ClientModInitializer { waitingOnItem = false; if (pendingPackCommands.isEmpty()) { try { - PackManager.getInstance().finish(); - } catch (IOException | CommandSyntaxException exception) { - throw new RuntimeException(exception); + if (!PackManager.getInstance().finish()) { + client.player.displayClientMessage(Component.literal("Errors occurred whilst trying to write the pack to disk!"), false); + return; + } + } catch (CommandSyntaxException e) { + throw new RuntimeException(e); } client.player.displayClientMessage(Component.literal("Wrote pack to disk"), false); + } else { + waitingOnClear = true; } } } diff --git a/src/main/java/org/geysermc/packgenerator/PackManager.java b/src/main/java/org/geysermc/packgenerator/PackManager.java index 7ea8143..aca6f3b 100644 --- a/src/main/java/org/geysermc/packgenerator/PackManager.java +++ b/src/main/java/org/geysermc/packgenerator/PackManager.java @@ -24,25 +24,17 @@ public final class PackManager { currentPack = new BedrockPack(name); } - public boolean map(ItemStack stack, boolean throwOnModelMissing) throws CommandSyntaxException { + public boolean map(ItemStack stack) throws CommandSyntaxException { ensurePackIsCreated(); - try { - currentPack.map(stack); - return true; - } catch (IllegalArgumentException exception) { - if (throwOnModelMissing) { - throw new SimpleCommandExceptionType(Component.literal("Item stack does not have a custom model")).create(); - } else { - return false; - } - } + return currentPack.map(stack); } - public void finish() throws CommandSyntaxException, IOException { + public boolean finish() throws CommandSyntaxException { ensurePackIsCreated(); - currentPack.save(); + boolean success = currentPack.save(); currentPack = null; + return success; } private void ensurePackIsCreated() throws CommandSyntaxException { diff --git a/src/main/java/org/geysermc/packgenerator/mapping/geyser/GeyserItemMapper.java b/src/main/java/org/geysermc/packgenerator/mapping/geyser/GeyserItemMapper.java index e6743e3..5f4d2cf 100644 --- a/src/main/java/org/geysermc/packgenerator/mapping/geyser/GeyserItemMapper.java +++ b/src/main/java/org/geysermc/packgenerator/mapping/geyser/GeyserItemMapper.java @@ -19,6 +19,7 @@ import net.minecraft.client.renderer.item.properties.select.TrimMaterialProperty import net.minecraft.core.component.DataComponentPatch; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ProblemReporter; import net.minecraft.world.item.CrossbowItem; import net.minecraft.world.item.equipment.trim.TrimMaterial; import net.minecraft.world.level.Level; @@ -37,9 +38,10 @@ import java.util.stream.Stream; public class GeyserItemMapper { - public static Stream mapItem(ResourceLocation modelLocation, String displayName, int protectionValue, DataComponentPatch componentPatch) { - MappingContext context = new MappingContext(List.of(), modelLocation, displayName, protectionValue, componentPatch); + public static Stream mapItem(ResourceLocation modelLocation, String displayName, int protectionValue, DataComponentPatch componentPatch, + ProblemReporter reporter) { ItemModel model = Minecraft.getInstance().getModelManager().getItemModel(modelLocation); + MappingContext context = new MappingContext(List.of(), modelLocation, displayName, protectionValue, componentPatch, reporter.forChild(() -> "model " + modelLocation + " ")); return mapItem(model, context); } @@ -53,14 +55,15 @@ public class GeyserItemMapper { return Stream.of(context.create(itemModel)); } case ConditionalItemModel conditional -> { - return mapConditionalModel(conditional, context); + return mapConditionalModel(conditional, context.child("condition " + conditional + " ")); } case SelectItemModel select -> { - return mapSelectModel(select, context); + return mapSelectModel(select, context.child("select " + select + " ")); } default -> {} } - throw new UnsupportedOperationException("Unable to map item model " + model.getClass()); + context.reporter.report(() -> "unable to map item model " + model.getClass()); + return Stream.empty(); } private static Stream mapConditionalModel(ConditionalItemModel model, MappingContext context) { @@ -71,15 +74,19 @@ public class GeyserItemMapper { case CustomModelDataProperty customModelData -> new GeyserConditionPredicate.CustomModelData(customModelData.index()); case HasComponent hasComponent -> new GeyserConditionPredicate.HasComponent(hasComponent.componentType()); // ignoreDefault property not a thing, we should look into that in Geyser! TODO case FishingRodCast ignored -> GeyserConditionPredicate.FISHING_ROD_CAST; - default -> throw new UnsupportedOperationException("Unsupported conditional model property " + property.getClass()); + default -> null; }; + if (predicateProperty == null) { + context.reporter.report(() -> "unsupported conditional model property " + property); + return Stream.empty(); + } ItemModel onTrue = ((ConditionalItemModelAccessor) model).getOnTrue(); ItemModel onFalse = ((ConditionalItemModelAccessor) model).getOnFalse(); return Stream.concat( - mapItem(onTrue, context.with(new GeyserConditionPredicate(predicateProperty, true))), - mapItem(onFalse, context.with(new GeyserConditionPredicate(predicateProperty, false))) + mapItem(onTrue, context.with(new GeyserConditionPredicate(predicateProperty, true), "condition on true ")), + mapItem(onFalse, context.with(new GeyserConditionPredicate(predicateProperty, false), "condition on false ")) ); } @@ -92,22 +99,32 @@ public class GeyserItemMapper { case ContextDimension ignored -> dimension -> new GeyserMatchPredicate.ContextDimension((ResourceKey) dimension); // Why, Mojang? case net.minecraft.client.renderer.item.properties.select.CustomModelDataProperty customModelData -> string -> new GeyserMatchPredicate.CustomModelData((String) string, customModelData.index()); - default -> throw new UnsupportedOperationException("Unsupported select model property " + property.getClass()); + default -> null; }; + if (dataConstructor == null) { + context.reporter.report(() -> "unsupported select model property " + property); + return Stream.empty(); + } //noinspection unchecked Object2ObjectMap cases = ((SelectItemModelCasesAccessor) model).geyser_mappings_generator$getCases(); return Stream.concat( cases.entrySet().stream() - .flatMap(caze -> mapItem(caze.getValue(), context.with(new GeyserMatchPredicate(dataConstructor.apply(caze.getKey()))))), - mapItem(cases.defaultReturnValue(), context) + .flatMap(caze -> mapItem(caze.getValue(), context.with(new GeyserMatchPredicate(dataConstructor.apply(caze.getKey())), "select case " + caze.getKey() + " "))), + mapItem(cases.defaultReturnValue(), context.child("default case ")) ); } - private record MappingContext(List predicateStack, ResourceLocation model, String displayName, int protectionValue, DataComponentPatch componentPatch) { + private record MappingContext(List predicateStack, ResourceLocation model, String displayName, int protectionValue, DataComponentPatch componentPatch, + ProblemReporter reporter) { - public MappingContext with(GeyserPredicate predicate) { - return new MappingContext(Stream.concat(predicateStack.stream(), Stream.of(predicate)).toList(), model, displayName, protectionValue, componentPatch); + public MappingContext with(GeyserPredicate predicate, String childName) { + return new MappingContext(Stream.concat(predicateStack.stream(), Stream.of(predicate)).toList(), model, displayName, protectionValue, componentPatch, + reporter.forChild(() -> childName)); + } + + public MappingContext child(String childName) { + return new MappingContext(predicateStack, model, displayName, protectionValue, componentPatch, reporter.forChild(() -> childName)); } public GeyserMapping create(ResourceLocation bedrockIdentifier) { diff --git a/src/main/java/org/geysermc/packgenerator/mapping/geyser/GeyserMappings.java b/src/main/java/org/geysermc/packgenerator/mapping/geyser/GeyserMappings.java index 96e2fcd..33687c6 100644 --- a/src/main/java/org/geysermc/packgenerator/mapping/geyser/GeyserMappings.java +++ b/src/main/java/org/geysermc/packgenerator/mapping/geyser/GeyserMappings.java @@ -7,6 +7,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.core.Holder; import net.minecraft.core.component.DataComponents; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ProblemReporter; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import org.geysermc.packgenerator.CodecUtil; @@ -48,20 +49,25 @@ public class GeyserMappings { mappings.put(item, mapping); } - public void map(ItemStack stack, Consumer mappingConsumer) { + public void map(ItemStack stack, ProblemReporter reporter, Consumer mappingConsumer) { Optional patchedModel = stack.getComponentsPatch().get(DataComponents.ITEM_MODEL); //noinspection OptionalAssignedToNull - annoying Mojang if (patchedModel == null || patchedModel.isEmpty()) { - throw new IllegalArgumentException("Item stack does not have a custom model"); + return; } ResourceLocation model = patchedModel.get(); String displayName = stack.getHoverName().getString(); int protectionValue = 0; // TODO check the attributes - GeyserItemMapper.mapItem(model, displayName, protectionValue, stack.getComponentsPatch()) + GeyserItemMapper.mapItem(model, displayName, protectionValue, stack.getComponentsPatch(), reporter) .forEach(mapping -> { - map(stack.getItemHolder(), mapping); + try { + map(stack.getItemHolder(), mapping); + } catch (IllegalArgumentException exception) { + reporter.forChild(() -> "mapping with bedrock identifier " + mapping.bedrockIdentifier() + " ").report(() -> "failed to add mapping to mappings file: " + exception.getMessage()); + return; + } mappingConsumer.accept(mapping); }); } diff --git a/src/main/java/org/geysermc/packgenerator/pack/BedrockPack.java b/src/main/java/org/geysermc/packgenerator/pack/BedrockPack.java index 1c38474..02041ea 100644 --- a/src/main/java/org/geysermc/packgenerator/pack/BedrockPack.java +++ b/src/main/java/org/geysermc/packgenerator/pack/BedrockPack.java @@ -3,6 +3,7 @@ package org.geysermc.packgenerator.pack; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.Minecraft; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ProblemReporter; import net.minecraft.world.item.ItemStack; import org.apache.commons.io.IOUtils; import org.geysermc.packgenerator.CodecUtil; @@ -10,16 +11,19 @@ import org.geysermc.packgenerator.PackConstants; import org.geysermc.packgenerator.mapping.attachable.AttachableMapper; import org.geysermc.packgenerator.mapping.geyser.GeyserMappings; import org.geysermc.packgenerator.pack.attachable.BedrockAttachable; +import org.jetbrains.annotations.NotNull; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; public class BedrockPack { private static final Path EXPORT_DIRECTORY = FabricLoader.getInstance().getGameDir().resolve("geyser"); @@ -30,6 +34,8 @@ public class BedrockPack { private static final Path MANIFEST_FILE = Path.of("manifest.json"); private static final Path ITEM_ATLAS_FILE = Path.of("textures/item_texture.json"); + private static final Path REPORT_FILE = Path.of("report.txt"); + private final String name; private final Path exportPath; private final Path packPath; @@ -39,22 +45,45 @@ public class BedrockPack { private final List attachables = new ArrayList<>(); private final List texturesToExport = new ArrayList<>(); + private final ProblemReporter.Collector reporter; + public BedrockPack(String name) throws IOException { this.name = name; + exportPath = createPackDirectory(name); packPath = exportPath.resolve(PACK_DIRECTORY); mappings = CodecUtil.readOrCompute(GeyserMappings.CODEC, exportPath.resolve(MAPPINGS_FILE), GeyserMappings::new); manifest = CodecUtil.readOrCompute(PackManifest.CODEC, packPath.resolve(MANIFEST_FILE), () -> defaultManifest(name)).increment(); itemTextures = CodecUtil.readOrCompute(BedrockTextureAtlas.ITEM_ATLAS_CODEC, packPath.resolve(ITEM_ATLAS_FILE), () -> BedrockTextureAtlas.itemAtlas(name, BedrockTextures.builder())).textures().toBuilder(); + + reporter = new ProblemReporter.Collector(() -> "Bedrock pack " + name); } public String name() { return name; } - public void map(ItemStack stack) { - mappings.map(stack, mapping -> { + public boolean map(ItemStack stack) { + if (stack.isEmpty()) { + return false; + } + AtomicBoolean problems = new AtomicBoolean(); + ProblemReporter mapReporter = new ProblemReporter() { + + @Override + public @NotNull ProblemReporter forChild(PathElement child) { + return reporter.forChild(child); + } + + @Override + public void report(Problem problem) { + problems.set(true); + reporter.report(problem); + } + }; + + mappings.map(stack, mapReporter, mapping -> { // TODO a proper way to get texture from item model itemTextures.withItemTexture(mapping, mapping.bedrockIdentifier().getPath()); ResourceLocation texture = mapping.bedrockIdentifier(); @@ -64,14 +93,27 @@ public class BedrockPack { texturesToExport.add(texture); AttachableMapper.mapItem(stack, mapping.bedrockIdentifier(), texturesToExport::add).ifPresent(attachables::add); }); + return problems.get(); } - public void save() throws IOException { - CodecUtil.trySaveJson(GeyserMappings.CODEC, mappings, exportPath.resolve(MAPPINGS_FILE)); - CodecUtil.trySaveJson(PackManifest.CODEC, manifest, packPath.resolve(MANIFEST_FILE)); - CodecUtil.trySaveJson(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.itemAtlas(name, itemTextures), packPath.resolve(ITEM_ATLAS_FILE)); + public boolean save() { + boolean success = true; + + try { + CodecUtil.trySaveJson(GeyserMappings.CODEC, mappings, exportPath.resolve(MAPPINGS_FILE)); + CodecUtil.trySaveJson(PackManifest.CODEC, manifest, packPath.resolve(MANIFEST_FILE)); + CodecUtil.trySaveJson(BedrockTextureAtlas.CODEC, BedrockTextureAtlas.itemAtlas(name, itemTextures), packPath.resolve(ITEM_ATLAS_FILE)); + } catch (IOException exception) { + reporter.forChild(() -> "saving Geyser mappings, pack manifest, and texture atlas ").report(() -> "failed to save to pack: " + exception); + success = false; + } for (BedrockAttachable attachable : attachables) { - attachable.save(packPath.resolve(ATTACHABLES_DIRECTORY)); + try { + attachable.save(packPath.resolve(ATTACHABLES_DIRECTORY)); + } catch (IOException exception) { + reporter.forChild(() -> "attachable for bedrock item " + attachable.info().identifier() + " ").report(() -> "failed to save to pack: " + exception); + success = false; + } } for (ResourceLocation texture : texturesToExport) { @@ -82,10 +124,19 @@ public class BedrockPack { try (OutputStream outputTexture = new FileOutputStream(texturePath.toFile())) { IOUtils.copy(inputTexture, outputTexture); } - } catch (FileNotFoundException exception) { - // TODO + } catch (IOException exception) { + ResourceLocation finalTexture = texture; + reporter.forChild(() -> "texture " + finalTexture + " ").report(() -> "failed to save to pack: " + exception); + success = false; } } + + try { + Files.writeString(exportPath.resolve(REPORT_FILE), reporter.getTreeReport()); + } catch (IOException exception) { + // TODO log + } + return success; } private static Path createPackDirectory(String name) throws IOException {