mirror of
https://github.com/Anuken/Mindustry.git
synced 2025-07-20 20:57:59 +07:00
989 lines
41 KiB
Java
989 lines
41 KiB
Java
package mindustry.mod;
|
|
|
|
import arc.*;
|
|
import arc.assets.*;
|
|
import arc.assets.loaders.SoundLoader.*;
|
|
import arc.audio.*;
|
|
import arc.files.*;
|
|
import arc.func.*;
|
|
import arc.graphics.*;
|
|
import arc.graphics.g2d.*;
|
|
import arc.math.*;
|
|
import arc.math.geom.*;
|
|
import arc.struct.*;
|
|
import arc.util.*;
|
|
import arc.util.serialization.*;
|
|
import arc.util.serialization.Json.*;
|
|
import arc.util.serialization.Jval.*;
|
|
import mindustry.*;
|
|
import mindustry.ai.types.*;
|
|
import mindustry.content.*;
|
|
import mindustry.content.TechTree.*;
|
|
import mindustry.ctype.*;
|
|
import mindustry.entities.*;
|
|
import mindustry.entities.Units.*;
|
|
import mindustry.entities.abilities.*;
|
|
import mindustry.entities.bullet.*;
|
|
import mindustry.entities.effect.*;
|
|
import mindustry.entities.part.*;
|
|
import mindustry.entities.part.DrawPart.*;
|
|
import mindustry.entities.pattern.*;
|
|
import mindustry.game.*;
|
|
import mindustry.game.Objectives.*;
|
|
import mindustry.gen.*;
|
|
import mindustry.graphics.*;
|
|
import mindustry.graphics.g3d.*;
|
|
import mindustry.graphics.g3d.PlanetGrid.*;
|
|
import mindustry.io.*;
|
|
import mindustry.maps.generators.*;
|
|
import mindustry.maps.planet.*;
|
|
import mindustry.mod.Mods.*;
|
|
import mindustry.type.*;
|
|
import mindustry.type.ammo.*;
|
|
import mindustry.type.weather.*;
|
|
import mindustry.world.*;
|
|
import mindustry.world.blocks.units.*;
|
|
import mindustry.world.blocks.units.UnitFactory.*;
|
|
import mindustry.world.consumers.*;
|
|
import mindustry.world.draw.*;
|
|
import mindustry.world.meta.*;
|
|
|
|
import java.lang.reflect.*;
|
|
|
|
import static mindustry.Vars.*;
|
|
|
|
@SuppressWarnings("unchecked")
|
|
public class ContentParser{
|
|
private static final boolean ignoreUnknownFields = true;
|
|
ObjectMap<Class<?>, ContentType> contentTypes = new ObjectMap<>();
|
|
ObjectSet<Class<?>> implicitNullable = ObjectSet.with(TextureRegion.class, TextureRegion[].class, TextureRegion[][].class, TextureRegion[][][].class);
|
|
ObjectMap<String, AssetDescriptor<?>> sounds = new ObjectMap<>();
|
|
Seq<ParseListener> listeners = new Seq<>();
|
|
|
|
ObjectMap<Class<?>, FieldParser> classParsers = new ObjectMap<>(){{
|
|
put(Effect.class, (type, data) -> {
|
|
if(data.isString()){
|
|
return field(Fx.class, data);
|
|
}
|
|
Class<? extends Effect> bc = resolve(data.getString("type", ""), ParticleEffect.class);
|
|
data.remove("type");
|
|
Effect result = make(bc);
|
|
readFields(result, data);
|
|
return result;
|
|
});
|
|
put(Sortf.class, (type, data) -> field(UnitSorts.class, data));
|
|
put(Interp.class, (type, data) -> field(Interp.class, data));
|
|
put(Blending.class, (type, data) -> field(Blending.class, data));
|
|
put(CacheLayer.class, (type, data) -> field(CacheLayer.class, data));
|
|
put(Attribute.class, (type, data) -> Attribute.get(data.asString()));
|
|
put(Schematic.class, (type, data) -> {
|
|
Object result = fieldOpt(Loadouts.class, data);
|
|
if(result != null){
|
|
return result;
|
|
}else{
|
|
String str = data.asString();
|
|
if(str.startsWith(Vars.schematicBaseStart)){
|
|
return Schematics.readBase64(str);
|
|
}else{
|
|
return Schematics.read(Vars.tree.get("schematics/" + str + "." + Vars.schematicExtension));
|
|
}
|
|
}
|
|
});
|
|
put(Color.class, (type, data) -> Color.valueOf(data.asString()));
|
|
put(StatusEffect.class, (type, data) -> {
|
|
if(data.isString()){
|
|
StatusEffect result = locate(ContentType.status, data.asString());
|
|
if(result != null) return result;
|
|
result = (StatusEffect)fieldOpt(StatusEffects.class, data);
|
|
if(result != null) return result;
|
|
throw new IllegalArgumentException("Unknown status effect: '" + data.asString() + "'");
|
|
}
|
|
StatusEffect effect = new StatusEffect(currentMod.name + "-" + data.getString("name"));
|
|
effect.minfo.mod = currentMod;
|
|
readFields(effect, data);
|
|
return effect;
|
|
});
|
|
put(BulletType.class, (type, data) -> {
|
|
if(data.isString()){
|
|
return field(Bullets.class, data);
|
|
}
|
|
var bc = resolve(data.getString("type", ""), BasicBulletType.class);
|
|
data.remove("type");
|
|
BulletType result = make(bc);
|
|
readFields(result, data);
|
|
return result;
|
|
});
|
|
put(AmmoType.class, (type, data) -> {
|
|
//string -> item
|
|
//if liquid ammo support is added, this should scan for liquids as well
|
|
if(data.isString()) return new ItemAmmoType(find(ContentType.item, data.asString()));
|
|
//number -> power
|
|
if(data.isNumber()) return new PowerAmmoType(data.asFloat());
|
|
|
|
var bc = resolve(data.getString("type", ""), ItemAmmoType.class);
|
|
data.remove("type");
|
|
AmmoType result = make(bc);
|
|
readFields(result, data);
|
|
return result;
|
|
});
|
|
put(DrawBlock.class, (type, data) -> {
|
|
if(data.isString()){
|
|
//try to instantiate
|
|
return make(resolve(data.asString()));
|
|
}
|
|
//array is shorthand for DrawMulti
|
|
if(data.isArray()){
|
|
return new DrawMulti(parser.readValue(DrawBlock[].class, data));
|
|
}
|
|
var bc = resolve(data.getString("type", ""), DrawDefault.class);
|
|
data.remove("type");
|
|
var result = make(bc);
|
|
readFields(result, data);
|
|
return result;
|
|
});
|
|
put(ShootPattern.class, (type, data) -> {
|
|
var bc = resolve(data.getString("type", ""), ShootPattern.class);
|
|
data.remove("type");
|
|
var result = make(bc);
|
|
readFields(result, data);
|
|
return result;
|
|
});
|
|
put(DrawPart.class, (type, data) -> {
|
|
var bc = resolve(data.getString("type", ""), RegionPart.class);
|
|
data.remove("type");
|
|
var result = make(bc);
|
|
readFields(result, data);
|
|
return result;
|
|
});
|
|
//TODO this is untested
|
|
put(PartProgress.class, (type, data) -> {
|
|
//simple case: it's a string or number constant
|
|
if(data.isString()) return field(PartProgress.class, data.asString());
|
|
if(data.isNumber()) return PartProgress.constant(data.asFloat());
|
|
|
|
if(!data.has("type")){
|
|
throw new RuntimeException("PartProgress object need a 'type' string field. Check the PartProgress class for a list of constants.");
|
|
}
|
|
|
|
PartProgress base = (PartProgress)field(PartProgress.class, data.getString("type"));
|
|
|
|
JsonValue opval =
|
|
data.has("operation") ? data.get("operation") :
|
|
data.has("op") ? data.get("op") : null;
|
|
|
|
//no operations I guess (why would you do this?)
|
|
if(opval == null){
|
|
return base;
|
|
}
|
|
|
|
//this is the name of the method to call
|
|
String op = opval.asString();
|
|
|
|
//I have to hard-code this, no easy way of getting parameter names, unfortunately
|
|
return switch(op){
|
|
case "inv" -> base.inv();
|
|
case "clamp" -> base.clamp();
|
|
case "delay" -> base.delay(data.getFloat("amount"));
|
|
case "shorten" -> base.shorten(data.getFloat("amount"));
|
|
case "add" -> base.add(data.getFloat("amount"));
|
|
case "blend" -> base.blend(parser.readValue(PartProgress.class, data.get("other")), data.getFloat("amount"));
|
|
case "mul" -> base.mul(parser.readValue(PartProgress.class, data.get("other")));
|
|
case "min" -> base.min(parser.readValue(PartProgress.class, data.get("other")));
|
|
case "sin" -> base.sin(data.getFloat("scl"), data.getFloat("mag"));
|
|
case "absin" -> base.absin(data.getFloat("scl"), data.getFloat("mag"));
|
|
case "curve" -> base.curve(parser.readValue(Interp.class, data.get("interp")));
|
|
default -> throw new RuntimeException("Unknown operation '" + op + "', check PartProgress class for a list of methods.");
|
|
};
|
|
});
|
|
put(PlanetGenerator.class, (type, data) -> {
|
|
var result = new AsteroidGenerator(); //only one type for now
|
|
readFields(result, data);
|
|
return result;
|
|
});
|
|
put(GenericMesh.class, (type, data) -> {
|
|
if(!data.isObject()) throw new RuntimeException("Meshes must be objects.");
|
|
if(!(currentContent instanceof Planet planet)) throw new RuntimeException("Meshes can only be parsed as parts of planets.");
|
|
|
|
String tname = Strings.capitalize(data.getString("type", "NoiseMesh"));
|
|
|
|
return switch(tname){
|
|
//TODO NoiseMesh is bad
|
|
case "NoiseMesh" -> new NoiseMesh(planet,
|
|
data.getInt("seed", 0), data.getInt("divisions", 1), data.getFloat("radius", 1f),
|
|
data.getInt("octaves", 1), data.getFloat("persistence", 0.5f), data.getFloat("scale", 1f), data.getFloat("mag", 0.5f),
|
|
Color.valueOf(data.getString("color1", data.getString("color", "ffffff"))),
|
|
Color.valueOf(data.getString("color2", data.getString("color", "ffffff"))),
|
|
data.getInt("colorOct", 1), data.getFloat("colorPersistence", 0.5f), data.getFloat("colorScale", 1f),
|
|
data.getFloat("colorThreshold", 0.5f));
|
|
case "MultiMesh" -> new MultiMesh(parser.readValue(GenericMesh[].class, data.get("meshes")));
|
|
case "MatMesh" -> new MatMesh(parser.readValue(GenericMesh.class, data.get("mesh")), parser.readValue(Mat3D.class, data.get("mat")));
|
|
default -> throw new RuntimeException("Unknown mesh type: " + tname);
|
|
};
|
|
});
|
|
put(Mat3D.class, (type, data) -> {
|
|
if(data == null) return new Mat3D();
|
|
|
|
//transform x y z format
|
|
if(data.has("x") && data.has("y") && data.has("z")){
|
|
return new Mat3D().translate(data.getFloat("x", 0f), data.getFloat("y", 0f), data.getFloat("z", 0f));
|
|
}
|
|
|
|
//transform array format
|
|
if(data.isArray() && data.size == 3){
|
|
return new Mat3D().setToTranslation(new Vec3(data.asFloatArray()));
|
|
}
|
|
|
|
Mat3D mat = new Mat3D();
|
|
|
|
//TODO this is kinda bad
|
|
for(var val : data){
|
|
switch(val.name){
|
|
case "translate", "trans" -> mat.translate(parser.readValue(Vec3.class, data));
|
|
case "scale", "scl" -> mat.scale(parser.readValue(Vec3.class, data));
|
|
case "rotate", "rot" -> mat.rotate(parser.readValue(Vec3.class, data), data.getFloat("degrees", 0f));
|
|
case "multiply", "mul" -> mat.mul(parser.readValue(Mat3D.class, data));
|
|
case "x", "y", "z" -> {}
|
|
default -> throw new RuntimeException("Unknown matrix transformation: '" + val.name + "'");
|
|
}
|
|
}
|
|
|
|
return mat;
|
|
});
|
|
put(Vec3.class, (type, data) -> {
|
|
if(data.isArray()) return new Vec3(data.asFloatArray());
|
|
return new Vec3(data.getFloat("x", 0f), data.getFloat("y", 0f), data.getFloat("z", 0f));
|
|
});
|
|
put(Sound.class, (type, data) -> {
|
|
if(fieldOpt(Sounds.class, data) != null) return fieldOpt(Sounds.class, data);
|
|
if(Vars.headless) return new Sound();
|
|
|
|
String name = "sounds/" + data.asString();
|
|
String path = Vars.tree.get(name + ".ogg").exists() ? name + ".ogg" : name + ".mp3";
|
|
|
|
if(sounds.containsKey(path)) return ((SoundParameter)sounds.get(path).params).sound;
|
|
var sound = new Sound();
|
|
AssetDescriptor<?> desc = Core.assets.load(path, Sound.class, new SoundParameter(sound));
|
|
desc.errored = Throwable::printStackTrace;
|
|
sounds.put(path, desc);
|
|
return sound;
|
|
});
|
|
put(Objectives.Objective.class, (type, data) -> {
|
|
if(data.isString()){
|
|
var cont = locateAny(data.asString());
|
|
if(cont == null) throw new IllegalArgumentException("Unknown objective content: " + data.asString());
|
|
return new Research((UnlockableContent)cont);
|
|
}
|
|
var oc = resolve(data.getString("type", ""), SectorComplete.class);
|
|
data.remove("type");
|
|
Objectives.Objective obj = make(oc);
|
|
readFields(obj, data);
|
|
return obj;
|
|
});
|
|
|
|
put(Ability.class, (type, data) -> {
|
|
Class<? extends Ability> oc = resolve(data.getString("type", ""));
|
|
data.remove("type");
|
|
Ability obj = make(oc);
|
|
readFields(obj, data);
|
|
return obj;
|
|
});
|
|
put(Weapon.class, (type, data) -> {
|
|
var oc = resolve(data.getString("type", ""), Weapon.class);
|
|
data.remove("type");
|
|
var weapon = make(oc);
|
|
readFields(weapon, data);
|
|
weapon.name = currentMod.name + "-" + weapon.name;
|
|
return weapon;
|
|
});
|
|
}};
|
|
/** Stores things that need to be parsed fully, e.g. reading fields of content.
|
|
* This is done to accommodate binding of content names first.*/
|
|
private Seq<Runnable> reads = new Seq<>();
|
|
private Seq<Runnable> postreads = new Seq<>();
|
|
private ObjectSet<Object> toBeParsed = new ObjectSet<>();
|
|
|
|
LoadedMod currentMod;
|
|
Content currentContent;
|
|
|
|
private Json parser = new Json(){
|
|
@Override
|
|
public <T> T readValue(Class<T> type, Class elementType, JsonValue jsonData, Class keyType){
|
|
T t = internalRead(type, elementType, jsonData, keyType);
|
|
if(t != null && !Reflect.isWrapper(t.getClass()) && (type == null || !type.isPrimitive())){
|
|
checkNullFields(t);
|
|
listeners.each(hook -> hook.parsed(type, jsonData, t));
|
|
}
|
|
return t;
|
|
}
|
|
|
|
private <T> T internalRead(Class<T> type, Class elementType, JsonValue jsonData, Class keyType){
|
|
if(type != null){
|
|
if(classParsers.containsKey(type)){
|
|
try{
|
|
return (T)classParsers.get(type).parse(type, jsonData);
|
|
}catch(Exception e){
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
//try to parse env bits
|
|
if((type == int.class || type == Integer.class) && jsonData.isArray()){
|
|
int value = 0;
|
|
for(var str : jsonData){
|
|
if(!str.isString()) throw new SerializationException("Integer bitfield values must all be strings. Found: " + str);
|
|
String field = str.asString();
|
|
value |= Reflect.<Integer>get(Env.class, field);
|
|
}
|
|
|
|
return (T)(Integer)value;
|
|
}
|
|
|
|
//try to parse "item/amount" syntax
|
|
if(type == ItemStack.class && jsonData.isString() && jsonData.asString().contains("/")){
|
|
String[] split = jsonData.asString().split("/");
|
|
|
|
return (T)fromJson(ItemStack.class, "{item: " + split[0] + ", amount: " + split[1] + "}");
|
|
}
|
|
|
|
//try to parse "liquid/amount" syntax
|
|
if(jsonData.isString() && jsonData.asString().contains("/")){
|
|
String[] split = jsonData.asString().split("/");
|
|
if(type == LiquidStack.class){
|
|
return (T)fromJson(LiquidStack.class, "{liquid: " + split[0] + ", amount: " + split[1] + "}");
|
|
}else if(type == ConsumeLiquid.class){
|
|
return (T)fromJson(ConsumeLiquid.class, "{liquid: " + split[0] + ", amount: " + split[1] + "}");
|
|
}
|
|
}
|
|
|
|
if(Content.class.isAssignableFrom(type)){
|
|
ContentType ctype = contentTypes.getThrow(type, () -> new IllegalArgumentException("No content type for class: " + type.getSimpleName()));
|
|
String prefix = currentMod != null ? currentMod.name + "-" : "";
|
|
T one = (T)Vars.content.getByName(ctype, prefix + jsonData.asString());
|
|
if(one != null) return one;
|
|
T two = (T)Vars.content.getByName(ctype, jsonData.asString());
|
|
|
|
if(two != null) return two;
|
|
throw new IllegalArgumentException("\"" + jsonData.name + "\": No " + ctype + " found with name '" + jsonData.asString() + "'.\nMake sure '" + jsonData.asString() + "' is spelled correctly, and that it really exists!\nThis may also occur because its file failed to parse.");
|
|
}
|
|
}
|
|
|
|
return super.readValue(type, elementType, jsonData, keyType);
|
|
}
|
|
};
|
|
|
|
private ObjectMap<ContentType, TypeParser<?>> parsers = ObjectMap.of(
|
|
ContentType.block, (TypeParser<Block>)(mod, name, value) -> {
|
|
readBundle(ContentType.block, name, value);
|
|
|
|
Block block;
|
|
|
|
if(locate(ContentType.block, name) != null){
|
|
if(value.has("type")){
|
|
Log.warn("Warning: '" + currentMod.name + "-" + name + "' re-declares a type. This will be interpreted as a new block. If you wish to override a vanilla block, omit the 'type' section, as vanilla block `type`s cannot be changed.");
|
|
block = make(resolve(value.getString("type", ""), Block.class), mod + "-" + name);
|
|
}else{
|
|
block = locate(ContentType.block, name);
|
|
}
|
|
}else{
|
|
block = make(resolve(value.getString("type", ""), Block.class), mod + "-" + name);
|
|
}
|
|
|
|
currentContent = block;
|
|
|
|
read(() -> {
|
|
if(value.has("consumes") && value.get("consumes").isObject()){
|
|
for(JsonValue child : value.get("consumes")){
|
|
switch(child.name){
|
|
case "item" -> block.consumeItem(find(ContentType.item, child.asString()));
|
|
case "items" -> block.consume((Consume)parser.readValue(ConsumeItems.class, child));
|
|
case "liquid" -> block.consume((Consume)parser.readValue(ConsumeLiquid.class, child));
|
|
case "liquids" -> block.consume((Consume)parser.readValue(ConsumeLiquids.class, child));
|
|
case "coolant" -> block.consume((Consume)parser.readValue(ConsumeCoolant.class, child));
|
|
case "power" -> {
|
|
if(child.isNumber()){
|
|
block.consumePower(child.asFloat());
|
|
}else{
|
|
block.consume((Consume)parser.readValue(ConsumePower.class, child));
|
|
}
|
|
}
|
|
case "powerBuffered" -> block.consumePowerBuffered(child.asFloat());
|
|
default -> throw new IllegalArgumentException("Unknown consumption type: '" + child.name + "' for block '" + block.name + "'.");
|
|
}
|
|
}
|
|
value.remove("consumes");
|
|
}
|
|
|
|
readFields(block, value, true);
|
|
|
|
if(block.size > maxBlockSize){
|
|
throw new IllegalArgumentException("Blocks cannot be larger than " + maxBlockSize);
|
|
}
|
|
|
|
//make block visible by default if there are requirements and no visibility set
|
|
if(value.has("requirements") && block.buildVisibility == BuildVisibility.hidden){
|
|
block.buildVisibility = BuildVisibility.shown;
|
|
}
|
|
});
|
|
|
|
return block;
|
|
},
|
|
ContentType.unit, (TypeParser<UnitType>)(mod, name, value) -> {
|
|
readBundle(ContentType.unit, name, value);
|
|
|
|
UnitType unit;
|
|
if(locate(ContentType.unit, name) == null){
|
|
unit = new UnitType(mod + "-" + name);
|
|
var typeVal = value.get("type");
|
|
|
|
if(typeVal != null && !typeVal.isString()){
|
|
throw new RuntimeException("Unit '" + name + "' has an incorrect type. Types must be strings.");
|
|
}
|
|
|
|
unit.constructor = unitType(typeVal);
|
|
}else{
|
|
unit = locate(ContentType.unit, name);
|
|
}
|
|
|
|
currentContent = unit;
|
|
//TODO test this!
|
|
read(() -> {
|
|
//add reconstructor type
|
|
if(value.has("requirements")){
|
|
JsonValue rec = value.remove("requirements");
|
|
|
|
UnitReq req = parser.readValue(UnitReq.class, rec);
|
|
|
|
if(req.block instanceof Reconstructor r){
|
|
if(req.previous != null){
|
|
r.upgrades.add(new UnitType[]{req.previous, unit});
|
|
}
|
|
}else if(req.block instanceof UnitFactory f){
|
|
f.plans.add(new UnitPlan(unit, req.time, req.requirements));
|
|
}else{
|
|
throw new IllegalArgumentException("Missing a valid 'block' in 'requirements'");
|
|
}
|
|
|
|
}
|
|
|
|
if(value.has("controller")){
|
|
unit.aiController = supply(resolve(value.getString("controller"), FlyingAI.class));
|
|
value.remove("controller");
|
|
}
|
|
|
|
if(value.has("defaultController")){
|
|
unit.controller = u -> supply(resolve(value.getString("defaultController"), FlyingAI.class)).get();
|
|
value.remove("defaultController");
|
|
}
|
|
|
|
//read extra default waves
|
|
if(value.has("waves")){
|
|
JsonValue waves = value.remove("waves");
|
|
SpawnGroup[] groups = parser.readValue(SpawnGroup[].class, waves);
|
|
for(SpawnGroup group : groups){
|
|
group.type = unit;
|
|
}
|
|
|
|
Vars.waves.get().addAll(groups);
|
|
}
|
|
|
|
readFields(unit, value, true);
|
|
});
|
|
|
|
return unit;
|
|
},
|
|
ContentType.weather, (TypeParser<Weather>)(mod, name, value) -> {
|
|
Weather item;
|
|
if(locate(ContentType.weather, name) != null){
|
|
item = locate(ContentType.weather, name);
|
|
readBundle(ContentType.weather, name, value);
|
|
}else{
|
|
readBundle(ContentType.weather, name, value);
|
|
item = make(resolve(getType(value), ParticleWeather.class), mod + "-" + name);
|
|
value.remove("type");
|
|
}
|
|
currentContent = item;
|
|
read(() -> readFields(item, value));
|
|
return item;
|
|
},
|
|
ContentType.item, parser(ContentType.item, Item::new),
|
|
ContentType.liquid, parser(ContentType.liquid, Liquid::new),
|
|
ContentType.status, parser(ContentType.status, StatusEffect::new),
|
|
ContentType.sector, (TypeParser<SectorPreset>)(mod, name, value) -> {
|
|
if(value.isString()){
|
|
return locate(ContentType.sector, name);
|
|
}
|
|
|
|
if(!value.has("sector") || !value.get("sector").isNumber()) throw new RuntimeException("SectorPresets must have a sector number.");
|
|
|
|
SectorPreset out = new SectorPreset(name, locate(ContentType.planet, value.getString("planet", "serpulo")), value.getInt("sector"));
|
|
value.remove("sector");
|
|
value.remove("planet");
|
|
currentContent = out;
|
|
read(() -> readFields(out, value));
|
|
return out;
|
|
},
|
|
ContentType.planet, (TypeParser<Planet>)(mod, name, value) -> {
|
|
if(value.isString()) return locate(ContentType.planet, name);
|
|
|
|
Planet parent = locate(ContentType.planet, value.getString("parent"));
|
|
Planet planet = new Planet(name, parent, value.getFloat("radius", 1f), value.getInt("sectorSize", 0));
|
|
|
|
if(value.has("mesh")){
|
|
planet.meshLoader = () -> parser.readValue(GenericMesh.class, value.get("mesh"));
|
|
}
|
|
|
|
//always one sector right now...
|
|
planet.sectors.add(new Sector(planet, Ptile.empty));
|
|
|
|
currentContent = planet;
|
|
read(() -> readFields(planet, value));
|
|
return planet;
|
|
}
|
|
);
|
|
|
|
private Prov<Unit> unitType(JsonValue value){
|
|
if(value == null) return UnitEntity::create;
|
|
return switch(value.asString()){
|
|
case "flying" -> UnitEntity::create;
|
|
case "mech" -> MechUnit::create;
|
|
case "legs" -> LegsUnit::create;
|
|
case "naval" -> UnitWaterMove::create;
|
|
case "payload" -> PayloadUnit::create;
|
|
case "missile" -> TimedKillUnit::create;
|
|
case "tether" -> BuildingTetherPayloadUnit::create;
|
|
default -> throw new RuntimeException("Invalid unit type: '" + value + "'. Must be 'flying/mech/legs/naval/payload/missile/tether'.");
|
|
};
|
|
}
|
|
|
|
private String getString(JsonValue value, String key){
|
|
if(value.has(key)){
|
|
return value.getString(key);
|
|
}else{
|
|
throw new IllegalArgumentException("You are missing a \"" + key + "\". It must be added before the file can be parsed.");
|
|
}
|
|
}
|
|
|
|
private String getType(JsonValue value){
|
|
return getString(value, "type");
|
|
}
|
|
|
|
private <T extends Content> T find(ContentType type, String name){
|
|
Content c = Vars.content.getByName(type, name);
|
|
if(c == null) c = Vars.content.getByName(type, currentMod.name + "-" + name);
|
|
if(c == null) throw new IllegalArgumentException("No " + type + " found with name '" + name + "'");
|
|
return (T)c;
|
|
}
|
|
|
|
private <T extends Content> TypeParser<T> parser(ContentType type, Func<String, T> constructor){
|
|
return (mod, name, value) -> {
|
|
T item;
|
|
if(locate(type, name) != null){
|
|
item = (T)locate(type, name);
|
|
readBundle(type, name, value);
|
|
}else{
|
|
readBundle(type, name, value);
|
|
item = constructor.get(mod + "-" + name);
|
|
}
|
|
currentContent = item;
|
|
read(() -> readFields(item, value));
|
|
return item;
|
|
};
|
|
}
|
|
|
|
private void readBundle(ContentType type, String name, JsonValue value){
|
|
UnlockableContent cont = locate(type, name) instanceof UnlockableContent ? locate(type, name) : null;
|
|
|
|
String entryName = cont == null ? type + "." + currentMod.name + "-" + name + "." : type + "." + cont.name + ".";
|
|
I18NBundle bundle = Core.bundle;
|
|
while(bundle.getParent() != null) bundle = bundle.getParent();
|
|
|
|
if(value.has("name")){
|
|
if(!Core.bundle.has(entryName + "name")){
|
|
bundle.getProperties().put(entryName + "name", value.getString("name"));
|
|
if(cont != null) cont.localizedName = value.getString("name");
|
|
}
|
|
value.remove("name");
|
|
}
|
|
|
|
if(value.has("description")){
|
|
if(!Core.bundle.has(entryName + "description")){
|
|
bundle.getProperties().put(entryName + "description", value.getString("description"));
|
|
if(cont != null) cont.description = value.getString("description");
|
|
}
|
|
value.remove("description");
|
|
}
|
|
}
|
|
|
|
/** Call to read a content's extra info later.*/
|
|
private void read(Runnable run){
|
|
Content cont = currentContent;
|
|
LoadedMod mod = currentMod;
|
|
reads.add(() -> {
|
|
this.currentMod = mod;
|
|
this.currentContent = cont;
|
|
run.run();
|
|
|
|
//check nulls after parsing
|
|
if(cont != null){
|
|
toBeParsed.remove(cont);
|
|
checkNullFields(cont);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void init(){
|
|
for(ContentType type : ContentType.all){
|
|
Seq<Content> arr = Vars.content.getBy(type);
|
|
if(!arr.isEmpty()){
|
|
Class<?> c = arr.first().getClass();
|
|
//get base content class, skipping intermediates
|
|
while(!(c.getSuperclass() == Content.class || c.getSuperclass() == UnlockableContent.class || Modifier.isAbstract(c.getSuperclass().getModifiers()))){
|
|
c = c.getSuperclass();
|
|
}
|
|
|
|
contentTypes.put(c, type);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void attempt(Runnable run){
|
|
try{
|
|
run.run();
|
|
}catch(Throwable t){
|
|
Log.err(t);
|
|
//don't overwrite double errors
|
|
markError(currentContent, t);
|
|
}
|
|
}
|
|
|
|
public void finishParsing(){
|
|
reads.each(this::attempt);
|
|
postreads.each(this::attempt);
|
|
reads.clear();
|
|
postreads.clear();
|
|
toBeParsed.clear();
|
|
}
|
|
|
|
/**
|
|
* Parses content from a json file.
|
|
* @param name the name of the file without its extension
|
|
* @param json the json to parse
|
|
* @param type the type of content this is
|
|
* @param file file that this content is being parsed from
|
|
* @return the content that was parsed
|
|
*/
|
|
public Content parse(LoadedMod mod, String name, String json, Fi file, ContentType type) throws Exception{
|
|
if(contentTypes.isEmpty()){
|
|
init();
|
|
}
|
|
|
|
//remove extra # characters to make it valid json... apparently some people have *unquoted* # characters in their json
|
|
if(file.extension().equals("json")){
|
|
json = json.replace("#", "\\#");
|
|
}
|
|
|
|
JsonValue value = parser.fromJson(null, Jval.read(json).toString(Jformat.plain));
|
|
|
|
if(!parsers.containsKey(type)){
|
|
throw new SerializationException("No parsers for content type '" + type + "'");
|
|
}
|
|
|
|
currentMod = mod;
|
|
boolean located = locate(type, name) != null;
|
|
Content c = parsers.get(type).parse(mod.name, name, value);
|
|
c.minfo.sourceFile = file;
|
|
toBeParsed.add(c);
|
|
|
|
if(!located){
|
|
c.minfo.mod = mod;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
public void markError(Content content, LoadedMod mod, Fi file, Throwable error){
|
|
Log.err("Error for @ / @:\n@\n", content, file, Strings.getStackTrace(error));
|
|
|
|
content.minfo.mod = mod;
|
|
content.minfo.sourceFile = file;
|
|
content.minfo.error = makeError(error, file);
|
|
content.minfo.baseError = error;
|
|
if(mod != null){
|
|
mod.erroredContent.add(content);
|
|
}
|
|
}
|
|
|
|
public void markError(Content content, Throwable error){
|
|
if(content.minfo != null && !content.hasErrored()){
|
|
markError(content, content.minfo.mod, content.minfo.sourceFile, error);
|
|
}
|
|
}
|
|
|
|
private String makeError(Throwable t, Fi file){
|
|
StringBuilder builder = new StringBuilder();
|
|
builder.append("[lightgray]").append("File: ").append(file.name()).append("[]\n\n");
|
|
|
|
if(t.getMessage() != null && t instanceof JsonParseException){
|
|
builder.append("[accent][[JsonParse][] ").append(":\n").append(t.getMessage());
|
|
}else if(t instanceof NullPointerException){
|
|
builder.append(Strings.neatError(t));
|
|
}else{
|
|
Seq<Throwable> causes = Strings.getCauses(t);
|
|
for(Throwable e : causes){
|
|
builder.append("[accent][[").append(e.getClass().getSimpleName().replace("Exception", ""))
|
|
.append("][] ")
|
|
.append(e.getMessage() != null ?
|
|
e.getMessage().replace("mindustry.", "").replace("arc.", "") : "").append("\n");
|
|
}
|
|
}
|
|
return builder.toString();
|
|
}
|
|
|
|
private <T extends MappableContent> T locate(ContentType type, String name){
|
|
T first = Vars.content.getByName(type, name); //try vanilla replacement
|
|
return first != null ? first : Vars.content.getByName(type, currentMod.name + "-" + name);
|
|
}
|
|
|
|
private <T extends MappableContent> T locateAny(String name){
|
|
for(ContentType t : ContentType.all){
|
|
var out = locate(t, name);
|
|
if(out != null){
|
|
return (T)out;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
<T> T make(Class<T> type){
|
|
try{
|
|
Constructor<T> cons = type.getDeclaredConstructor();
|
|
cons.setAccessible(true);
|
|
return cons.newInstance();
|
|
}catch(Exception e){
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
private <T> T make(Class<T> type, String name){
|
|
try{
|
|
Constructor<T> cons = type.getDeclaredConstructor(String.class);
|
|
cons.setAccessible(true);
|
|
return cons.newInstance(name);
|
|
}catch(Exception e){
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
private <T> Prov<T> supply(Class<T> type){
|
|
try{
|
|
Constructor<T> cons = type.getDeclaredConstructor();
|
|
return () -> {
|
|
try{
|
|
return cons.newInstance();
|
|
}catch(Exception e){
|
|
throw new RuntimeException(e);
|
|
}
|
|
};
|
|
}catch(Exception e){
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
Object field(Class<?> type, JsonValue value){
|
|
return field(type, value.asString());
|
|
}
|
|
|
|
/** Gets a field from a static class by name, throwing a descriptive exception if not found. */
|
|
private Object field(Class<?> type, String name){
|
|
try{
|
|
Object b = type.getField(name).get(null);
|
|
if(b == null) throw new IllegalArgumentException(type.getSimpleName() + ": not found: '" + name + "'");
|
|
return b;
|
|
}catch(Exception e){
|
|
throw new RuntimeException(e);
|
|
}
|
|
}
|
|
|
|
Object fieldOpt(Class<?> type, JsonValue value){
|
|
try{
|
|
return type.getField(value.asString()).get(null);
|
|
}catch(Exception e){
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void checkNullFields(Object object){
|
|
if(object == null || object instanceof Number || object instanceof String || toBeParsed.contains(object) || object.getClass().getName().startsWith("arc.")) return;
|
|
|
|
parser.getFields(object.getClass()).values().toSeq().each(field -> {
|
|
try{
|
|
if(field.field.getType().isPrimitive()) return;
|
|
|
|
if(!field.field.isAnnotationPresent(Nullable.class) && field.field.get(object) == null && !implicitNullable.contains(field.field.getType())){
|
|
throw new RuntimeException("'" + field.field.getName() + "' in " +
|
|
((object.getClass().isAnonymousClass() ? object.getClass().getSuperclass() : object.getClass()).getSimpleName()) +
|
|
" is missing! Object = " + object + ", field = (" + field.field.getName() + " = " + field.field.get(object) + ")");
|
|
}
|
|
}catch(Exception e){
|
|
throw new RuntimeException(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void readFields(Object object, JsonValue jsonMap, boolean stripType){
|
|
if(stripType) jsonMap.remove("type");
|
|
readFields(object, jsonMap);
|
|
}
|
|
|
|
void readFields(Object object, JsonValue jsonMap){
|
|
JsonValue research = jsonMap.remove("research");
|
|
|
|
toBeParsed.remove(object);
|
|
var type = object.getClass();
|
|
var fields = parser.getFields(type);
|
|
for(JsonValue child = jsonMap.child; child != null; child = child.next){
|
|
FieldMetadata metadata = fields.get(child.name().replace(" ", "_"));
|
|
if(metadata == null){
|
|
if(ignoreUnknownFields){
|
|
Log.warn("@: Ignoring unknown field: " + child.name + " (" + type.getName() + ")", object);
|
|
continue;
|
|
}else{
|
|
SerializationException ex = new SerializationException("Field not found: " + child.name + " (" + type.getName() + ")");
|
|
ex.addTrace(child.trace());
|
|
throw ex;
|
|
}
|
|
}
|
|
Field field = metadata.field;
|
|
try{
|
|
field.set(object, parser.readValue(field.getType(), metadata.elementType, child, metadata.keyType));
|
|
}catch(IllegalAccessException ex){
|
|
throw new SerializationException("Error accessing field: " + field.getName() + " (" + type.getName() + ")", ex);
|
|
}catch(SerializationException ex){
|
|
ex.addTrace(field.getName() + " (" + type.getName() + ")");
|
|
throw ex;
|
|
}catch(RuntimeException runtimeEx){
|
|
SerializationException ex = new SerializationException(runtimeEx);
|
|
ex.addTrace(child.trace());
|
|
ex.addTrace(field.getName() + " (" + type.getName() + ")");
|
|
throw ex;
|
|
}
|
|
}
|
|
|
|
if(object instanceof UnlockableContent unlock && research != null){
|
|
|
|
//add research tech node
|
|
String researchName;
|
|
ItemStack[] customRequirements;
|
|
|
|
//research can be a single string or an object with parent and requirements
|
|
if(research.isString()){
|
|
researchName = research.asString();
|
|
customRequirements = null;
|
|
}else{
|
|
researchName = research.getString("parent");
|
|
customRequirements = research.has("requirements") ? parser.readValue(ItemStack[].class, research.get("requirements")) : null;
|
|
}
|
|
|
|
//remove old node
|
|
TechNode lastNode = TechTree.all.find(t -> t.content == unlock);
|
|
if(lastNode != null){
|
|
lastNode.remove();
|
|
}
|
|
|
|
TechNode node = new TechNode(null, unlock, customRequirements == null ? ItemStack.empty : customRequirements);
|
|
LoadedMod cur = currentMod;
|
|
|
|
postreads.add(() -> {
|
|
currentContent = unlock;
|
|
currentMod = cur;
|
|
|
|
//add custom objectives
|
|
if(research.has("objectives")){
|
|
node.objectives.addAll(parser.readValue(Objective[].class, research.get("objectives")));
|
|
}
|
|
|
|
//all items have a produce requirement unless already specified
|
|
if(object instanceof Item i && !node.objectives.contains(o -> o instanceof Produce p && p.content == i)){
|
|
node.objectives.add(new Produce(i));
|
|
}
|
|
|
|
//remove old node from parent
|
|
if(node.parent != null){
|
|
node.parent.children.remove(node);
|
|
}
|
|
|
|
if(customRequirements == null){
|
|
node.setupRequirements(unlock.researchRequirements());
|
|
}
|
|
|
|
//find parent node.
|
|
TechNode parent = TechTree.all.find(t -> t.content.name.equals(researchName) || t.content.name.equals(currentMod.name + "-" + researchName) || t.content.name.equals(SaveVersion.mapFallback(researchName)));
|
|
|
|
if(parent == null){
|
|
Log.warn("Content '" + researchName + "' isn't in the tech tree, but '" + unlock.name + "' requires it to be researched.");
|
|
}else{
|
|
//add this node to the parent
|
|
if(!parent.children.contains(node)){
|
|
parent.children.add(node);
|
|
}
|
|
//reparent the node
|
|
node.parent = parent;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Tries to resolve a class from the class type map. */
|
|
<T> Class<T> resolve(String base){
|
|
return resolve(base, null);
|
|
}
|
|
|
|
/** Tries to resolve a class from the class type map. */
|
|
<T> Class<T> resolve(String base, Class<T> def){
|
|
//no base class specified
|
|
if((base == null || base.isEmpty()) && def != null) return def;
|
|
|
|
//return mapped class if found in the global map
|
|
var out = ClassMap.classes.get(!base.isEmpty() && Character.isLowerCase(base.charAt(0)) ? Strings.capitalize(base) : base);
|
|
if(out != null) return (Class<T>)out;
|
|
|
|
//try to resolve it as a raw class name
|
|
if(base.indexOf('.') != -1){
|
|
try{
|
|
return (Class<T>)Class.forName(base);
|
|
}catch(Exception ignored){
|
|
//try to use mod class loader
|
|
try{
|
|
return (Class<T>)Class.forName(base, true, mods.mainLoader());
|
|
}catch(Exception ignore){}
|
|
}
|
|
}
|
|
|
|
if(def != null){
|
|
Log.warn("[@] No type '" + base + "' found, defaulting to type '" + def.getSimpleName() + "'", currentContent == null ? currentMod.name : "");
|
|
return def;
|
|
}
|
|
throw new IllegalArgumentException("Type not found: " + base);
|
|
}
|
|
|
|
private interface FieldParser{
|
|
Object parse(Class<?> type, JsonValue value) throws Exception;
|
|
}
|
|
|
|
private interface TypeParser<T extends Content>{
|
|
T parse(String mod, String name, JsonValue value) throws Exception;
|
|
}
|
|
|
|
//intermediate class for parsing
|
|
static class UnitReq{
|
|
public Block block;
|
|
public ItemStack[] requirements = {};
|
|
@Nullable
|
|
public UnitType previous;
|
|
public float time = 60f * 10f;
|
|
}
|
|
|
|
public interface ParseListener{
|
|
void parsed(Class<?> type, JsonValue jsonData, Object result);
|
|
}
|
|
|
|
} |