diff --git a/runelite-client/src/main/java/net/runelite/client/game/NPCManager.java b/runelite-client/src/main/java/net/runelite/client/game/NPCManager.java index 71255a4a63..4c2ae76d8c 100644 --- a/runelite-client/src/main/java/net/runelite/client/game/NPCManager.java +++ b/runelite-client/src/main/java/net/runelite/client/game/NPCManager.java @@ -26,12 +26,12 @@ package net.runelite.client.game; import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import java.io.InputStream; +import com.google.gson.stream.JsonReader; +import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; +import java.io.IOException; import java.io.InputStreamReader; -import java.lang.reflect.Type; -import java.util.Map; +import java.nio.charset.StandardCharsets; import javax.annotation.Nullable; import javax.inject.Inject; import javax.inject.Singleton; @@ -41,20 +41,37 @@ import lombok.extern.slf4j.Slf4j; @Singleton public class NPCManager { - private final ImmutableMap statsMap; + private ImmutableMap statsMap; @Inject private NPCManager() { - final Gson gson = new Gson(); + Completable.fromAction(this::loadStats) + .subscribeOn(Schedulers.computation()) + .subscribe( + () -> log.debug("Loaded {} NPC stats", statsMap.size()), + ex -> log.warn("Error loading NPC stats", ex) + ); + } - final Type typeToken = new TypeToken>() + private void loadStats() throws IOException + { + try (JsonReader reader = new JsonReader(new InputStreamReader(NPCManager.class.getResourceAsStream("/npc_stats.json"), StandardCharsets.UTF_8))) { - }.getType(); + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(2821); + reader.beginObject(); - final InputStream statsFile = getClass().getResourceAsStream("/npc_stats.json"); - final Map stats = gson.fromJson(new InputStreamReader(statsFile), typeToken); - statsMap = ImmutableMap.copyOf(stats); + while (reader.hasNext()) + { + builder.put( + Integer.parseInt(reader.nextName()), + NPCStats.NPC_STATS_TYPE_ADAPTER.read(reader) + ); + } + + reader.endObject(); + statsMap = builder.build(); + } } /** diff --git a/runelite-client/src/main/java/net/runelite/client/game/NPCStats.java b/runelite-client/src/main/java/net/runelite/client/game/NPCStats.java index d324098226..1dc0c3c98e 100644 --- a/runelite-client/src/main/java/net/runelite/client/game/NPCStats.java +++ b/runelite-client/src/main/java/net/runelite/client/game/NPCStats.java @@ -24,9 +24,15 @@ */ package net.runelite.client.game; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import lombok.Builder; import lombok.Value; @Value +@Builder(builderClassName = "Builder") public class NPCStats { private final String name; @@ -77,4 +83,120 @@ public class NPCStats return (1 + Math.floor(averageLevel * (averageDefBonus + bonusStrength + bonusAttack) / 5120) / 40); } + + // Because this class is here we can't add the TypeAdapter to gson (easily) + // doesn't mean we can't use one to do it a bit quicker + public static final TypeAdapter NPC_STATS_TYPE_ADAPTER = new TypeAdapter() + { + @Override + public void write(JsonWriter out, NPCStats value) + { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public NPCStats read(JsonReader in) throws IOException + { + in.beginObject(); + NPCStats.Builder builder = NPCStats.builder(); + + // Name is the only one that's guaranteed + in.skipValue(); + builder.name(in.nextString()); + + while (in.hasNext()) + { + switch (in.nextName()) + { + case "hitpoints": + builder.hitpoints(in.nextInt()); + break; + case "combatLevel": + builder.combatLevel(in.nextInt()); + break; + case "slayerLevel": + builder.slayerLevel(in.nextInt()); + break; + case "attackSpeed": + builder.attackSpeed(in.nextInt()); + break; + case "attackLevel": + builder.attackLevel(in.nextInt()); + break; + case "strengthLevel": + builder.strengthLevel(in.nextInt()); + break; + case "defenceLevel": + builder.defenceLevel(in.nextInt()); + break; + case "rangeLevel": + builder.rangeLevel(in.nextInt()); + break; + case "magicLevel": + builder.magicLevel(in.nextInt()); + break; + case "stab": + builder.stab(in.nextInt()); + break; + case "slash": + builder.slash(in.nextInt()); + break; + case "crush": + builder.crush(in.nextInt()); + break; + case "range": + builder.range(in.nextInt()); + break; + case "magic": + builder.magic(in.nextInt()); + break; + case "stabDef": + builder.stabDef(in.nextInt()); + break; + case "slashDef": + builder.slashDef(in.nextInt()); + break; + case "crushDef": + builder.crushDef(in.nextInt()); + break; + case "rangeDef": + builder.rangeDef(in.nextInt()); + break; + case "magicDef": + builder.magicDef(in.nextInt()); + break; + case "bonusAttack": + builder.bonusAttack(in.nextInt()); + break; + case "bonusStrength": + builder.bonusStrength(in.nextInt()); + break; + case "bonusRangeStrength": + builder.bonusRangeStrength(in.nextInt()); + break; + case "bonusMagicDamage": + builder.bonusMagicDamage(in.nextInt()); + break; + case "poisonImmune": + builder.poisonImmune(in.nextBoolean()); + break; + case "venomImmune": + builder.venomImmune(in.nextBoolean()); + break; + case "dragon": + builder.dragon(in.nextBoolean()); + break; + case "demon": + builder.demon(in.nextBoolean()); + break; + case "undead": + builder.undead(in.nextBoolean()); + break; + } + } + + in.endObject(); + return builder.build(); + } + }; } \ No newline at end of file