diff --git a/.gitignore b/.gitignore
index 3408caf..30ff733 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,8 @@ libraries/
logs/
generated/
output/
+states.txt
+states_output.txt
### Java files ###
*.class
diff --git a/src/main/java/com/viaversion/mappingsgenerator/helper/AnnoyingBlockStateMapper.java b/src/main/java/com/viaversion/mappingsgenerator/helper/AnnoyingBlockStateMapper.java
new file mode 100644
index 0000000..10f8898
--- /dev/null
+++ b/src/main/java/com/viaversion/mappingsgenerator/helper/AnnoyingBlockStateMapper.java
@@ -0,0 +1,88 @@
+package com.viaversion.mappingsgenerator.helper;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.viaversion.mappingsgenerator.util.GsonUtil;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+import static com.viaversion.mappingsgenerator.MappingsOptimizer.MAPPINGS_DIR;
+
+/**
+ * Similar to {@link BlockStateMapper} in use, except for the array of wood/stone-based building blocks.
+ *
+ * Directly edits the diff file.
+ */
+final class AnnoyingBlockStateMapper {
+
+ private static final List> MAPPERS = new ArrayList<>();
+ private static final List WONDERFUL_STATES = List.of(
+ "_wood", "_log", "_sapling", "_wall", "_slab", "_stairs",
+ "_trapdoor", "_door", "_button", "_hanging_sign", "_wall_sign", "_sign",
+ "_leaves", "_fence_gate", "_fence", "_pressure_plate"
+ );
+
+ public static void main(final String[] args) throws IOException {
+ // Input examples
+ final String from = "1.21.4";
+ final String to = "1.21.2";
+ replace("resin_brick", "brick");
+ replace("tuff", "andesite");
+ contains("copper", "brick");
+
+ final Path path = MAPPINGS_DIR.resolve("diff").resolve(String.format("mapping-%sto%s.json", from, to));
+ final JsonObject object = GsonUtil.GSON.fromJson(Files.readString(path), JsonObject.class);
+ final JsonObject blockStates = object.getAsJsonObject("blockstates");
+ final JsonObject outputStates = new JsonObject();
+ final Set handled = new HashSet<>();
+ for (final Map.Entry entry : blockStates.entrySet()) {
+ final String value = entry.getValue().getAsString();
+ final String key = entry.getKey();
+ if (!value.isEmpty()) {
+ outputStates.add(key, entry.getValue());
+ continue;
+ }
+
+ final String keyPart = key.split("\\[")[0];
+ if (handled.contains(keyPart)) {
+ outputStates.add(key, entry.getValue());
+ continue;
+ }
+
+ final String wonderfulState = WONDERFUL_STATES.stream().filter(keyPart::endsWith).findAny().orElse(null);
+ if (wonderfulState == null) {
+ outputStates.add(key, entry.getValue());
+ continue;
+ }
+
+ handled.add(key);
+ String outputKey = keyPart.replace(wonderfulState, "");
+ for (final Function mapper : MAPPERS) {
+ outputKey = mapper.apply(outputKey);
+ }
+ outputStates.addProperty(keyPart, outputKey + wonderfulState + "[");
+ }
+
+ object.add("blockstates", outputStates);
+ Files.writeString(path, GsonUtil.GSON.toJson(object));
+ }
+
+ private static void equals(final String from, final String to) {
+ MAPPERS.add(s -> s.equals(from) ? to : s);
+ }
+
+ private static void contains(final String from, final String to) {
+ MAPPERS.add(s -> s.contains(from) ? to : s);
+ }
+
+ private static void replace(final String from, final String to) {
+ MAPPERS.add(s -> s.replace(from, to));
+ }
+}
diff --git a/src/main/java/com/viaversion/mappingsgenerator/helper/BlockStateMapper.java b/src/main/java/com/viaversion/mappingsgenerator/helper/BlockStateMapper.java
new file mode 100644
index 0000000..7d1198a
--- /dev/null
+++ b/src/main/java/com/viaversion/mappingsgenerator/helper/BlockStateMapper.java
@@ -0,0 +1,227 @@
+package com.viaversion.mappingsgenerator.helper;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Utility to more easily map blockstates.
+ *
+ * Copy the stubbed lines from diff mappings into a file called states.txt and update the CONSUMER contents.
+ * Different methods will do different things, the result will be printed to the console.
+ */
+final class BlockStateMapper {
+
+ private static final Function CONSUMER = state -> {
+ state.addProperty(5, "waterlogged", "false");
+ state.setState("glow_lichen");
+ return state;
+ };
+
+ public static void main(final String[] args) throws IOException {
+ applyFunction();
+ }
+
+ public static void applyFunction() throws IOException {
+ final String content = Files.readString(Path.of("states.txt"));
+ for (final String line : content.split("\n")) {
+ String trimmedLine = line.replace("\"", "").trim();
+ if (trimmedLine.endsWith(": ,")) {
+ trimmedLine = trimmedLine.substring(0, trimmedLine.length() - 3);
+ }
+
+ final BlockState state = new BlockState(trimmedLine);
+ System.out.println("\"" + trimmedLine + "\": \"" + CONSUMER.apply(state) + "\",");
+ }
+ }
+
+ public static void replace(final String... replacements) throws IOException {
+ final String content = Files.readString(Path.of("states.txt"));
+ for (final String line : content.split("\n")) {
+ boolean found = false;
+ for (int i = 0; i < replacements.length; i += 2) {
+ final String from = replacements[i];
+ if (!line.contains(from)) {
+ continue;
+ }
+
+ final String to = replacements[i + 1];
+ final String[] split = line.split("\": \"", 2);
+ System.out.println(split[0] + "\": \"" + split[1].replace(from, to));
+ found = true;
+ break;
+ }
+
+ if (!found) {
+ System.out.println(line);
+ }
+ }
+ }
+
+ public static void editKey() throws IOException {
+ final String newName = "dark_oak_slab";
+ final String content = Files.readString(Path.of("states.txt"));
+ for (String line : content.split("\"\",")) {
+ if (line.trim().equals("}")) {
+ continue;
+ }
+
+ final String firstPart = line;
+ line = line.replace("\"", "").replace(": ", "").trim();
+ final String[] split = line.split("\\[", 2);
+ System.out.println(firstPart + "\"minecraft:" + newName + "[" + split[1] + "\",");
+ }
+ }
+
+ public static final class BlockState {
+
+ private final List properties = new ArrayList<>();
+ private String state;
+
+ public BlockState(final String state) {
+ final int start = state.indexOf('[');
+ if (start == -1) {
+ this.state = state;
+ return;
+ }
+
+ if (!state.endsWith("]")) {
+ throw new IllegalArgumentException("Invalid block state: " + state);
+ }
+
+ this.state = state.substring(0, start);
+
+ int lastCommaIndex = start;
+ int commaIndex;
+ while ((commaIndex = state.indexOf(',', lastCommaIndex + 1)) != -1) {
+ final String part = state.substring(lastCommaIndex + 1, commaIndex);
+ final String[] split = part.split("=", 2);
+ properties.add(new Property(split[0], split[1]));
+
+ lastCommaIndex = commaIndex;
+ }
+
+ final String part = state.substring(lastCommaIndex + 1, state.length() - 1);
+ final String[] split = part.split("=", 2);
+ properties.add(new Property(split[0], split[1]));
+ }
+
+ public @Nullable Property getProperty(final String key) {
+ for (final Property property : properties) {
+ if (property.key.equals(key)) {
+ return property;
+ }
+ }
+ return null;
+ }
+
+ public void addProperty(final String key, final String value) {
+ properties.add(new Property(key, value));
+ }
+
+ public void addProperty(final int index, final String key, final String value) {
+ properties.add(index, new Property(key, value));
+ }
+
+ public @Nullable Property removeProperty(final String key) {
+ for (int i = 0; i < properties.size(); i++) {
+ final Property property = properties.get(i);
+ if (property.key.equals(key)) {
+ properties.remove(i);
+ return property;
+ }
+ }
+ return null;
+ }
+
+ public List getProperties() {
+ return properties;
+ }
+
+ public String getState() {
+ return state;
+ }
+
+ public void setState(final String state) {
+ this.state = state;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder builder = new StringBuilder(state).append('[');
+ for (final Property property : properties) {
+ builder.append(property.getKey()).append('=').append(property.getValue()).append(',');
+ }
+ return builder.substring(0, builder.length() - 1) + "]";
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final BlockState that = (BlockState) o;
+ if (!properties.equals(that.properties)) return false;
+ return state.equals(that.state);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = properties.hashCode();
+ result = 31 * result + state.hashCode();
+ return result;
+ }
+
+ public BlockState copy() {
+ return new BlockState(toString());
+ }
+ }
+
+ public static final class Property {
+
+ private String key;
+ private String value;
+
+ public Property(final String key, final String value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(final String key) {
+ this.key = key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(final String value) {
+ this.value = value;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ final Property property = (Property) o;
+ if (!key.equals(property.key)) return false;
+ return value.equals(property.value);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = key.hashCode();
+ result = 31 * result + value.hashCode();
+ return result;
+ }
+ }
+}
diff --git a/src/main/java/com/viaversion/mappingsgenerator/helper/TranslationMapper.java b/src/main/java/com/viaversion/mappingsgenerator/helper/TranslationMapper.java
new file mode 100644
index 0000000..90d00ba
--- /dev/null
+++ b/src/main/java/com/viaversion/mappingsgenerator/helper/TranslationMapper.java
@@ -0,0 +1,107 @@
+package com.viaversion.mappingsgenerator.helper;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.viaversion.mappingsgenerator.util.PathUtil;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility to compare generate a diff of translations between two Minecraft versions.
+ * Copy the output into VB's translations file.
+ */
+final class TranslationMapper {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TranslationMapper.class.getSimpleName());
+ private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
+
+ public static void main(final String[] args) throws IOException {
+ final String oldVer = "1.21.2";
+ final String newVer = "1.21.4-rc3";
+
+ final Map oldTranslations = load(oldVer);
+ final Set oldValues = new HashSet<>(oldTranslations.values());
+ final Map newTranslations = load(newVer);
+
+ final JsonObject diff = new JsonObject();
+ for (final Map.Entry entry : newTranslations.entrySet()) {
+ if (oldTranslations.containsKey(entry.getKey())) {
+ continue;
+ }
+
+ if (oldValues.contains(entry.getValue())) {
+ LOGGER.warn("Changed value: {}", entry.getValue());
+ continue;
+ }
+
+ diff.addProperty(entry.getKey(), entry.getValue());
+ }
+
+ // Check for removed translations
+ for (final Map.Entry entry : oldTranslations.entrySet()) {
+ if (!newTranslations.containsKey(entry.getKey())) {
+ //LOGGER.warn("mappings.put(\"" + entry.getKey() + "\", \"" + entry.getValue() + "\");");
+ }
+ }
+
+ LOGGER.info(diff.toString());
+ LOGGER.info("Mappings size: {}", diff.size());
+ }
+
+ private static Map load(final String version) throws IOException {
+ final File jarFile = PathUtil.minecraftDir().resolve("versions").resolve(version).resolve(version + ".jar").toFile();
+ if (!jarFile.exists()) {
+ throw new IllegalArgumentException("File " + jarFile + " does not exist");
+ }
+
+ final String contents;
+ try (final ZipFile file = new ZipFile(jarFile)) {
+ ZipEntry langEntry = file.getEntry("assets/minecraft/lang/en_us.json");
+ if (langEntry == null) {
+ // Pre 1.13 translations
+ langEntry = file.getEntry("assets/minecraft/lang/en_us.lang");
+ if (langEntry != null) {
+ try (final InputStream inputStream = file.getInputStream(langEntry)) {
+ contents = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ return loadLegacyTranslations(contents);
+ }
+ throw new IllegalArgumentException("File " + jarFile + " does not contain en_us.json");
+ }
+
+ try (final InputStream inputStream = file.getInputStream(langEntry)) {
+ contents = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ }
+
+ final JsonObject object = GSON.fromJson(contents, JsonObject.class);
+ final Map translations = new LinkedHashMap<>();
+ for (final Map.Entry entry : object.entrySet()) {
+ translations.put(entry.getKey(), entry.getValue().getAsString());
+ }
+ return translations;
+ }
+
+ private static Map loadLegacyTranslations(final String contents) {
+ final Map translations = new LinkedHashMap<>();
+ contents.lines().forEach(line -> {
+ final int index = line.indexOf('=');
+ if (index != -1) {
+ translations.put(line.substring(0, index), line.substring(index + 1));
+ }
+ });
+ return translations;
+ }
+}
diff --git a/src/main/java/com/viaversion/mappingsgenerator/util/GsonUtil.java b/src/main/java/com/viaversion/mappingsgenerator/util/GsonUtil.java
new file mode 100644
index 0000000..4e3d45a
--- /dev/null
+++ b/src/main/java/com/viaversion/mappingsgenerator/util/GsonUtil.java
@@ -0,0 +1,9 @@
+package com.viaversion.mappingsgenerator.util;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+public final class GsonUtil {
+
+ public static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
+}
diff --git a/src/main/java/com/viaversion/mappingsgenerator/util/PathUtil.java b/src/main/java/com/viaversion/mappingsgenerator/util/PathUtil.java
new file mode 100644
index 0000000..eba75a0
--- /dev/null
+++ b/src/main/java/com/viaversion/mappingsgenerator/util/PathUtil.java
@@ -0,0 +1,22 @@
+package com.viaversion.mappingsgenerator.util;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public final class PathUtil {
+
+ public static Path minecraftDir() {
+ // Windows path
+ Path minecraftDir = Paths.get(home(), "AppData", "Roaming", ".minecraft");
+ if (!Files.isDirectory(minecraftDir)) {
+ // MacOS path
+ minecraftDir = Paths.get(home(), "Library", "Application Support", "minecraft");
+ }
+ return minecraftDir;
+ }
+
+ private static String home() {
+ return System.getProperty("user.home");
+ }
+}