- tools/quests.py writes pack/config/ftbquests/quests/ in FTB Quests 2101's format (data.snbt, chapters/*.snbt, texts in lang/en_us.snbt) with ids derived from the YAML names, so progress survives regeneration. Flexible progression, no lock icons, no rewards (Adrian, 2026-11-03). tools/build.py runs it. - docs/quests.yaml: 18 chapters, 74 quests. Getting Started; one chapter per zone and the End from a zone template (reach it, catch three of its Pokemon, mine its ores, beat its guardian) plus chapter extras (Mire Waders, Eyes of Ender, Eternatus); Pokemon Machines; Base and Jobs. - Node positions: --pull reads them from an FTB Quests folder into the layout block (only moved nodes); tools/prism_instance.py pulls from the instance before each sync and rebuilds if something moved. - The pack's own events as hidden advancements (cobblesurvival:events/...), granted by QuestEvents: zone reached, guardian beaten, machine filled, End boss beaten. - Self-tests quest_book_loads and quest_events_mark_zones; the guardian and furnace tests check their events. Self-tests and island check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
6.2 KiB
Kotlin
109 lines
6.2 KiB
Kotlin
package dev.cobblesurvival.selftest
|
|
|
|
import com.cobblemon.mod.common.api.spawning.CobblemonSpawnPools
|
|
import com.cobblemon.mod.common.api.spawning.detail.PokemonSpawnDetail
|
|
import dev.cobblesurvival.progress.QuestEvents
|
|
import dev.cobblesurvival.zone.Zones
|
|
import net.minecraft.gametest.framework.GameTestAssertException
|
|
import net.minecraft.resources.ResourceLocation
|
|
import net.minecraft.world.item.ItemStack
|
|
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
|
|
* reflection, so the companion mod needs neither at compile time.
|
|
*/
|
|
object QuestTests {
|
|
fun all() = listOf(
|
|
SelfTestCase("quests_have_cobblemon_tasks", 20, ::cobblemonTasks),
|
|
SelfTestCase("jei_addon_shows_island_spawns", 20, ::addonSpawns),
|
|
SelfTestCase("quest_book_loads", 20, ::bookLoads),
|
|
SelfTestCase("quest_events_mark_zones", 200, ::zoneEvent),
|
|
)
|
|
|
|
/** At least this many quests in the book (docs/quests.yaml: 74 on 2026-11-03). */
|
|
private const val MIN_QUESTS = 70
|
|
|
|
private fun field(obj: Any, name: String): Any? {
|
|
var c: Class<*>? = obj.javaClass
|
|
while (c != null) {
|
|
runCatching { return c.getDeclaredField(name).apply { isAccessible = true }.get(obj) }
|
|
c = c.superclass
|
|
}
|
|
throw GameTestAssertException("${obj.javaClass.simpleName} has no field $name")
|
|
}
|
|
|
|
/**
|
|
* FTB Quests loaded the generated book (tools/quests.py): enough quests, flexible progression, and every task
|
|
* understood: no unknown task type, every item exists, every advancement exists on the server.
|
|
*/
|
|
private fun bookLoads(s: Scenario) {
|
|
val file = Class.forName("dev.ftb.mods.ftbquests.quest.ServerQuestFile").getField("INSTANCE").get(null)
|
|
?: throw GameTestAssertException("FTB Quests has no server quest file")
|
|
val quests = mutableListOf<Any>()
|
|
file.javaClass.getMethod("forAllQuests", Consumer::class.java).invoke(file, Consumer<Any> { quests += it })
|
|
s.check(quests.size >= MIN_QUESTS) { "FTB Quests loaded ${quests.size} quests, not the book's $MIN_QUESTS or more" }
|
|
s.check(field(file, "progressionMode").toString().equals("flexible", ignoreCase = true)) { "the book isn't in flexible mode" }
|
|
val problems = mutableListOf<String>()
|
|
for (quest in quests) {
|
|
for (task in quest.javaClass.getMethod("getTasks").invoke(quest) as Collection<*>) {
|
|
when (task!!.javaClass.simpleName) {
|
|
"CustomTask" -> problems += "unknown task type in quest ${quest}"
|
|
"ItemTask" -> if ((field(task, "itemStack") as ItemStack).isEmpty) problems += "an item task with no item in $quest"
|
|
"AdvancementTask" -> {
|
|
val id = field(task, "advancement") as ResourceLocation
|
|
if (s.level.server.advancements.get(id) == null) problems += "no advancement $id"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
s.check(problems.isEmpty()) { problems.take(5).joinToString("; ") }
|
|
s.helper.succeed()
|
|
}
|
|
|
|
/** Walking into a zone marks the hidden advancement its "reach the zone" quest checks (progress/QuestEvents.kt). */
|
|
private fun zoneEvent(s: Scenario) {
|
|
val sample = Zones.samples["button_isle"] ?: throw GameTestAssertException("no sample point for Button Isle")
|
|
val player = s.player()
|
|
s.helper.startSequence()
|
|
.thenExecute {
|
|
s.level.getChunk(sample.x shr 4, sample.z shr 4)
|
|
s.moveTo(player, sample.x + 0.5, s.level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, sample.x, sample.z).toDouble(), sample.z + 0.5)
|
|
}
|
|
.thenWaitUntil { s.check(QuestEvents.done(player, "zone/button_isle")) { "reaching Button Isle didn't mark zone/button_isle" } }
|
|
.thenSucceed()
|
|
}
|
|
|
|
/** Cobblemon Quests Reloaded adds its task type (catch, defeat, evolve, level up, ...) to FTB Quests. */
|
|
private fun cobblemonTasks(s: Scenario) {
|
|
val types = Class.forName("dev.ftb.mods.ftbquests.quest.task.TaskTypes").getField("TYPES").get(null) as Map<*, *>
|
|
s.check(types.keys.any { it.toString() == "cobblemon_tasks:cobblemon_task" }) { "FTB Quests has no Cobblemon task type: ${types.keys}" }
|
|
s.helper.succeed()
|
|
}
|
|
|
|
/**
|
|
* Cobblemon Info for REI/JEI/EMI shows what the pack's own spawn pools say (Adrian: check that its spawn info is
|
|
* right for the pack): every entry it lists is one of the zone pools' entries, at the zone's levels, and it lists
|
|
* all of them. It can't show the zone itself, only the zone's biomes (docs/design.md, Phase 8).
|
|
*/
|
|
private fun addonSpawns(s: Scenario) {
|
|
val loader = Class.forName("com.cobbledex.SpawnDataLoader")
|
|
val shown = (loader.getMethod("loadFromRuntime").invoke(loader.getField("INSTANCE").get(null)) as Map<*, *>).values.flatMap { it as List<*> }
|
|
fun field(info: Any?, name: String) = info!!.javaClass.getMethod("get" + name.replaceFirstChar { it.uppercase() }).invoke(info) as String
|
|
val ours = CobblemonSpawnPools.WORLD_SPAWN_POOL.details.filterIsInstance<PokemonSpawnDetail>().associateBy { it.id }
|
|
s.check(shown.isNotEmpty()) { "the JEI add-on shows no spawns" }
|
|
val strangers = shown.map { field(it, "id") }.filter { it !in ours }
|
|
s.check(strangers.isEmpty()) { "the JEI add-on shows ${strangers.size} spawns the pack doesn't have, e.g. ${strangers.take(5)}" }
|
|
val wrongLevels = shown.filter { info ->
|
|
val zone = Zones.byId[field(info, "id").substringBefore('-')] ?: return@filter true
|
|
val (low, high) = field(info, "levelRange").split("-").map { it.toInt() }
|
|
low < zone.minLevel || high > zone.maxLevel
|
|
}
|
|
s.check(wrongLevels.isEmpty()) { "the JEI add-on shows levels outside the zone's band: ${wrongLevels.take(5).map { field(it, "id") + " " + field(it, "levelRange") }}" }
|
|
val missing = ours.keys - shown.map { field(it, "id") }.toSet()
|
|
s.check(missing.isEmpty()) { "the JEI add-on leaves out ${missing.size} of the pack's spawns, e.g. ${missing.take(5)}" }
|
|
s.helper.succeed()
|
|
}
|
|
}
|