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:
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()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user