Extract default values for entity fields

This commit is contained in:
Ryan 2022-07-20 22:51:01 -07:00
parent b9fca3503c
commit 37b285209f
4 changed files with 438 additions and 20 deletions

View file

@ -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;
}
}

View file

@ -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<World> registryRef, RegistryEntry<DimensionType> dimension, Supplier<Profiler> 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<Entity> getEntityLookup() {
return null;
}
@Override
public QueryableTickScheduler<Block> getBlockTickScheduler() {
return null;
}
@Override
public QueryableTickScheduler<Fluid> 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<? extends PlayerEntity> getPlayers() {
return List.of();
}
@Override
public RegistryEntry<Biome> 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;
}
}
}

View file

@ -2,12 +2,26 @@ package dev._00a.valence_extractor;
import com.google.gson.*; import com.google.gson.*;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.minecraft.block.BlockState;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityPose;
import net.minecraft.entity.EntityType; import net.minecraft.entity.EntityType;
import net.minecraft.entity.data.DataTracker;
import net.minecraft.entity.data.TrackedData; import net.minecraft.entity.data.TrackedData;
import net.minecraft.entity.data.TrackedDataHandlerRegistry; 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.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.Registry;
import net.minecraft.util.registry.RegistryEntry;
import net.minecraft.village.VillagerData;
import net.minecraft.world.EmptyBlockView; import net.minecraft.world.EmptyBlockView;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -19,8 +33,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.HashSet; import java.util.*;
import java.util.Locale;
public class Extractor implements ModInitializer { public class Extractor implements ModInitializer {
public static final String MOD_ID = "valence_extractor"; public static final String MOD_ID = "valence_extractor";
@ -28,6 +41,90 @@ public class Extractor implements ModInitializer {
private Gson gson; private Gson gson;
private Path outputDirectory; 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 @Override
public void onInitialize() { public void onInitialize() {
LOGGER.info("Starting extractor..."); LOGGER.info("Starting extractor...");
@ -43,11 +140,11 @@ public class Extractor implements ModInitializer {
System.exit(1); System.exit(1);
} }
LOGGER.info("Extractor finished successfully"); LOGGER.info("Extractor finished successfully.");
System.exit(0); System.exit(0);
} }
void extractBlocks() throws IOException { private void extractBlocks() throws IOException {
var blocksJson = new JsonArray(); var blocksJson = new JsonArray();
var stateIdCounter = 0; var stateIdCounter = 0;
@ -110,37 +207,45 @@ public class Extractor implements ModInitializer {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
void extractEntities() throws IOException, IllegalAccessException { private void extractEntities() throws IOException, IllegalAccessException, NoSuchFieldException {
var entitiesJson = new JsonArray(); final var entitiesJson = new JsonArray();
var entityClasses = new HashSet<Class<? extends Entity>>(); final var entityClasses = new HashSet<Class<? extends Entity>>();
final var dummyWorld = DummyWorld.INSTANCE;
for (var f : EntityType.class.getFields()) { for (var f : EntityType.class.getFields()) {
if (f.getType().equals(EntityType.class)) { if (f.getType().equals(EntityType.class)) {
var entityType = (EntityType<?>) f.get(null); var entityType = (EntityType<?>) f.get(null);
var entityClass = (Class<? extends Entity>) ((ParameterizedType) f.getGenericType()).getActualTypeArguments()[0]; var entityClass = (Class<? extends Entity>) ((ParameterizedType) f.getGenericType()).getActualTypeArguments()[0];
var entityJson = new JsonObject(); // 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.
while (entityClasses.add(entityClass)) { // 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.
entityJson.addProperty("class", entityClass.getSimpleName()); // 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) { var dataTrackerField = Entity.class.getDeclaredField("dataTracker");
entityJson.addProperty("translation_key", entityType.getTranslationKey()); dataTrackerField.setAccessible(true);
} else {
entityJson.add("translation_key", null); 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(); var fieldsJson = new JsonArray();
for (var entityField : entityClass.getDeclaredFields()) { for (var entityField : entityClass.getDeclaredFields()) {
if (entityField.getType().equals(TrackedData.class)) { if (entityField.getType().equals(TrackedData.class)) {
entityField.setAccessible(true); entityField.setAccessible(true);
var data = (TrackedData<? extends Entity>) entityField.get(null); var data = (TrackedData<?>) entityField.get(null);
var fieldJson = new JsonObject(); var fieldJson = new JsonObject();
fieldJson.addProperty("name", entityField.getName().toLowerCase(Locale.ROOT)); fieldJson.addProperty("name", entityField.getName().toLowerCase(Locale.ROOT));
fieldJson.addProperty("index", data.getId()); fieldJson.addProperty("index", data.getId());
fieldJson.addProperty("type_id", TrackedDataHandlerRegistry.getId(data.getType())); 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); fieldsJson.add(fieldJson);
} }
} }
@ -156,9 +261,6 @@ public class Extractor implements ModInitializer {
entityClass = (Class<? extends Entity>) parent; entityClass = (Class<? extends Entity>) parent;
entityType = null; entityType = null;
}
if (entityJson.size() > 0) {
entitiesJson.add(entityJson); entitiesJson.add(entityJson);
} }
} }
@ -167,7 +269,7 @@ public class Extractor implements ModInitializer {
writeJsonFile("entities.json", entitiesJson); 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 out = outputDirectory.resolve(fileName);
var fileWriter = new FileWriter(out.toFile(), StandardCharsets.UTF_8); var fileWriter = new FileWriter(out.toFile(), StandardCharsets.UTF_8);
gson.toJson(element, fileWriter); gson.toJson(element, fileWriter);

View file

@ -0,0 +1,19 @@
package dev._00a.valence_extractor;
import sun.reflect.ReflectionFactory;
public class Util {
/**
* Magically creates an instance of a <i>concrete</i> class without calling its constructor.
*/
public static <T> T magicallyInstantiate(Class<T> 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);
}
}
}