Cobblemon Survival Pack: phases 0-2 and Phase 3 zones and hazards

Free NeoForge 1.21.1 modpack on Cobblemon 1.8.4 built with packwiz, plus
the Kotlin companion mod (cobblesurvival) and its built-in data pack.

- Phase 0: packwiz pack with Modrinth/CurseForge cross-links, build and
  export tooling, Prism instance sync, test server with smoke checks.
- Phase 1: Poke Ball shaped island (WorldGen Editor preset, seed isle-26),
  Adrian's biome list, no natural ore, structures without ore or vanilla
  mobs, no Nether portals, world template builder and world report.
- Phase 2: permadeath (CobbleHardcoreMon plus the companion mod's gap
  fixes), Revival Charm, expensive Healing Machine, Lootr refreshing
  loot, in-game self-tests on Minecraft's GameTest framework.
- Phase 3 so far: 14 zones with their own spawn pools and level bands
  (tools/zone_layout.py, tools/zone_spawns.py, "zone" spawn condition),
  Cold Sweat heat and cold as gates with boss-drop insulation recipes,
  spawn survey and hazard self-tests in tools/quick_check.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
claudeandClaude Opus 5.5 committed 2026-10-29 19:01:21 +01:00
commit 398301f0be
232 files changed
+72665

No files matched your search

@@ -0,0 +1,125 @@
package dev.cobblesurvival.selftest
import com.mojang.brigadier.builder.LiteralArgumentBuilder
import dev.cobblesurvival.CobbleSurvival
import net.minecraft.commands.CommandSourceStack
import net.minecraft.commands.Commands
import net.minecraft.core.BlockPos
import net.minecraft.gametest.framework.GameTestInfo
import net.minecraft.gametest.framework.GameTestRunner
import net.minecraft.gametest.framework.GameTestTicker
import net.minecraft.gametest.framework.RetryOptions
import net.minecraft.gametest.framework.StructureGridSpawner
import net.minecraft.gametest.framework.TestFunction
import net.minecraft.network.chat.Component
import net.minecraft.server.MinecraftServer
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.biome.Biomes
import net.minecraft.world.level.block.Rotation
import net.neoforged.neoforge.common.NeoForge
import net.neoforged.neoforge.event.tick.ServerTickEvent
/**
* One in-game test: [body] sets it up through its [Scenario] and must end in a pass or a failure within [maxTicks].
* [islandOnly] tests need the island world and are skipped elsewhere (tools/selftest.py uses a flat world).
* Tests of one [batch] run side by side; batches run one after another (a test that changes the world around it,
* like the biome or the time of day, gets a batch of its own).
*/
class SelfTestCase(
val name: String,
val maxTicks: Int,
val body: (Scenario) -> Unit,
val islandOnly: Boolean = false,
val batch: String = CobbleSurvival.MOD_ID,
)
/**
* In-game tests on a real server with all of the pack's mods and configs: `/cobblesurvival selftest run` starts them
* on a platform high above spawn, `/cobblesurvival selftest status` reports. They use the vanilla GameTest
* framework, which NeoForge only switches on in a development environment, so this object creates and ticks the
* tests itself. Only there when the server runs with `-Dcobblesurvival.selftest=true` (tools/quick_check.py);
* players never see the command.
*/
object SelfTest {
val enabled: Boolean = java.lang.Boolean.getBoolean("cobblesurvival.selftest")
private const val TEMPLATE = "cobblesurvival:selftest/empty"
private const val PLATFORM_Y = 200
private const val TESTS_PER_ROW = 4
private const val FORCED_CHUNKS = 9
private val cases: List<SelfTestCase> get() =
PermadeathTests.all() + SurvivalRulesTests.all() + ZoneTests.all() + HazardTests.all()
private var running: List<Scenario> = emptyList()
private var forced: List<ChunkPos> = emptyList()
private var summary = "selftest not run"
fun register() {
if (enabled) NeoForge.EVENT_BUS.addListener(::onServerTick)
}
fun command(): LiteralArgumentBuilder<CommandSourceStack> =
Commands.literal("selftest")
.requires { it.hasPermission(4) }
.then(Commands.literal("run").executes { context ->
val reply = start(context.source.server)
context.source.sendSuccess({ Component.literal(reply) }, false)
1
})
.then(Commands.literal("status").executes { context ->
val done = running.count { it.result != null }
val reply = if (running.isNotEmpty()) "selftest running: $done/${running.size} done" else summary
context.source.sendSuccess({ Component.literal(reply) }, false)
1
})
private fun start(server: MinecraftServer): String {
if (running.isNotEmpty()) return "selftest already running"
val level = server.overworld()
val spawn = level.sharedSpawnPos
val origin = BlockPos(spawn.x, PLATFORM_Y, spawn.z)
val corner = ChunkPos(origin)
forced = (0 until FORCED_CHUNKS).flatMap { dx -> (0 until FORCED_CHUNKS).map { dz -> ChunkPos(corner.x + dx, corner.z + dz) } }
forced.forEach { level.setChunkForced(it.x, it.z, true) }
// Only the island world type puts basalt deltas into the overworld.
val island = level.chunkSource.generator.biomeSource.possibleBiomes().any { it.`is`(Biomes.BASALT_DELTAS) }
val (runnable, skipped) = cases.partition { island || !it.islandOnly }
val scenarios = runnable.mapIndexed { index, case -> Scenario(case, index) }
val infos = scenarios.map { scenario ->
val function = TestFunction(scenario.case.batch, "${CobbleSurvival.MOD_ID}.${scenario.case.name}", TEMPLATE,
scenario.case.maxTicks, 0L, true) { helper -> scenario.begin(helper) }
GameTestInfo(function, Rotation.NONE, level, RetryOptions.noRetries()).also { it.addListener(scenario) }
}
running = scenarios
GameTestTicker.SINGLETON.clear()
GameTestRunner.Builder.fromInfo(infos, level)
.newStructureSpawner(StructureGridSpawner(origin, TESTS_PER_ROW, false))
.build()
.start()
val skippedNote = if (skipped.isEmpty()) "" else ", skipped ${skipped.size} that need the island world"
CobbleSurvival.LOGGER.info("selftest started {} tests at {}{}", infos.size, origin.toShortString(), skippedNote)
return "selftest started ${infos.size} tests$skippedNote"
}
private fun onServerTick(event: ServerTickEvent.Post) {
if (running.isEmpty()) return
try {
GameTestTicker.SINGLETON.tick()
} catch (e: Exception) {
// Only assertion failures are handled by the framework; anything else would crash the server.
CobbleSurvival.LOGGER.error("selftest: a test threw; ending the run", e)
running.forEach { it.abort(e) }
}
if (running.any { it.result == null }) return
val failed = running.filter { !it.passed }
summary = "selftest finished: ${running.size - failed.size}/${running.size} passed" +
failed.joinToString("") { "; FAILED ${it.case.name}: ${it.result}" }
if (failed.isEmpty()) CobbleSurvival.LOGGER.info(summary) else CobbleSurvival.LOGGER.error(summary)
GameTestTicker.SINGLETON.clear()
val level = event.server.overworld()
forced.forEach { level.setChunkForced(it.x, it.z, false) }
running = emptyList()
}
}