diff --git a/extractor/src/main/java/dev/_00a/valence_extractor/DummyPlayerEntity.java b/extractor/src/main/java/dev/_00a/valence_extractor/DummyPlayerEntity.java new file mode 100644 index 0000000..153273c --- /dev/null +++ b/extractor/src/main/java/dev/_00a/valence_extractor/DummyPlayerEntity.java @@ -0,0 +1,42 @@ +package dev._00a.valence_extractor; + +import com.mojang.authlib.GameProfile; +import net.minecraft.entity.Entity; +import net.minecraft.entity.data.DataTracker; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.encryption.PlayerPublicKey; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +public class DummyPlayerEntity extends PlayerEntity { + public static final DummyPlayerEntity INSTANCE; + + static { + INSTANCE = Util.magicallyInstantiate(DummyPlayerEntity.class); + + try { + var dataTrackerField = Entity.class.getDeclaredField("dataTracker"); + dataTrackerField.setAccessible(true); + dataTrackerField.set(INSTANCE, new DataTracker(INSTANCE)); + + INSTANCE.initDataTracker(); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private DummyPlayerEntity(World world, BlockPos pos, float yaw, GameProfile gameProfile, @Nullable PlayerPublicKey publicKey) { + super(world, pos, yaw, gameProfile, publicKey); + } + + @Override + public boolean isSpectator() { + return false; + } + + @Override + public boolean isCreative() { + return false; + } +} diff --git a/extractor/src/main/java/dev/_00a/valence_extractor/DummyWorld.java b/extractor/src/main/java/dev/_00a/valence_extractor/DummyWorld.java new file mode 100644 index 0000000..3a9c7fb --- /dev/null +++ b/extractor/src/main/java/dev/_00a/valence_extractor/DummyWorld.java @@ -0,0 +1,255 @@ +package dev._00a.valence_extractor; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.fluid.Fluid; +import net.minecraft.item.map.MapState; +import net.minecraft.recipe.RecipeManager; +import net.minecraft.scoreboard.Scoreboard; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.random.Random; +import net.minecraft.util.profiler.Profiler; +import net.minecraft.util.registry.DynamicRegistryManager; +import net.minecraft.util.registry.RegistryEntry; +import net.minecraft.util.registry.RegistryKey; +import net.minecraft.world.Difficulty; +import net.minecraft.world.GameRules; +import net.minecraft.world.MutableWorldProperties; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.ChunkManager; +import net.minecraft.world.dimension.DimensionType; +import net.minecraft.world.entity.EntityLookup; +import net.minecraft.world.event.GameEvent; +import net.minecraft.world.tick.QueryableTickScheduler; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Supplier; + +public class DummyWorld extends World { + + public static final DummyWorld INSTANCE; + + static { + INSTANCE = Util.magicallyInstantiate(DummyWorld.class); + + try { + var randomField = World.class.getDeclaredField("random"); + randomField.setAccessible(true); + randomField.set(INSTANCE, Random.create()); + + var propertiesField = World.class.getDeclaredField("properties"); + propertiesField.setAccessible(true); + propertiesField.set(INSTANCE, new DummyMutableWorldProperties()); + + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private DummyWorld(MutableWorldProperties properties, RegistryKey registryRef, RegistryEntry dimension, Supplier profiler, boolean isClient, boolean debugWorld, long seed, int maxChainedNeighborUpdates) { + super(properties, registryRef, dimension, profiler, isClient, debugWorld, seed, maxChainedNeighborUpdates); + } + + @Override + public void updateListeners(BlockPos pos, BlockState oldState, BlockState newState, int flags) { + + } + + @Override + public void playSound(@Nullable PlayerEntity except, double x, double y, double z, SoundEvent sound, SoundCategory category, float volume, float pitch, long seed) { + + } + + @Override + public void playSoundFromEntity(@Nullable PlayerEntity except, Entity entity, SoundEvent sound, SoundCategory category, float volume, float pitch, long seed) { + + } + + @Override + public String asString() { + return ""; + } + + @Nullable + @Override + public Entity getEntityById(int id) { + return null; + } + + @Nullable + @Override + public MapState getMapState(String id) { + return null; + } + + @Override + public void putMapState(String id, MapState state) { + + } + + @Override + public int getNextMapId() { + return 0; + } + + @Override + public void setBlockBreakingInfo(int entityId, BlockPos pos, int progress) { + + } + + @Override + public Scoreboard getScoreboard() { + return new Scoreboard(); + } + + @Override + public RecipeManager getRecipeManager() { + return new RecipeManager(); + } + + @Override + protected EntityLookup getEntityLookup() { + return null; + } + + @Override + public QueryableTickScheduler getBlockTickScheduler() { + return null; + } + + @Override + public QueryableTickScheduler getFluidTickScheduler() { + return null; + } + + @Override + public ChunkManager getChunkManager() { + return null; + } + + @Override + public void syncWorldEvent(@Nullable PlayerEntity player, int eventId, BlockPos pos, int data) { + + } + + @Override + public void emitGameEvent(GameEvent event, Vec3d emitterPos, GameEvent.Emitter emitter) { + + } + + @Override + public DynamicRegistryManager getRegistryManager() { + return null; + } + + @Override + public float getBrightness(Direction direction, boolean shaded) { + return 0; + } + + @Override + public List getPlayers() { + return List.of(); + } + + @Override + public RegistryEntry getGeneratorStoredBiome(int biomeX, int biomeY, int biomeZ) { + return null; + } + + private static class DummyMutableWorldProperties implements MutableWorldProperties { + + @Override + public int getSpawnX() { + return 0; + } + + @Override + public void setSpawnX(int spawnX) { + + } + + @Override + public int getSpawnY() { + return 0; + } + + @Override + public void setSpawnY(int spawnY) { + + } + + @Override + public int getSpawnZ() { + return 0; + } + + @Override + public void setSpawnZ(int spawnZ) { + + } + + @Override + public float getSpawnAngle() { + return 0; + } + + @Override + public void setSpawnAngle(float spawnAngle) { + + } + + @Override + public long getTime() { + return 0; + } + + @Override + public long getTimeOfDay() { + return 0; + } + + @Override + public boolean isThundering() { + return false; + } + + @Override + public boolean isRaining() { + return false; + } + + @Override + public void setRaining(boolean raining) { + + } + + @Override + public boolean isHardcore() { + return false; + } + + @Override + public GameRules getGameRules() { + return null; + } + + @Override + public Difficulty getDifficulty() { + return null; + } + + @Override + public boolean isDifficultyLocked() { + return false; + } + } +} diff --git a/extractor/src/main/java/dev/_00a/valence_extractor/Extractor.java b/extractor/src/main/java/dev/_00a/valence_extractor/Extractor.java index d85203a..b59e43d 100644 --- a/extractor/src/main/java/dev/_00a/valence_extractor/Extractor.java +++ b/extractor/src/main/java/dev/_00a/valence_extractor/Extractor.java @@ -2,12 +2,26 @@ package dev._00a.valence_extractor; import com.google.gson.*; import net.fabricmc.api.ModInitializer; +import net.minecraft.block.BlockState; import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityPose; import net.minecraft.entity.EntityType; +import net.minecraft.entity.data.DataTracker; import net.minecraft.entity.data.TrackedData; import net.minecraft.entity.data.TrackedDataHandlerRegistry; +import net.minecraft.entity.passive.CatVariant; +import net.minecraft.entity.passive.FrogVariant; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.text.Text; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.EulerAngle; +import net.minecraft.util.math.GlobalPos; import net.minecraft.util.registry.Registry; +import net.minecraft.util.registry.RegistryEntry; +import net.minecraft.village.VillagerData; import net.minecraft.world.EmptyBlockView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,8 +33,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashSet; -import java.util.Locale; +import java.util.*; public class Extractor implements ModInitializer { public static final String MOD_ID = "valence_extractor"; @@ -28,6 +41,90 @@ public class Extractor implements ModInitializer { private Gson gson; private Path outputDirectory; + private static JsonElement trackedDataToJson(Object data) { + if (data instanceof BlockPos bp) { + var json = new JsonObject(); + json.addProperty("x", bp.getX()); + json.addProperty("y", bp.getY()); + json.addProperty("z", bp.getZ()); + return json; + } else if (data instanceof Boolean b) { + return new JsonPrimitive(b); + } else if (data instanceof Byte b) { + return new JsonPrimitive(b); + } else if (data instanceof CatVariant cv) { + return new JsonPrimitive(Registry.CAT_VARIANT.getId(cv).getPath()); + } else if (data instanceof EntityPose ep) { + return new JsonPrimitive(ep.toString()); + } else if (data instanceof Direction d) { + return new JsonPrimitive(d.toString()); + } else if (data instanceof Float f) { + return new JsonPrimitive(f); + } else if (data instanceof FrogVariant fv) { + return new JsonPrimitive(Registry.FROG_VARIANT.getId(fv).getPath()); + } else if (data instanceof Integer i) { + return new JsonPrimitive(i); + } else if (data instanceof ItemStack is) { + // TODO + return new JsonPrimitive(is.toString()); + } else if (data instanceof NbtCompound nbt) { + // TODO: base64 binary representation or SNBT? + return new JsonPrimitive(nbt.toString()); + } else if (data instanceof Optional opt) { + var inner = opt.orElse(null); + if (inner == null) { + return null; + } else if (inner instanceof BlockPos) { + return Extractor.trackedDataToJson(inner); + } else if (inner instanceof BlockState bs) { + // TODO: get raw block state ID. + return new JsonPrimitive(bs.toString()); + } else if (inner instanceof GlobalPos gp) { + var json = new JsonObject(); + json.addProperty("dimension", gp.getDimension().getValue().toString()); + + var posJson = new JsonObject(); + posJson.addProperty("x", gp.getPos().getX()); + posJson.addProperty("y", gp.getPos().getY()); + posJson.addProperty("z", gp.getPos().getZ()); + + json.add("position", posJson); + return json; + } else if (inner instanceof Text) { + return Extractor.trackedDataToJson(inner); + } else if (inner instanceof UUID uuid) { + return new JsonPrimitive(uuid.toString()); + } else { + throw new IllegalArgumentException("Unknown tracked optional type " + inner.getClass().getName()); + } + } else if (data instanceof OptionalInt oi) { + return oi.isPresent() ? new JsonPrimitive(oi.getAsInt()) : null; + } else if (data instanceof RegistryEntry re) { + return new JsonPrimitive(re.getKey().map(k -> k.getValue().getPath()).orElse("")); + } else if (data instanceof ParticleEffect pe) { + return new JsonPrimitive(pe.asString()); + } else if (data instanceof EulerAngle ea) { + var json = new JsonObject(); + json.addProperty("yaw", ea.getYaw()); + json.addProperty("pitch", ea.getPitch()); + json.addProperty("roll", ea.getRoll()); + return json; + } else if (data instanceof String s) { + return new JsonPrimitive(s); + } else if (data instanceof Text t) { + // TODO: return text as json element. + return new JsonPrimitive(t.getString()); + } else if (data instanceof VillagerData vd) { + var json = new JsonObject(); + json.addProperty("level", vd.getLevel()); + json.addProperty("type", vd.getType().toString()); + json.addProperty("profession", vd.getProfession().toString()); + return json; + } + + throw new IllegalArgumentException("Unexpected tracked type " + data.getClass().getName()); + } + @Override public void onInitialize() { LOGGER.info("Starting extractor..."); @@ -43,11 +140,11 @@ public class Extractor implements ModInitializer { System.exit(1); } - LOGGER.info("Extractor finished successfully"); + LOGGER.info("Extractor finished successfully."); System.exit(0); } - void extractBlocks() throws IOException { + private void extractBlocks() throws IOException { var blocksJson = new JsonArray(); var stateIdCounter = 0; @@ -110,37 +207,45 @@ public class Extractor implements ModInitializer { } @SuppressWarnings("unchecked") - void extractEntities() throws IOException, IllegalAccessException { - var entitiesJson = new JsonArray(); - var entityClasses = new HashSet>(); + private void extractEntities() throws IOException, IllegalAccessException, NoSuchFieldException { + final var entitiesJson = new JsonArray(); + final var entityClasses = new HashSet>(); + + final var dummyWorld = DummyWorld.INSTANCE; for (var f : EntityType.class.getFields()) { if (f.getType().equals(EntityType.class)) { var entityType = (EntityType) f.get(null); var entityClass = (Class) ((ParameterizedType) f.getGenericType()).getActualTypeArguments()[0]; - var entityJson = new JsonObject(); - while (entityClasses.add(entityClass)) { - entityJson.addProperty("class", entityClass.getSimpleName()); + // While we can use the tracked data registry and reflection to get the tracked fields on entities, we won't know what their default values are because they are assigned in the entity's constructor. + // To obtain this, we create a dummy world to spawn the entities into and then read the data tracker field from the base entity class. + // We also handle player entities specially since they cannot be spawned with EntityType#create. + final var entityInstance = entityType.equals(EntityType.PLAYER) ? DummyPlayerEntity.INSTANCE : entityType.create(dummyWorld); - if (entityType != null) { - entityJson.addProperty("translation_key", entityType.getTranslationKey()); - } else { - entityJson.add("translation_key", null); - } + var dataTrackerField = Entity.class.getDeclaredField("dataTracker"); + dataTrackerField.setAccessible(true); + + while (entityClasses.add(entityClass)) { + var entityJson = new JsonObject(); + entityJson.addProperty("class", entityClass.getSimpleName()); + entityJson.add("translation_key", entityType != null ? new JsonPrimitive(entityType.getTranslationKey()) : null); var fieldsJson = new JsonArray(); for (var entityField : entityClass.getDeclaredFields()) { if (entityField.getType().equals(TrackedData.class)) { entityField.setAccessible(true); - var data = (TrackedData) entityField.get(null); + var data = (TrackedData) entityField.get(null); var fieldJson = new JsonObject(); fieldJson.addProperty("name", entityField.getName().toLowerCase(Locale.ROOT)); fieldJson.addProperty("index", data.getId()); fieldJson.addProperty("type_id", TrackedDataHandlerRegistry.getId(data.getType())); + var dataTracker = (DataTracker) dataTrackerField.get(entityInstance); + fieldJson.add("default_value", Extractor.trackedDataToJson(dataTracker.get(data))); + fieldsJson.add(fieldJson); } } @@ -156,9 +261,6 @@ public class Extractor implements ModInitializer { entityClass = (Class) parent; entityType = null; - } - - if (entityJson.size() > 0) { entitiesJson.add(entityJson); } } @@ -167,7 +269,7 @@ public class Extractor implements ModInitializer { writeJsonFile("entities.json", entitiesJson); } - void writeJsonFile(String fileName, JsonElement element) throws IOException { + private void writeJsonFile(String fileName, JsonElement element) throws IOException { var out = outputDirectory.resolve(fileName); var fileWriter = new FileWriter(out.toFile(), StandardCharsets.UTF_8); gson.toJson(element, fileWriter); diff --git a/extractor/src/main/java/dev/_00a/valence_extractor/Util.java b/extractor/src/main/java/dev/_00a/valence_extractor/Util.java new file mode 100644 index 0000000..173ae32 --- /dev/null +++ b/extractor/src/main/java/dev/_00a/valence_extractor/Util.java @@ -0,0 +1,19 @@ +package dev._00a.valence_extractor; + +import sun.reflect.ReflectionFactory; + +public class Util { + /** + * Magically creates an instance of a concrete class without calling its constructor. + */ + public static T magicallyInstantiate(Class clazz) { + var rf = ReflectionFactory.getReflectionFactory(); + try { + var objCon = Object.class.getDeclaredConstructor(); + var con = rf.newConstructorForSerialization(clazz, objCon); + return clazz.cast(con.newInstance()); + } catch (Throwable e) { + throw new IllegalArgumentException("Failed to magically instantiate " + clazz.getName(), e); + } + } +}