ContentParser是原版json解析中重要的一环,由于json本身的各种限制,诸如requirements、consume等无法写出,这就需要解析器来帮助我们把json专用语法转换为java内容。

Json Java
Number
String
Object
List
Bool
Int, Float
String, Object, Int
Object
ArrayList, ObjectMap, Seq
Boolean

温馨提示:请跳过任何一个你看不懂的字,专注于你能看懂的,如果想看懂,可以考虑去学java。

本文已完结

字段声明

  1. @SuppressWarnings("unchecked")
  2. public class ContentParser{
  3. private static final boolean ignoreUnknownFields = true;
  4. ObjectMap<Class<?>, ContentType> contentTypes = new ObjectMap<>();
  5. ObjectSet<Class<?>> implicitNullable = ObjectSet.with(TextureRegion.class, TextureRegion[].class, TextureRegion[][].class, TextureRegion[][][].class);
  6. ObjectMap<String, AssetDescriptor<?>> sounds = new ObjectMap<>();
  7. Seq<ParseListener> listeners = new Seq<>();

这里定义了ignoreUnknownFields字段,正如其名,意思是“忽略未知字段”,其余ObjectMap和ObjectSet可先忽略。

ClassParsers 字段解析

classParsers的作用是建立类与解析办法的一一对应,由具体的读取json文件的方法调用,所以在这个文件里我们需要建立起需要的类与解析办法的一一对应。具体调用时,先判断此Map中是否含有所需要的类,在读取出对应值这个lambda,然后传入type和jsonData调用lambda以获取结果。

需要注意的是,给这个lambda传的参数不一定是Json的值,可能已经经过处理。另一种可能是,lambda要求字符串,但json值却是字符串列表,这个时候主体解析部分编写得当也可以奏效。

  1. put(Effect.class, (type, data) -> {
  2. if(data.isString()){
  3. return field(Fx.class, data);
  4. }
  5. if(data.isArray()){
  6. return new MultiEffect(parser.readValue(Effect[].class, data));
  7. }
  8. Class<? extends Effect> bc = resolve(data.getString("type", ""), ParticleEffect.class);
  9. data.remove("type");
  10. Effect result = make(bc);
  11. readFields(result, data);
  12. return result;
  13. });

这是对于Effect类(特效)的识别,在json的预期写法中(下称json中,其实anuke本人并不写json但他会制定语法规则),effect相关的键名(比如hitEffect、idleEffect、craftEffect)可能传来的是一个字符串、一个列表或一个对象:

  • 对于字符串,如第3行,返回的是content/Fx.java中同名的特效;
  • 对于列表,如第6行,返回的是一个由列表内全部特效组成的一个综合特效(MultiEffect)
  • 如果都不是,说明这是一个由大括号括起来的对象:对付json对象第一步是看他的type,第8行,试图获取对象中type的值,如果没有type的话就默认值”ParticleEffect”(需要注意的是,前方的Class<? extends Effect>代表:这个对象到底是什么类(type)还没定下来,但是必须确保它继承于Effect,不继承就抛Error不让你进游戏);第9行,从整个数据中剔出type;第10行,把这个json对象转换成java实例、赋值给result;第11行,把剩余字段添加到result身上;第12行返回result。
  1. put(Sortf.class, (type, data) -> field(UnitSorts.class, data));
  2. put(Interp.class, (type, data) -> field(Interp.class, data));
  3. put(Blending.class, (type, data) -> field(Blending.class, data));
  4. put(CacheLayer.class, (type, data) -> field(CacheLayer.class, data));

这四个东西分别是单位排序、插补、混合、缓存层。总体来说无用。

  1. put(Attribute.class, (type, data) -> Attribute.get(data.asString()));

这是对Attribute类(地板属性)的识别,因为Attribute已经为我们封装好了get方法,直接调用即可。

  1. put(Schematic.class, (type, data) -> {
  2. Object result = fieldOpt(Loadouts.class, data);
  3. if(result != null){
  4. return result;
  5. }else{
  6. String str = data.asString();
  7. if(str.startsWith(Vars.schematicBaseStart)){
  8. return Schematics.readBase64(str);
  9. }else{
  10. return Schematics.read(Vars.tree.get("schematics/" + str + "." + Vars.schematicExtension));
  11. }
  12. }
  13. });

这是对Schematics类(蓝图)的识别,在json中,截至目前毫无用处。唯一在小行星生成器被用到。

  1. put(Color.class, (type, data) -> Color.valueOf(data.asString()));

这是对Color类(颜色)的识别。原版所有需要颜色的地方在json传入的都是六位或八位十六进制色,这里使用了Color的valueOf方法来返回一个颜色的对象。

  1. put(StatusEffect.class, (type, data) -> {
  2. if(data.isString()){
  3. StatusEffect result = locate(ContentType.status, data.asString());
  4. if(result != null) return result;
  5. throw new IllegalArgumentException("Unknown status effect: '" + data.asString() + "'");
  6. }
  7. StatusEffect effect = new StatusEffect(currentMod.name + "-" + data.getString("name"));
  8. effect.minfo.mod = currentMod;
  9. readFields(effect, data);
  10. return effect;
  11. });

这是对StatusEffect类(状态效果)的识别。在json中,和buff有关的键可能传来字符串或对象。

  • 如果传来字符串,那么说明这个是游戏已有的buff,那么我们就需要从原版或模组,总之类型是StatusEffect的东西中定位出一个。如寻找到了直接返回,没找到就直接报错。
  • 如果传来对象,那说明这个是玩家自定义的,那么就创建对象并读取字段赋值再返回。
  1. put(UnitCommand.class, (type, data) -> {
  2. if(data.isString()){
  3. var cmd = UnitCommand.all.find(u -> u.name.equals(data.asString()));
  4. if(cmd != null){
  5. return cmd;
  6. }else{
  7. throw new IllegalArgumentException("Unknown unit command name: " + data.asString());
  8. }
  9. }else{
  10. throw new IllegalArgumentException("Unit commands must be strings.");
  11. }
  12. });

这里是对UnitCommand类(单位RTS命令)的识别,在json中要求传来一个字符串。作为137新添加的代码,要求十分严格,找不到或不是字符串都会报错。

  1. put(BulletType.class, (type, data) -> {
  2. if(data.isString()){
  3. return field(Bullets.class, data);
  4. }
  5. Class<?> bc = resolve(data.getString("type", ""), BasicBulletType.class);
  6. data.remove("type");
  7. BulletType result = (BulletType)make(bc);
  8. readFields(result, data);
  9. return result;
  10. });

这是对BulletType类(子弹类型)的识别。

  • 当传来字符串时,返回一个与参数同名的对象,且字段全部默认
  • 当传来对象时,先找到type字段,有则在BulletType中寻找,找不到则默认设置BasicBulletType,之后按照字段解析出对象返回。
  1. put(AmmoType.class, (type, data) -> {
  2. //string -> item
  3. //if liquid ammo support is added, this should scan for liquids as well
  4. if(data.isString()) return new ItemAmmoType(find(ContentType.item, data.asString()));
  5. //number -> power
  6. if(data.isNumber()) return new PowerAmmoType(data.asFloat());
  7. var bc = resolve(data.getString("type", ""), ItemAmmoType.class);
  8. data.remove("type");
  9. AmmoType result = make(bc);
  10. readFields(result, data);
  11. r

这是对AmmoType类(弹药类型)的识别。

  • 字符串解析成物品或流体;
  • 数字解析成电力;
  • 对象则提取type创建对象提取字段进行赋值并返回。
  1. put(DrawBlock.class, (type, data) -> {
  2. if(data.isString()){
  3. //try to instantiate
  4. return make(resolve(data.asString()));
  5. }
  6. //array is shorthand for DrawMulti
  7. if(data.isArray()){
  8. return new DrawMulti(parser.readValue(DrawBlock[].class, data));
  9. }
  10. var bc = resolve(data.getString("type", ""), DrawDefault.class);
  11. data.remove("type");
  12. DrawBlock result = make(bc);
  13. readFields(result, data);
  14. return result;
  15. });

这是对DrawBlock类(方块绘制)的识别。

  • 字符串则使用同名DrawBlock;
  • 列表则使用DrawMulti括起来;
  • 对象则提取type创建对象提取字段进行赋值并返回。
  1. put(ShootPattern.class, (type, data) -> {
  2. var bc = resolve(data.getString("type", ""), ShootPattern.class);
  3. data.remove("type");
  4. var result = make(bc);
  5. readFields(result, data);
  6. return result;
  7. });
  8. put(DrawPart.class, (type, data) -> {
  9. Class<?> bc = resolve(data.getString("type", ""), RegionPart.class);
  10. data.remove("type");
  11. var result = make(bc);
  12. readFields(result, data);
  13. return result;
  14. });

这两个同理,提取type-创建对象-提取字段-进行赋值并返回。

  1. //TODO this is untested
  2. put(PartProgress.class, (type, data) -> {
  3. //simple case: it's a string or number constant
  4. if(data.isString()) return field(PartProgress.class, data.asString());
  5. if(data.isNumber()) return PartProgress.constant(data.asFloat());
  6. if(!data.has("type")){
  7. throw new RuntimeException("PartProgress object need a 'type' string field. Check the PartProgress class for a list of constants.");
  8. }
  9. PartProgress base = (PartProgress)field(PartProgress.class, data.getString("type"));
  10. JsonValue opval =
  11. data.has("operation") ? data.get("operation") :
  12. data.has("op") ? data.get("op") : null;
  13. //no operations I guess (why would you do this?)
  14. if(opval == null){
  15. return base;
  16. }
  17. //this is the name of the method to call
  18. String op = opval.asString();
  19. //I have to hard-code this, no easy way of getting parameter names, unfortunately
  20. return switch(op){
  21. case "inv" -> base.inv();
  22. case "slope" -> base.slope();
  23. case "clamp" -> base.clamp();
  24. case "delay" -> base.delay(data.getFloat("amount"));
  25. case "sustain" -> base.sustain(data.getFloat("offset", 0f), data.getFloat("grow", 0f), data.getFloat("sustain"));
  26. case "shorten" -> base.shorten(data.getFloat("amount"));
  27. case "add" -> data.has("amount") ? base.add(data.getFloat("amount")) : base.add(parser.readValue(PartProgress.class, data.get("other")));
  28. case "blend" -> base.blend(parser.readValue(PartProgress.class, data.get("other")), data.getFloat("amount"));
  29. case "mul" -> base.mul(parser.readValue(PartProgress.class, data.get("other")));
  30. case "min" -> base.min(parser.readValue(PartProgress.class, data.get("other")));
  31. case "sin" -> base.sin(data.getFloat("scl"), data.getFloat("mag"));
  32. case "absin" -> base.absin(data.getFloat("scl"), data.getFloat("mag"));
  33. case "curve" -> base.curve(parser.readValue(Interp.class, data.get("interp")));
  34. default -> throw new RuntimeException("Unknown operation '" + op + "', check PartProgress class for a list of methods.");
  35. };
  36. });

这个东西我不知道是什么,我也不知道怎么用,也不知道谁知道。总之写了这么多好像json并用不了的样子。

  1. put(PlanetGenerator.class, (type, data) -> {
  2. var result = new AsteroidGenerator(); //only one type for now
  3. readFields(result, data);
  4. return result;
  5. });

这是对PlanerGenerator类(行星地图生成器)的识别。目前凡是json中,这个类一律返回AsteroidGenerator(小行星)。所以写星球就别想了

  1. put(GenericMesh.class, (type, data) -> {
  2. if(!data.isObject()) throw new RuntimeException("Meshes must be objects.");
  3. if(!(currentContent instanceof Planet planet)) throw new RuntimeException("Meshes can only be parsed as parts of planets.");
  4. String tname = Strings.capitalize(data.getString("type", "NoiseMesh"));
  5. return switch(tname){
  6. //TODO NoiseMesh is bad
  7. case "NoiseMesh" -> new NoiseMesh(planet,
  8. data.getInt("seed", 0), data.getInt("divisions", 1), data.getFloat("radius", 1f),
  9. data.getInt("octaves", 1), data.getFloat("persistence", 0.5f), data.getFloat("scale", 1f), data.getFloat("mag", 0.5f),
  10. Color.valueOf(data.getString("color1", data.getString("color", "ffffff"))),
  11. Color.valueOf(data.getString("color2", data.getString("color", "ffffff"))),
  12. data.getInt("colorOct", 1), data.getFloat("colorPersistence", 0.5f), data.getFloat("colorScale", 1f),
  13. data.getFloat("colorThreshold", 0.5f));
  14. case "MultiMesh" -> new MultiMesh(parser.readValue(GenericMesh[].class, data.get("meshes")));
  15. case "MatMesh" -> new MatMesh(parser.readValue(GenericMesh.class, data.get("mesh")), parser.readValue(Mat3D.class, data.get("mat")));
  16. default -> throw new RuntimeException("Unknown mesh type: " + tname);
  17. };
  18. });

这是Mesh,是行星云层和地面六边形的绘制器。别看了别看了

  1. put(Mat3D.class, (type, data) -> {
  2. if(data == null) return new Mat3D();
  3. //transform x y z format
  4. if(data.has("x") && data.has("y") && data.has("z")){
  5. return new Mat3D().translate(data.getFloat("x", 0f), data.getFloat("y", 0f), data.getFloat("z", 0f));
  6. }
  7. //transform array format
  8. if(data.isArray() && data.size == 3){
  9. return new Mat3D().setToTranslation(new Vec3(data.asFloatArray()));
  10. }
  11. Mat3D mat = new Mat3D();
  12. //TODO this is kinda bad
  13. for(var val : data){
  14. switch(val.name){
  15. case "translate", "trans" -> mat.translate(parser.readValue(Vec3.class, data));
  16. case "scale", "scl" -> mat.scale(parser.readValue(Vec3.class, data));
  17. case "rotate", "rot" -> mat.rotate(parser.readValue(Vec3.class, data), data.getFloat("degrees", 0f));
  18. case "multiply", "mul" -> mat.mul(parser.readValue(Mat3D.class, data));
  19. case "x", "y", "z" -> {}
  20. default -> throw new RuntimeException("Unknown matrix transformation: '" + val.name + "'");
  21. }
  22. }
  23. return mat;
  24. });

这是Mat3(Martix3D)三维矩阵,和刚才Mesh的MatMesh是配套的。当你在凝视深渊的时候,深渊也在凝视着你

  1. put(Vec3.class, (type, data) -> {
  2. if(data.isArray()) return new Vec3(data.asFloatArray());
  3. return new Vec3(data.getFloat("x", 0f), data.getFloat("y", 0f), data.getFloat("z", 0f));
  4. });

这是Vec3(Vector3)三维矢量,和刚才Mat3是配套的。不然他为什么不解析Vec2

  1. put(Sound.class, (type, data) -> {
  2. if(fieldOpt(Sounds.class, data) != null) return fieldOpt(Sounds.class, data);
  3. if(Vars.headless) return new Sound();
  4. String name = "sounds/" + data.asString();
  5. String path = Vars.tree.get(name + ".ogg").exists() ? name + ".ogg" : name + ".mp3";
  6. if(sounds.containsKey(path)) return ((SoundParameter)sounds.get(path).params).sound;
  7. var sound = new Sound();
  8. AssetDescriptor<?> desc = Core.assets.load(path, Sound.class, new SoundParameter(sound));
  9. desc.errored = Throwable::printStackTrace;
  10. sounds.put(path, desc);
  11. return sound;
  12. });

这是Sound类(音效)的识别。首先他一定传来一个字符串。

  • Sounds类里能找到,说明这是游戏内建音效,直接返回即可;
  • 游戏无头(headless),说明正运行在一个无需播放音效的服务器上,返回一个空音效;
  • 下面则是在模组文件夹内寻找。name代表这个音效如果是本模组添加则应处于的位置,path则是同名音效位置。两个值之间进行“包含”比较。(因为音效既可以是ogg也可以是mp3,所以我们要让~/mods/my-mod/sounds/114.ogg和~/mods/my-mod/sounds/114.mp3都可以被匹配到音效114)
  1. put(Objectives.Objective.class, (type, data) -> {
  2. if(data.isString()){
  3. var cont = locateAny(data.asString());
  4. if(cont == null) throw new IllegalArgumentException("Unknown objective content: " + data.asString());
  5. return new Research((UnlockableContent)cont);
  6. }
  7. var oc = resolve(data.getString("type", ""), SectorComplete.class);
  8. data.remove("type");
  9. Objectives.Objective obj = make(oc);
  10. readFields(obj, data);
  11. return obj;
  12. });

这是对Objectives.Objective类(研究额外需求)的识别。

  • 如果传进来一个字符串,意思就是要求研究的额外需求是研究此内容。这个内容可能是五大类任意一个(物品/流体/方块/单位/行星),所以要定位原版所有内容。
  • 如果传进来一个对象,那就提取type-创建对象-提取字段-进行赋值并返回。
  1. put(Ability.class, (type, data) -> {
  2. Class<? extends Ability> oc = resolve(data.getString("type", ""));
  3. data.remove("type");
  4. Ability obj = make(oc);
  5. readFields(obj, data);
  6. return obj;
  7. });

这是提取type-创建对象-提取字段-进行赋值并返回。

  1. put(Weapon.class, (type, data) -> {
  2. var oc = resolve(data.getString("type", ""), Weapon.class);
  3. data.remove("type");
  4. var weapon = make(oc);
  5. readFields(weapon, data);
  6. weapon.name = currentMod.name + "-" + weapon.name;
  7. return weapon;
  8. });

这是提取type-创建对象-提取字段-进行赋值并返回,但在返回前会在name前加上mod的name以正确调用贴图。

再次的字段与方法声明

  1. /** Stores things that need to be parsed fully, e.g. reading fields of content.
  2. * This is done to accommodate binding of content names first.*/
  3. private Seq<Runnable> reads = new Seq<>();
  4. private Seq<Runnable> postreads = new Seq<>();
  5. private ObjectSet<Object> toBeParsed = new ObjectSet<>();
  6. LoadedMod currentMod;
  7. Content currentContent;

Parser 写法转换

  1. private Json parser = new Json(){

写法转换,而与其他不同的是,在json中的写法没有完全表示出java的半点痕迹,完完全全新开创的写法。

  1. @Override
  2. public <T> T readValue(Class<T> type, Class elementType, JsonValue jsonData, Class keyType){
  3. T t = internalRead(type, elementType, jsonData, keyType);
  4. if(t != null && !Reflect.isWrapper(t.getClass()) && (type == null || !type.isPrimitive())){
  5. checkNullFields(t);
  6. listeners.each(hook -> hook.parsed(type, jsonData, t));
  7. }
  8. return t;
  9. }

这是json读取值的方法,这里的T都是泛型,Java作为一门强类型语言,必须明确写出方法的返回值以及变量的类型。但在这个使用场景,我们不知道返回的是一个PlanetGenerator还是一个BasicBulletType,所以我们使用到了泛型,他的意思是指在编写时不指定类型。不过要注意的是,java的泛型实则是把每一种可能都写一遍。

  1. private <T> T internalRead(Class<T> type, Class elementType, JsonValue jsonData, Class keyType){
  2. if(type != null){
  3. if(classParsers.containsKey(type)){
  4. try{
  5. return (T)classParsers.get(type).parse(type, jsonData);
  6. }catch(Exception e){
  7. throw new RuntimeException(e);
  8. }
  9. }

这个就是真正的解析了,传进来一个json值和他预期的类(type)。这是第一段,判断type在classParsers里有没有,有就直接调用里面的解析并返回,如果执行出现问题就扔错误。

  1. if((type == int.class || type == Integer.class) && jsonData.isArray()){
  2. int value = 0;
  3. for(var str : jsonData){
  4. if(!str.isString()) throw new SerializationException("Integer bitfield values must all be strings. Found: " + str);
  5. String field = str.asString();
  6. value |= Reflect.<Integer>get(Env.class, field);
  7. }
  8. return (T)(Integer)value;
  9. }

这是第二段,假如classParsers没有,那我们还有一些额外的int、ItemStack、LiquidStack、ConsumeLiquid的解析,他们有种种原因没能塞进classParser,所以来这里额外处理。

这里是对Env类(环境)的转数字。Env是游戏采用位运算一个很巧妙的例子。每一种env都是2的幂,通过或运算算出当前环境的类型。再通过与、异或运算来计算envEnable和envDisable。

源代码如下:

  1. /** Environmental flags for different types of locations. */
  2. public class Env{
  3. public static final int
  4. //is on a planet
  5. terrestrial = 1,
  6. //is in space, no atmosphere
  7. space = 1 << 1,
  8. //is underwater, on a planet
  9. underwater = 1 << 2,
  10. //has a spores
  11. spores = 1 << 3,
  12. //has a scorching env effect
  13. scorching = 1 << 4,
  14. //has oil reservoirs
  15. groundOil = 1 << 5,
  16. //has water reservoirs
  17. groundWater = 1 << 6,
  18. //has oxygen in the atmosphere
  19. oxygen = 1 << 7,
  20. //all attributes combined, only used for bitmasking purposes
  21. any = 0xffffffff,
  22. //no attributes (0)
  23. none = 0;
  24. }

例如当前环境在行星上且有孢子,符合terrestrial和spore,那么我们就对00000001和00001000做或运算,得到00001001,就是当前的环境值。假如有物品要求在孢子环境才能运作(即envEnable=8),那么工作原理就是(环境值&8)不为零。

在json中,我们可以把字符串列表转为正整数(可能你以为我表达不严谨,但是确实是任何情况下均可以这么转换),解析就是用增强for逐个累加。

  1. //try to parse "item/amount" syntax
  2. if(type == ItemStack.class && jsonData.isString() && jsonData.asString().contains("/")){
  3. String[] split = jsonData.asString().split("/");
  4. return (T)fromJson(ItemStack.class, "{item: " + split[0] + ", amount: " + split[1] + "}");
  5. }

这是对ItemStack的转写。注意判断条件是预计type必须为ItemStack。处理方式就是按斜杠分割,第一项塞进item,第二项塞进amount。

  1. //try to parse "liquid/amount" syntax
  2. if(jsonData.isString() && jsonData.asString().contains("/")){
  3. String[] split = jsonData.asString().split("/");
  4. if(type == LiquidStack.class){
  5. return (T)fromJson(LiquidStack.class, "{liquid: " + split[0] + ", amount: " + split[1] + "}");
  6. }else if(type == ConsumeLiquid.class){
  7. return (T)fromJson(ConsumeLiquid.class, "{liquid: " + split[0] + ", amount: " + split[1] + "}");
  8. }
  9. }

这是对截止到最后仍然没有解析并且含有斜杠的转写,有斜杠就先分割,在判断是LiquidStack还是ConsumeLiquid,然后把分割结果分别加到liquid和amount后。

  1. if(Content.class.isAssignableFrom(type)){
  2. ContentType ctype = contentTypes.getThrow(type, () -> new IllegalArgumentException("No content type for class: " + type.getSimpleName()));
  3. String prefix = currentMod != null ? currentMod.name + "-" : "";
  4. T one = (T)Vars.content.getByName(ctype, prefix + jsonData.asString());
  5. if(one != null) return one;
  6. T two = (T)Vars.content.getByName(ctype, jsonData.asString());
  7. if(two != null) return two;
  8. 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.");
  9. }
  10. }
  11. return super.readValue(type, elementType, jsonData, keyType);
  12. }
  13. };

如果到这一步还没return走,说明要么type是假的,要么某些内容不存在。2行代码会尝试从ContentType枚举找这个type,如果没有就报“没有这个类”的错误。如果存在,说明问题只有东西找不到,然后尝试加模组名前缀和不加两次获取,再找不到,只能报错“内容找不到”了。

Parsers 八大类解析

  1. private ObjectMap<ContentType, TypeParser<?>> parsers = ObjectMap.of(

一种内容基本类型,一个解析lambda。

方块 block

  1. ContentType.block, (TypeParser<Block>)(mod, name, value) -> {
  2. readBundle(ContentType.block, name, value);
  3. Block block;
  4. if(locate(ContentType.block, name) != null){
  5. if(value.has("type")){
  6. 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.");
  7. block = make(resolve(value.getString("type", ""), Block.class), mod + "-" + name);
  8. }else{
  9. block = locate(ContentType.block, name);
  10. }
  11. }else{
  12. block = make(resolve(value.getString("type", ""), Block.class), mod + "-" + name);
  13. }

第一步自然是读取bundle,这个方法稍后即可见到。第二步则是个双层if,我们来逐层分析。

第一层判断的是是否是原版已有方块:

  • 如果不是就读取type创建对象;
  • 如果是,继续判断是否有type:如果有,警告“含有type字段则会创建新方块”并创建新对象;如果没有,就视为对这个方块进行增改操作。
  1. currentContent = block;
  2. read(() -> {
  3. if(value.has("consumes") && value.get("consumes").isObject()){
  4. for(JsonValue child : value.get("consumes")){
  5. switch(child.name){
  6. case "item" -> block.consumeItem(find(ContentType.item, child.asString()));
  7. case "itemCharged" -> block.consume((Consume)parser.readValue(ConsumeItemCharged.class, child));
  8. case "itemFlammable" -> block.consume((Consume)parser.readValue(ConsumeItemFlammable.class, child));
  9. case "itemRadioactive" -> block.consume((Consume)parser.readValue(ConsumeItemRadioactive.class, child));
  10. case "itemExplosive" -> block.consume((Consume)parser.readValue(ConsumeItemExplosive.class, child));
  11. case "itemExplode" -> block.consume((Consume)parser.readValue(ConsumeItemExplode.class, child));
  12. case "items" -> block.consume(child.isArray() ?
  13. new ConsumeItems(parser.readValue(ItemStack[].class, child)) :
  14. parser.readValue(ConsumeItems.class, child));
  15. case "liquidFlammable" -> block.consume((Consume)parser.readValue(ConsumeLiquidFlammable.class, child));
  16. case "liquid" -> block.consume((Consume)parser.readValue(ConsumeLiquid.class, child));
  17. case "liquids" -> block.consume(child.isArray() ?
  18. new ConsumeLiquids(parser.readValue(LiquidStack[].class, child)) :
  19. parser.readValue(ConsumeLiquids.class, child));
  20. case "coolant" -> block.consume((Consume)parser.readValue(ConsumeCoolant.class, child));
  21. case "power" -> {
  22. if(child.isNumber()){
  23. block.consumePower(child.asFloat());
  24. }else{
  25. block.consume((Consume)parser.readValue(ConsumePower.class, child));
  26. }
  27. }
  28. case "powerBuffered" -> block.consumePowerBuffered(child.asFloat());
  29. default -> throw new IllegalArgumentException("Unknown consumption type: '" + child.name + "' for block '" + block.name + "'.");
  30. }
  31. }
  32. value.remove("consumes");
  33. }

第三步是个大switch,这里是对consumes的解析。首先试图读取consumes字段并判断是否为对象(即大括号括起来者)。然后,逐次解析键值对。

  • item:即为单个物品,会从Items找到同名实例并consumeItem;
  • itemCharged itemFlammable itemRadioactive itemExplosive itemExplode liquidFlammable:即为与物品/流体的同名属性,往发电机的consumes放这个会很有用;
  • items:正规的多个物品,如果为列表那么读取每一项的值,如果不是就读他自己即可;
  • liquid/liquids同item/items
  • power:如果为数字就是普普通通的消耗电力,但也可以赋值对象;
  • poweeBuffered:电量缓存。
  1. readFields(block, value, true);
  2. if(block.size > maxBlockSize){
  3. throw new IllegalArgumentException("Blocks cannot be larger than " + maxBlockSize);
  4. }

超过最大尺寸就报错(最大尺寸为16)

  1. if(value.has("requirements") && block.buildVisibility == BuildVisibility.hidden){
  2. block.buildVisibility = BuildVisibility.shown;
  3. }
  4. });
  5. return block;
  6. },

从136起默认方块处于隐藏状态,所以要么设置requirements,要么设置buildVisibility。最后处理完毕,返回。

单位 unit

  1. ContentType.unit, (TypeParser<UnitType>)(mod, name, value) -> {
  2. readBundle(ContentType.unit, name, value);
  3. UnitType unit;
  4. if(locate(ContentType.unit, name) == null){
  5. unit = make(resolve(value.getString("template", ""), UnitType.class), mod + "-" + name);

先读取bundle,再创建对象。

  1. if(value.has("template")){
  2. value.remove("template");
  3. }
  4. var typeVal = value.get("type");
  5. if(typeVal != null && !typeVal.isString()){
  6. throw new RuntimeException("Unit '" + name + "' has an incorrect type. Types must be strings.");
  7. }
  8. unit.constructor = unitType(typeVal);
  9. }else{
  10. unit = locate(ContentType.unit, name);
  11. }

虽然查看了templete,但是旋即又给删了。实际上解析的还是type。先检测是否为空和是否为字符串,再处理constructor,注意这里unitType时下文一个方法。

这个else是上面的locate的,如果检测到了则把读取字段的目标设置为找到的同名单位。

  1. currentContent = unit;
  2. //TODO test this!
  3. read(() -> {
  4. //add reconstructor type
  5. if(value.has("requirements")){
  6. JsonValue rec = value.remove("requirements");
  7. UnitReq req = parser.readValue(UnitReq.class, rec);
  8. if(req.block instanceof Reconstructor r){
  9. if(req.previous != null){
  10. r.upgrades.add(new UnitType[]{req.previous, unit});
  11. }
  12. }else if(req.block instanceof UnitFactory f){
  13. f.plans.add(new UnitPlan(unit, req.time, req.requirements));
  14. }else{
  15. throw new IllegalArgumentException("Missing a valid 'block' in 'requirements'");
  16. }

anuke本人都说未经测试,所以就忽略吧。rec是requirements的json值,然后转换为req对象。req的一个属性block是重构工厂或兵工厂,若为重构工厂再读previous(前代),有就加配方,兵工厂直接加配方,否则,报错。

  1. if(value.has("controller") || value.has("aiController")){
  2. unit.aiController = supply(resolve(value.getString("controller", value.getString("aiController", "")), FlyingAI.class));
  3. value.remove("controller");
  4. }
  5. if(value.has("defaultController")){
  6. var sup = supply(resolve(value.getString("defaultController"), FlyingAI.class));
  7. unit.controller = u -> sup.get();
  8. value.remove("defaultController");
  9. }

这是ai相关的解析,游戏会读取controller或aiController来应用ai。defaulrController因为lambda所以也要解析。

  1. //read extra default waves
  2. if(value.has("waves")){
  3. JsonValue waves = value.remove("waves");
  4. SpawnGroup[] groups = parser.readValue(SpawnGroup[].class, waves);
  5. for(SpawnGroup group : groups){
  6. group.type = unit;
  7. }
  8. Vars.waves.get().addAll(groups);
  9. }
  10. readFields(unit, value, true);
  11. });
  12. return unit;
  13. },

这个是对怪物生成的解析,也没什么用。

天气 weather

  1. ContentType.weather, (TypeParser<Weather>)(mod, name, value) -> {
  2. Weather item;
  3. if(locate(ContentType.weather, name) != null){
  4. item = locate(ContentType.weather, name);
  5. readBundle(ContentType.weather, name, value);
  6. }else{
  7. readBundle(ContentType.weather, name, value);
  8. item = make(resolve(getType(value), ParticleWeather.class), mod + "-" + name);
  9. value.remove("type");
  10. }
  11. currentContent = item;
  12. read(() -> readFields(item, value));
  13. return item;
  14. },

判断已有,生成对象并赋值。

物品 item

  1. ContentType.item, parser(ContentType.item, Item::new),

物品直接新建一个

流体 liquid

  1. ContentType.liquid, (TypeParser<Liquid>)(mod, name, value) -> {
  2. Liquid liquid;
  3. if(locate(ContentType.liquid, name) != null){
  4. liquid = locate(ContentType.liquid, name);
  5. readBundle(ContentType.liquid, name, value);
  6. }else{
  7. readBundle(ContentType.liquid, name, value);
  8. liquid = make(resolve(value.getString("type", null), Liquid.class), mod + "-" + name);
  9. value.remove("type");
  10. }
  11. currentContent = liquid;
  12. read(() -> readFields(liquid, value));
  13. return liquid;
  14. },

判断已有,生成对象并赋值。

状态效果 status

  1. ContentType.status, parser(ContentType.status, StatusEffect::new),

状态效果也直接新建

区块 sector

  1. ContentType.sector, (TypeParser<SectorPreset>)(mod, name, value) -> {
  2. if(value.isString()){
  3. return locate(ContentType.sector, name);
  4. }
  5. if(!value.has("sector") || !value.get("sector").isNumber()) throw new RuntimeException("SectorPresets must have a sector number.");
  6. SectorPreset out = new SectorPreset(name, locate(ContentType.planet, value.getString("planet", "serpulo")), value.getInt("sector"));
  7. value.remove("sector");
  8. value.remove("planet");
  9. currentContent = out;
  10. read(() -> readFields(out, value));
  11. return out;
  12. },

先检测sector字段是否存在且为数字,再检测原版覆盖,最后读取字段并返回。

行星 planet

  1. ContentType.planet, (TypeParser<Planet>)(mod, name, value) -> {
  2. if(value.isString()) return locate(ContentType.planet, name);
  3. Planet parent = locate(ContentType.planet, value.getString("parent"));
  4. Planet planet = new Planet(name, parent, value.getFloat("radius", 1f), value.getInt("sectorSize", 0));
  5. if(value.has("mesh")){
  6. planet.meshLoader = () -> parser.readValue(GenericMesh.class, value.get("mesh"));
  7. }
  8. //always one sector right now...
  9. planet.sectors.add(new Sector(planet, Ptile.empty));
  10. currentContent = planet;
  11. read(() -> readFields(planet, value));
  12. return planet;
  13. }
  14. );

检测parent和sector,赋值对象返回。