Phase 8: the pack's own JEI pages for every drop source

Adrian, 2026-11-02/03: every item that can drop gets a JEI page naming its
sources, alongside the Cobblemon Info add-on (which shows drops and biomes,
not zones). The companion mod's JEI plugin (jei/DropSourcesJei.kt) adds:
- Pokemon drops on the island: 794 species of lines that live on the
  island, with chances, amounts and the zones they spawn in (or where their
  line lives); spawns with their own drops (Beheeyem in the End). The Drop
  Synthesizer core is the catalyst.
- Guardians and the End boss; Sacrifice Altar tiers and specials; Rebirth
  Chamber; zone ore veins with regrow times; Cobblebase pasture finds per
  skill and zone.

JEI runs on the client, so the pages read assets/cobblesurvival/jei/
drop_sources.json inside the mod, written by tools/jei_sources.py from the
pack's files. Self-tests drop_sources_match_the_server (file vs live server:
drops, zones, own-drop spawns, guardians, End boss, altar) and
jei_pages_build (every page builds, every item exists). The ore a vein gives
moved into world/zones.json (ores -> item), shared with the quest book.
JEI's API (19.64.0.455) is a compile-only dependency from JEI's own Maven.
Self-tests and the island check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-11-03 11:58:18 +01:00
1 parent c5d1cf93f4
commit 72965caedf
13 files changed
+19637 -49

No files matched your search

@@ -1,14 +1,24 @@
package dev.cobblesurvival.selftest
import com.cobblemon.mod.common.api.drop.DropTable
import com.cobblemon.mod.common.api.drop.ItemDropEntry
import com.cobblemon.mod.common.api.pokemon.PokemonSpecies
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
import dev.cobblesurvival.boss.BossDefs
import dev.cobblesurvival.jei.DropSources
import dev.cobblesurvival.jei.DropSourcesJei
import dev.cobblesurvival.machine.MachineDefs
import dev.cobblesurvival.progress.QuestEvents
import dev.cobblesurvival.zone.Guardians
import dev.cobblesurvival.zone.Zones
import java.util.function.Consumer
import net.minecraft.core.registries.BuiltInRegistries
import net.minecraft.gametest.framework.GameTestAssertException
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.item.ItemStack
import net.minecraft.world.item.Items
import net.minecraft.world.level.levelgen.Heightmap
import java.util.function.Consumer
/**
* Roadmap Phase 8 (Adrian, 2026-11-03): the add-ons the quest book and JEI lean on work with the pack. Read by
@@ -20,8 +30,73 @@ object QuestTests {
SelfTestCase("jei_addon_shows_island_spawns", 20, ::addonSpawns),
SelfTestCase("quest_book_loads", 20, ::bookLoads),
SelfTestCase("quest_events_mark_zones", 200, ::zoneEvent),
SelfTestCase("drop_sources_match_the_server", 20, ::dropSources),
SelfTestCase("jei_pages_build", 20, ::jeiPages),
)
/**
* Every JEI page builds (the same code JEI calls on the client), one per Pokémon, guardian, altar tier, vein zone
* and pasture list, and every item on them exists (an unknown id would show as a barrier).
*/
private fun jeiPages(s: Scenario) {
val data = DropSources.data
val pages = mapOf(
"pokemon" to DropSourcesJei.pokemonPages(), "bosses" to DropSourcesJei.bossPages(), "altar" to DropSourcesJei.altarPages(),
"rebirth" to DropSourcesJei.rebirthPages(), "veins" to DropSourcesJei.veinPages(), "finds" to DropSourcesJei.findPages(),
)
s.check(pages.getValue("pokemon").size == data.pokemon.size + data.spawn_drops.size) { "${pages.getValue("pokemon").size} Pokémon pages for ${data.pokemon.size} Pokémon" }
s.check(pages.getValue("bosses").size == data.guardians.size + 1) { "${pages.getValue("bosses").size} boss pages" }
val unknown = pages.values.flatten().flatMap { it.outputs }.map { it.stack }.filter { it.`is`(Items.BARRIER) || it.isEmpty }
s.check(unknown.isEmpty()) { "${unknown.size} items on the JEI pages don't exist" }
val noPicture = pages.getValue("pokemon").count { it.icon?.`is`(Items.BARRIER) != false }
s.check(noPicture == 0) { "$noPicture Pokémon pages without a picture" }
s.helper.succeed()
}
/** A drop as "item chance% ×count", the same way from the JEI data and from Cobblemon's live tables. */
private fun dropKey(item: String, chance: Double?, count: String?) = "$item ${"%.2f".format(chance ?: 100.0)}% ×${count ?: "1"}"
private fun liveDrops(table: DropTable?): Set<String> = table?.entries.orEmpty().filterIsInstance<ItemDropEntry>().map { e ->
dropKey(e.item.toString(), e.percentage.toDouble().takeIf { it < 100.0 }, e.quantityRange?.let { if (it.first == it.last) "${it.first}" else "${it.first}-${it.last}" } ?: e.quantity.toString())
}.toSet()
/**
* The JEI pages' data (assets/cobblesurvival/jei/drop_sources.json, tools/jei_sources.py) still says what the
* server does: every Pokémon's drops and zones, the spawns with drops of their own, the guardians, the End boss
* and the Sacrifice Altar. When this fails, run tools/jei_sources.py and rebuild.
*/
private fun dropSources(s: Scenario) {
val data = DropSources.data
val problems = mutableListOf<String>()
val details = CobblemonSpawnPools.WORLD_SPAWN_POOL.details.filterIsInstance<PokemonSpawnDetail>()
val zonesOf = details.groupBy({ "cobblemon:${it.pokemon.species?.lowercase()}" }, { it.id.substringBefore('-') }).mapValues { it.value.toSet() }
for (p in data.pokemon) {
val species = PokemonSpecies.getByIdentifier(ResourceLocation.parse(p.species)) ?: continue.also { problems += "no species ${p.species}" }
val shown = p.drops.map { dropKey(it.item, it.chance, it.count) }.toSet()
if (shown != liveDrops(species.drops)) problems += "${p.species} drops ${liveDrops(species.drops)}, the JEI data says $shown"
if (p.zones.map { it.zone }.toSet() != zonesOf[p.species].orEmpty()) problems += "${p.species} spawns in ${zonesOf[p.species]}, the JEI data says ${p.zones.map { it.zone }}"
}
val ownDrops = details.filter { it.drops != null }.map { "cobblemon:${it.pokemon.species?.lowercase()} in ${it.id.substringBefore('-')}" }.toSet()
val shownOwn = data.spawn_drops.map { "${it.species} in ${it.zone}" }.toSet()
if (ownDrops != shownOwn) problems += "spawns with their own drops: $ownDrops, the JEI data says $shownOwn"
for (g in data.guardians) {
val def = Guardians.definitions[g.zone] ?: continue.also { problems += "no guardian for ${g.zone}" }
val live = "${def.pokemon.substringBefore(' ')} ${Regex("level=(\\d+)").find(def.pokemon)?.groupValues?.get(1)} ${def.drops.map { "${it.first} ×${it.second}" }}"
val shown = "${g.species.substringAfter(':')} ${g.level} ${g.drops.map { "${it.item} ×${it.count}" }}"
if (live != shown) problems += "guardian of ${g.zone}: $live, the JEI data says $shown"
}
if (data.guardians.size != Guardians.definitions.size) problems += "${Guardians.definitions.size} guardians, the JEI data has ${data.guardians.size}"
BossDefs.end?.end?.let { end ->
if ("cobblemon:${end.species}" != data.end_boss.species || end.level != data.end_boss.level) problems += "the End boss is ${end.species} Lv ${end.level}"
}
val altar = MachineDefs.byCore.values.firstNotNullOfOrNull { it.altar }
val liveTiers = altar?.tiers.orEmpty().map { t -> "${t.minLevel}: ${t.rewards.map { "${BuiltInRegistries.ITEM.getKey(it.item)} ×${it.count}" }}" }
val shownTiers = data.altar.tiers.map { t -> "${t.min_level}: ${t.rewards.map { "${it.item} ×${it.count}" }}" }
if (liveTiers != shownTiers) problems += "altar tiers $liveTiers, the JEI data says $shownTiers"
s.check(problems.isEmpty()) { "${problems.size} differences (run tools/jei_sources.py): ${problems.take(4).joinToString("; ")}" }
s.helper.succeed()
}
/** At least this many quests in the book (docs/quests.yaml: 74 on 2026-11-03). */
private const val MIN_QUESTS = 70