Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,19 @@ data class PhaseConfig(
}
}

@ConfigSerializable
data class ParentPhase(
val id: String,
val weightOverride: Int? = null
)
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing validation for weightOverride: The ParentPhase class accepts an optional weightOverride parameter but doesn't validate that it's non-negative. A negative or zero weightOverride could lead to unexpected behavior in the weight calculations. Consider adding an init block to ParentPhase that validates weightOverride?.let { require(it > 0) { "weightOverride must be greater than 0 if specified" } }.

Suggested change
)
) {
init {
weightOverride?.let {
require(it > 0) { "weightOverride must be greater than 0 if specified" }
}
}
}

Copilot uses AI. Check for mistakes.

@ConfigSerializable
data class Phase(
val id: String,
val displayName: String = id.replaceFirstChar { it.uppercaseChar() }.replace('_', ' '),
val startsAt: Int,
val weight: Int,
val parents: List<String>,
val parents: List<ParentPhase>,
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential breaking change for configuration serialization: Changing the type of parents from List<String> to List<ParentPhase> changes the serialization format. While Configurate (Sponge) may automatically handle deserialization of simple strings to ParentPhase objects (since ParentPhase has a primary constructor accepting a String), this should be verified. Existing YAML configs with parents: ["phase_id"] need to work with the new structure. Consider testing backward compatibility or providing migration guidance if automatic coercion doesn't work.

Copilot uses AI. Check for mistakes.
val blocks: List<BlockEntry>,
val entities: List<EntityEntry>
) {
Expand Down Expand Up @@ -92,29 +98,32 @@ data class PhaseConfig(
var remainingBudget = parentBudget

var levelFactor = 1.0
var levelFactorSum = 0.0
val levelFactors = ArrayList<Double>(parents.size)
for (i in parents.indices) {
levelFactors += levelFactor
levelFactorSum += levelFactor
val levelFactors = parents.map {
val f = levelFactor
levelFactor *= PARENT_DECAY
f
}

for ((idx, parentId) in parents.withIndex()) {
val effectiveParentWeights = parents.mapIndexed { idx, parentPhase ->
val override = parentPhase.weightOverride?.toDouble()
(levelFactors[idx]) * (override ?: 1.0)
}

val totalParentWeight = effectiveParentWeights.sum()

for ((idx, parentPhase) in parents.withIndex()) {
if (remainingBudget <= 1e-9) break
val parent = config.findById(parentId) ?: continue
val parent = config.findById(parentPhase.id) ?: continue
Comment on lines +107 to +116
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug in parent weight distribution: The effectiveParentWeights and totalParentWeight are calculated before checking if parent phases exist (line 116). If a parent phase ID is invalid or doesn't exist, it still contributes to totalParentWeight, causing incorrect budget distribution among valid parents. For example, if two parents have equal weights but one doesn't exist, the valid parent only receives 50% of the budget instead of 100%. Consider filtering out non-existent parents before calculating effectiveParentWeights, or recalculating the distribution after skipping invalid parents.

Copilot uses AI. Check for mistakes.
val parentBlocks = parent.blocks
if (parentBlocks.isEmpty()) continue

val parentRawTotal = parentBlocks.sumOf { it.weight.toDouble() }
if (parentRawTotal <= 0.0) continue

val shareForThisParent = if (levelFactorSum > 0.0)
parentBudget * (levelFactors[idx] / levelFactorSum)
else 0.0
val shareForThisParent =
if (totalParentWeight > 0.0)
parentBudget * (effectiveParentWeights[idx] / totalParentWeight)
else 0.0

val assigned = shareForThisParent.coerceAtMost(remainingBudget)
val scale = assigned / parentRawTotal
val scale = assigned / parentBlocks.sumOf { it.weight.toDouble() }

choices.ensureCapacity(choices.size + parentBlocks.size)
for (entry in parentBlocks) {
Expand Down Expand Up @@ -144,34 +153,38 @@ data class PhaseConfig(

val extraSteps = (this.weight - 1).coerceAtLeast(0)
val parentShare = (extraSteps * PARENT_SHARE_PER_WEIGHT).coerceIn(0.0, PARENT_SHARE_MAX)

val parentBudget = (if (ownTotal > 0.0) ownTotal else 1.0) * parentShare
var remainingBudget = parentBudget

var levelFactor = 1.0
var levelFactorSum = 0.0
val levelFactors = ArrayList<Double>(parents.size)
for (i in parents.indices) {
levelFactors += levelFactor
levelFactorSum += levelFactor
val levelFactors = parents.map {
val f = levelFactor
levelFactor *= PARENT_DECAY
f
}

val effectiveParentWeights = parents.mapIndexed { idx, parentPhase ->
val override = parentPhase.weightOverride?.toDouble()
(levelFactors[idx]) * (override ?: 1.0)
}

for ((idx, parentId) in parents.withIndex()) {
val totalParentWeight = effectiveParentWeights.sum()

for ((idx, parentPhase) in parents.withIndex()) {
if (remainingBudget <= 1e-9) break
val parent = config.findById(parentId) ?: continue
val parent = config.findById(parentPhase.id) ?: continue
Comment on lines +166 to +175
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug in parent weight distribution: The effectiveParentWeights and totalParentWeight are calculated before checking if parent phases exist (line 175). If a parent phase ID is invalid or doesn't exist, it still contributes to totalParentWeight, causing incorrect budget distribution among valid parents. For example, if two parents have equal weights but one doesn't exist, the valid parent only receives 50% of the budget instead of 100%. Consider filtering out non-existent parents before calculating effectiveParentWeights, or recalculating the distribution after skipping invalid parents.

Copilot uses AI. Check for mistakes.
val parentEntities = parent.entities
if (parentEntities.isEmpty()) continue

val parentRawTotal = parentEntities.sumOf { it.weight }
if (parentRawTotal <= 0.0) continue

val shareForThisParent = if (levelFactorSum > 0.0)
parentBudget * (levelFactors[idx] / levelFactorSum)
else 0.0
val shareForThisParent =
if (totalParentWeight > 0.0)
parentBudget * (effectiveParentWeights[idx] / totalParentWeight)
else 0.0

val assigned = shareForThisParent.coerceAtMost(remainingBudget)
val scale = assigned / parentRawTotal
val parentTotal = parentEntities.sumOf { it.weight }
if (parentTotal <= 0.0) continue
val scale = assigned / parentTotal

for (e in parentEntities) {
val w = e.weight * scale
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ val defaultPhases = listOf(
id = "early_mining",
startsAt = 150,
weight = 2,
parents = listOf("start_plains"),
parents = listOf(ParentPhase("start_plains")),
blocks = listOf(
BlockEntry(data = "minecraft:stone", weight = 50),
BlockEntry(data = "minecraft:coal_ore", weight = 20),
Expand All @@ -40,7 +40,7 @@ val defaultPhases = listOf(
id = "early_caves",
startsAt = 350,
weight = 2,
parents = listOf("early_mining"),
parents = listOf(ParentPhase("early_mining")),
blocks = listOf(
BlockEntry(data = "minecraft:stone", weight = 40),
BlockEntry(data = "minecraft:coal_ore", weight = 25),
Expand All @@ -57,7 +57,7 @@ val defaultPhases = listOf(
id = "iron_age",
startsAt = 700,
weight = 3,
parents = listOf("early_caves"),
parents = listOf(ParentPhase("early_caves")),
blocks = listOf(
BlockEntry(data = "minecraft:stone", weight = 35),
BlockEntry(data = "minecraft:iron_ore", weight = 30),
Expand All @@ -74,7 +74,7 @@ val defaultPhases = listOf(
id = "abandoned_mineshaft",
startsAt = 1100,
weight = 3,
parents = listOf("iron_age"),
parents = listOf(ParentPhase("iron_age")),
blocks = listOf(
BlockEntry(data = "minecraft:oak_planks", weight = 20),
BlockEntry(data = "minecraft:oak_log", weight = 15),
Expand All @@ -91,7 +91,7 @@ val defaultPhases = listOf(
id = "deep_caves",
startsAt = 1600,
weight = 4,
parents = listOf("iron_age"),
parents = listOf(ParentPhase("iron_age")),
blocks = listOf(
BlockEntry(data = "minecraft:deepslate", weight = 40),
BlockEntry(data = "minecraft:iron_ore", weight = 20),
Expand All @@ -108,7 +108,7 @@ val defaultPhases = listOf(
id = "redstone_labs",
startsAt = 2200,
weight = 4,
parents = listOf("deep_caves"),
parents = listOf(ParentPhase("deep_caves")),
blocks = listOf(
BlockEntry(data = "minecraft:redstone_ore", weight = 35),
BlockEntry(data = "minecraft:deepslate", weight = 30),
Expand All @@ -125,7 +125,7 @@ val defaultPhases = listOf(
id = "lava_depths",
startsAt = 3000,
weight = 5,
parents = listOf("deep_caves"),
parents = listOf(ParentPhase("deep_caves")),
blocks = listOf(
BlockEntry(data = "minecraft:basalt", weight = 30),
BlockEntry(data = "minecraft:magma_block", weight = 15),
Expand All @@ -141,7 +141,7 @@ val defaultPhases = listOf(
id = "dripstone_caves",
startsAt = 3600,
weight = 5,
parents = listOf("deep_caves"),
parents = listOf(ParentPhase("deep_caves")),
blocks = listOf(
BlockEntry(data = "minecraft:dripstone_block", weight = 40),
BlockEntry(data = "minecraft:pointed_dripstone", weight = 20),
Expand All @@ -157,7 +157,7 @@ val defaultPhases = listOf(
id = "lush_caves",
startsAt = 4200,
weight = 5,
parents = listOf("deep_caves"),
parents = listOf(ParentPhase("deep_caves")),
blocks = listOf(
BlockEntry(data = "minecraft:moss_block", weight = 30),
BlockEntry(data = "minecraft:clay", weight = 25),
Expand All @@ -173,7 +173,7 @@ val defaultPhases = listOf(
id = "nether_entry",
startsAt = 5000,
weight = 6,
parents = listOf("lava_depths"),
parents = listOf(ParentPhase("lava_depths")),
blocks = listOf(
BlockEntry(data = "minecraft:netherrack", weight = 60),
BlockEntry(data = "minecraft:nether_quartz_ore", weight = 20),
Expand All @@ -189,7 +189,7 @@ val defaultPhases = listOf(
id = "nether_fortress",
startsAt = 5800,
weight = 7,
parents = listOf("nether_entry"),
parents = listOf(ParentPhase("nether_entry")),
blocks = listOf(
BlockEntry(data = "minecraft:nether_bricks", weight = 40),
BlockEntry(data = "minecraft:soul_sand", weight = 20),
Expand All @@ -205,7 +205,7 @@ val defaultPhases = listOf(
id = "crimson_forest",
startsAt = 6500,
weight = 7,
parents = listOf("nether_entry"),
parents = listOf(ParentPhase("nether_entry")),
blocks = listOf(
BlockEntry(data = "minecraft:crimson_nylium", weight = 30),
BlockEntry(data = "minecraft:crimson_stem", weight = 25),
Expand All @@ -221,7 +221,7 @@ val defaultPhases = listOf(
id = "warped_forest",
startsAt = 7200,
weight = 7,
parents = listOf("nether_entry"),
parents = listOf(ParentPhase("nether_entry")),
blocks = listOf(
BlockEntry(data = "minecraft:warped_nylium", weight = 30),
BlockEntry(data = "minecraft:warped_stem", weight = 25),
Expand All @@ -237,7 +237,7 @@ val defaultPhases = listOf(
id = "basalt_deltas",
startsAt = 7800,
weight = 8,
parents = listOf("nether_entry"),
parents = listOf(ParentPhase("nether_entry")),
blocks = listOf(
BlockEntry(data = "minecraft:basalt", weight = 50),
BlockEntry(data = "minecraft:blackstone", weight = 30),
Expand All @@ -252,7 +252,7 @@ val defaultPhases = listOf(
id = "diamond_depths",
startsAt = 9000,
weight = 8,
parents = listOf("deep_caves"),
parents = listOf(ParentPhase("deep_caves")),
blocks = listOf(
BlockEntry(data = "minecraft:deepslate", weight = 50),
BlockEntry(data = "minecraft:diamond_ore", weight = 15),
Expand All @@ -269,7 +269,7 @@ val defaultPhases = listOf(
id = "ancient_city",
startsAt = 10000,
weight = 9,
parents = listOf("diamond_depths"),
parents = listOf(ParentPhase("diamond_depths")),
blocks = listOf(
BlockEntry(data = "minecraft:sculk", weight = 40),
BlockEntry(data = "minecraft:sculk_catalyst", weight = 10),
Expand All @@ -286,7 +286,7 @@ val defaultPhases = listOf(
id = "stronghold",
startsAt = 11000,
weight = 9,
parents = listOf("diamond_depths"),
parents = listOf(ParentPhase("diamond_depths")),
blocks = listOf(
BlockEntry(data = "minecraft:stone_bricks", weight = 40),
BlockEntry(data = "minecraft:cracked_stone_bricks", weight = 15),
Expand All @@ -303,7 +303,7 @@ val defaultPhases = listOf(
id = "the_end",
startsAt = 13000,
weight = 10,
parents = listOf("stronghold"),
parents = listOf(ParentPhase("stronghold")),
blocks = listOf(
BlockEntry(data = "minecraft:end_stone", weight = 70),
BlockEntry(data = "minecraft:obsidian", weight = 20),
Expand All @@ -318,7 +318,7 @@ val defaultPhases = listOf(
id = "end_cities",
startsAt = 15000,
weight = 11,
parents = listOf("the_end"),
parents = listOf(ParentPhase("the_end")),
blocks = listOf(
BlockEntry(data = "minecraft:purpur_block", weight = 40),
BlockEntry(data = "minecraft:end_stone", weight = 30),
Expand All @@ -335,7 +335,7 @@ val defaultPhases = listOf(
id = "overworld_chaos",
startsAt = 17000,
weight = 12,
parents = listOf("the_end"),
parents = listOf(ParentPhase("the_end")),
blocks = listOf(
BlockEntry(data = "minecraft:stone", weight = 30),
BlockEntry(data = "minecraft:deepslate", weight = 30),
Expand All @@ -352,7 +352,7 @@ val defaultPhases = listOf(
id = "event_finale",
startsAt = 20000,
weight = 13,
parents = listOf("overworld_chaos"),
parents = listOf(ParentPhase("overworld_chaos")),
blocks = listOf(
BlockEntry(data = "minecraft:ancient_debris", weight = 10),
BlockEntry(data = "minecraft:netherite_block", weight = 1),
Expand Down