Phase 2: Core math (BezierCurve, Vec3d) and road data structures (RoadProfile, RoadSegment)
This commit is contained in:
201
src/main/kotlin/com/bnair/roadrunner/math/BezierCurve.kt
Normal file
201
src/main/kotlin/com/bnair/roadrunner/math/BezierCurve.kt
Normal file
@@ -0,0 +1,201 @@
|
||||
package com.bnair.roadrunner.math
|
||||
|
||||
/**
|
||||
* A Cubic Bezier Curve in 3D space.
|
||||
*
|
||||
* The curve is defined by 4 control points:
|
||||
* - p0: Start point (the curve passes through this)
|
||||
* - p1: First control point (influences curve direction from start)
|
||||
* - p2: Second control point (influences curve direction towards end)
|
||||
* - p3: End point (the curve passes through this)
|
||||
*
|
||||
* The curve is parametrized by t in [0, 1].
|
||||
*/
|
||||
data class BezierCurve(
|
||||
val p0: Vec3d,
|
||||
val p1: Vec3d,
|
||||
val p2: Vec3d,
|
||||
val p3: Vec3d
|
||||
) {
|
||||
/**
|
||||
* Evaluates the curve at parameter t (0 to 1).
|
||||
* Uses the cubic Bezier formula: B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
|
||||
*/
|
||||
fun evaluate(t: Double): Vec3d {
|
||||
val u = 1.0 - t
|
||||
val tt = t * t
|
||||
val uu = u * u
|
||||
val uuu = uu * u
|
||||
val ttt = tt * t
|
||||
|
||||
return p0 * uuu +
|
||||
p1 * (3.0 * uu * t) +
|
||||
p2 * (3.0 * u * tt) +
|
||||
p3 * ttt
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the tangent (first derivative) at parameter t.
|
||||
* This gives the direction of the curve at that point.
|
||||
* B'(t) = 3(1-t)²(P1-P0) + 6(1-t)t(P2-P1) + 3t²(P3-P2)
|
||||
*/
|
||||
fun tangent(t: Double): Vec3d {
|
||||
val u = 1.0 - t
|
||||
val uu = u * u
|
||||
val tt = t * t
|
||||
|
||||
return (p1 - p0) * (3.0 * uu) +
|
||||
(p2 - p1) * (6.0 * u * t) +
|
||||
(p3 - p2) * (3.0 * tt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a normalized tangent (unit direction vector) at parameter t.
|
||||
*/
|
||||
fun direction(t: Double): Vec3d = tangent(t).normalize()
|
||||
|
||||
/**
|
||||
* Computes the second derivative (acceleration) at parameter t.
|
||||
* Useful for calculating curvature and banking.
|
||||
* B''(t) = 6(1-t)(P2-2P1+P0) + 6t(P3-2P2+P1)
|
||||
*/
|
||||
fun acceleration(t: Double): Vec3d {
|
||||
val u = 1.0 - t
|
||||
return (p2 - p1 * 2.0 + p0) * (6.0 * u) +
|
||||
(p3 - p2 * 2.0 + p1) * (6.0 * t)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the curvature at parameter t.
|
||||
* Curvature = |B'(t) × B''(t)| / |B'(t)|³
|
||||
* Higher curvature = sharper turn = more banking needed.
|
||||
*/
|
||||
fun curvature(t: Double): Double {
|
||||
val d1 = tangent(t)
|
||||
val d2 = acceleration(t)
|
||||
val cross = d1.cross(d2)
|
||||
val d1Len = d1.length()
|
||||
return if (d1Len > 0.0) cross.length() / (d1Len * d1Len * d1Len) else 0.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Samples the curve into discrete points.
|
||||
* @param segments Number of segments (returns segments + 1 points)
|
||||
* @return List of points along the curve
|
||||
*/
|
||||
fun sample(segments: Int): List<Vec3d> {
|
||||
require(segments > 0) { "Segments must be positive" }
|
||||
return (0..segments).map { i ->
|
||||
evaluate(i.toDouble() / segments)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Samples the curve adaptively based on curvature.
|
||||
* Straighter sections get fewer samples, curves get more.
|
||||
* @param minSegments Minimum number of segments
|
||||
* @param maxSegments Maximum number of segments
|
||||
* @param curvatureThreshold How sensitive to curvature changes
|
||||
*/
|
||||
fun sampleAdaptive(
|
||||
minSegments: Int = 10,
|
||||
maxSegments: Int = 100,
|
||||
curvatureThreshold: Double = 0.1
|
||||
): List<Vec3d> {
|
||||
val points = mutableListOf<Vec3d>()
|
||||
points.add(p0)
|
||||
|
||||
fun subdivide(tStart: Double, tEnd: Double, depth: Int) {
|
||||
if (depth > 10) { // Prevent infinite recursion
|
||||
points.add(evaluate(tEnd))
|
||||
return
|
||||
}
|
||||
|
||||
val tMid = (tStart + tEnd) / 2.0
|
||||
val curveMid = curvature(tMid)
|
||||
|
||||
// If curvature is high or segment is large, subdivide
|
||||
val segmentLength = (evaluate(tEnd) - evaluate(tStart)).length()
|
||||
if (curveMid > curvatureThreshold && segmentLength > 1.0 && points.size < maxSegments) {
|
||||
subdivide(tStart, tMid, depth + 1)
|
||||
subdivide(tMid, tEnd, depth + 1)
|
||||
} else {
|
||||
points.add(evaluate(tEnd))
|
||||
}
|
||||
}
|
||||
|
||||
val initialSegments = minSegments.coerceAtLeast(4)
|
||||
for (i in 0 until initialSegments) {
|
||||
val tStart = i.toDouble() / initialSegments
|
||||
val tEnd = (i + 1).toDouble() / initialSegments
|
||||
subdivide(tStart, tEnd, 0)
|
||||
}
|
||||
|
||||
return points.distinct()
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximates the total arc length of the curve.
|
||||
* Uses numerical integration (sampling).
|
||||
*/
|
||||
fun arcLength(segments: Int = 100): Double {
|
||||
var length = 0.0
|
||||
var prev = p0
|
||||
for (i in 1..segments) {
|
||||
val curr = evaluate(i.toDouble() / segments)
|
||||
length += (curr - prev).length()
|
||||
prev = curr
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new curve that smoothly continues from this one.
|
||||
* The new curve starts at this curve's end with matching tangent.
|
||||
* @param endPoint Where the new curve should end
|
||||
* @param endTangentScale How far the control point extends (affects curve shape)
|
||||
*/
|
||||
fun continueTo(endPoint: Vec3d, endTangentScale: Double = 0.3): BezierCurve {
|
||||
val startTangent = tangent(1.0).normalize()
|
||||
val newP1 = p3 + startTangent * (p3 - p2).length() // Match incoming tangent
|
||||
val curveLength = (endPoint - p3).length()
|
||||
val newP2 = endPoint - (endPoint - newP1).normalize() * (curveLength * endTangentScale)
|
||||
return BezierCurve(p3, newP1, newP2, endPoint)
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a simple straight line as a degenerate Bezier curve.
|
||||
*/
|
||||
fun line(start: Vec3d, end: Vec3d): BezierCurve {
|
||||
val third = (end - start) / 3.0
|
||||
return BezierCurve(
|
||||
start,
|
||||
start + third,
|
||||
end - third,
|
||||
end
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a curve from start to end with specified start and end tangents.
|
||||
* This is useful for connecting road segments with specific angles.
|
||||
*/
|
||||
fun fromTangents(
|
||||
start: Vec3d,
|
||||
startTangent: Vec3d,
|
||||
end: Vec3d,
|
||||
endTangent: Vec3d,
|
||||
tangentScale: Double = 0.3
|
||||
): BezierCurve {
|
||||
val distance = (end - start).length()
|
||||
val scale = distance * tangentScale
|
||||
return BezierCurve(
|
||||
start,
|
||||
start + startTangent.normalize() * scale,
|
||||
end - endTangent.normalize() * scale,
|
||||
end
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/main/kotlin/com/bnair/roadrunner/math/Vec3d.kt
Normal file
63
src/main/kotlin/com/bnair/roadrunner/math/Vec3d.kt
Normal file
@@ -0,0 +1,63 @@
|
||||
package com.bnair.roadrunner.math
|
||||
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* A simple 3D vector class for road calculations.
|
||||
* Using our own to avoid loader-specific dependencies in core math.
|
||||
*/
|
||||
data class Vec3d(
|
||||
val x: Double,
|
||||
val y: Double,
|
||||
val z: Double
|
||||
) {
|
||||
operator fun plus(other: Vec3d) = Vec3d(x + other.x, y + other.y, z + other.z)
|
||||
operator fun minus(other: Vec3d) = Vec3d(x - other.x, y - other.y, z - other.z)
|
||||
operator fun times(scalar: Double) = Vec3d(x * scalar, y * scalar, z * scalar)
|
||||
operator fun div(scalar: Double) = Vec3d(x / scalar, y / scalar, z / scalar)
|
||||
|
||||
fun dot(other: Vec3d): Double = x * other.x + y * other.y + z * other.z
|
||||
|
||||
fun cross(other: Vec3d) = Vec3d(
|
||||
y * other.z - z * other.y,
|
||||
z * other.x - x * other.z,
|
||||
x * other.y - y * other.x
|
||||
)
|
||||
|
||||
fun length(): Double = sqrt(x * x + y * y + z * z)
|
||||
|
||||
fun lengthSquared(): Double = x * x + y * y + z * z
|
||||
|
||||
fun normalize(): Vec3d {
|
||||
val len = length()
|
||||
return if (len > 0.0) this / len else this
|
||||
}
|
||||
|
||||
fun lerp(other: Vec3d, t: Double): Vec3d {
|
||||
return this + (other - this) * t
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a perpendicular vector in the XZ plane (horizontal).
|
||||
* Useful for calculating road width offsets.
|
||||
*/
|
||||
fun perpendicularXZ(): Vec3d {
|
||||
return Vec3d(-z, 0.0, x).normalize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to block coordinates (floored integers).
|
||||
*/
|
||||
fun toBlockPos(): Triple<Int, Int, Int> {
|
||||
return Triple(
|
||||
kotlin.math.floor(x).toInt(),
|
||||
kotlin.math.floor(y).toInt(),
|
||||
kotlin.math.floor(z).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ZERO = Vec3d(0.0, 0.0, 0.0)
|
||||
val UP = Vec3d(0.0, 1.0, 0.0)
|
||||
}
|
||||
}
|
||||
131
src/main/kotlin/com/bnair/roadrunner/road/RoadProfile.kt
Normal file
131
src/main/kotlin/com/bnair/roadrunner/road/RoadProfile.kt
Normal file
@@ -0,0 +1,131 @@
|
||||
package com.bnair.roadrunner.road
|
||||
|
||||
import com.bnair.roadrunner.math.Vec3d
|
||||
|
||||
enum class LaneType {
|
||||
DRIVING,
|
||||
PUBLIC_TRANSPORT,
|
||||
PEDESTRIAN,
|
||||
SHOULDER,
|
||||
MEDIAN_BARRIER,
|
||||
MEDIAN_GRASS
|
||||
}
|
||||
|
||||
enum class TerrainMode {
|
||||
FOLLOW_TERRAIN,
|
||||
FLAT_CUT_FILL
|
||||
}
|
||||
|
||||
data class LaneDefinition(
|
||||
val type: LaneType,
|
||||
val width: Double,
|
||||
val material: String = when (type) {
|
||||
LaneType.DRIVING -> "minecraft:gray_concrete"
|
||||
LaneType.PUBLIC_TRANSPORT -> "minecraft:yellow_concrete"
|
||||
LaneType.PEDESTRIAN -> "minecraft:stone_bricks"
|
||||
LaneType.SHOULDER -> "minecraft:gravel"
|
||||
LaneType.MEDIAN_BARRIER -> "minecraft:stone_brick_wall"
|
||||
LaneType.MEDIAN_GRASS -> "minecraft:grass_block"
|
||||
}
|
||||
)
|
||||
|
||||
data class RoadProfile(
|
||||
val lanesLeft: List<LaneDefinition>,
|
||||
val lanesRight: List<LaneDefinition>,
|
||||
val median: LaneDefinition?,
|
||||
val terrainMode: TerrainMode = TerrainMode.FLAT_CUT_FILL,
|
||||
val elevationOffset: Double = 0.0,
|
||||
val bankingEnabled: Boolean = true,
|
||||
val maxBankingAngle: Double = 15.0
|
||||
) {
|
||||
val totalWidth: Double
|
||||
get() {
|
||||
val leftWidth = lanesLeft.sumOf { it.width }
|
||||
val rightWidth = lanesRight.sumOf { it.width }
|
||||
val medianWidth = median?.width ?: 0.0
|
||||
return leftWidth + rightWidth + medianWidth
|
||||
}
|
||||
|
||||
fun getOffsetForLane(side: Side, laneIndex: Int): Double {
|
||||
val medianHalfWidth = (median?.width ?: 0.0) / 2.0
|
||||
|
||||
return when (side) {
|
||||
Side.LEFT -> {
|
||||
var offset = -medianHalfWidth
|
||||
for (i in 0 until laneIndex) {
|
||||
offset -= lanesLeft[i].width
|
||||
}
|
||||
offset - lanesLeft[laneIndex].width / 2.0
|
||||
}
|
||||
Side.RIGHT -> {
|
||||
var offset = medianHalfWidth
|
||||
for (i in 0 until laneIndex) {
|
||||
offset += lanesRight[i].width
|
||||
}
|
||||
offset + lanesRight[laneIndex].width / 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Side { LEFT, RIGHT }
|
||||
|
||||
companion object {
|
||||
fun singleLaneEachWay(laneWidth: Double = 3.5): RoadProfile {
|
||||
return RoadProfile(
|
||||
lanesLeft = listOf(LaneDefinition(LaneType.DRIVING, laneWidth)),
|
||||
lanesRight = listOf(LaneDefinition(LaneType.DRIVING, laneWidth)),
|
||||
median = null
|
||||
)
|
||||
}
|
||||
|
||||
fun highway(
|
||||
lanesPerSide: Int = 2,
|
||||
laneWidth: Double = 3.5,
|
||||
shoulderWidth: Double = 2.0,
|
||||
medianWidth: Double = 3.0,
|
||||
medianType: LaneType = LaneType.MEDIAN_BARRIER
|
||||
): RoadProfile {
|
||||
val sideLanes = buildList {
|
||||
add(LaneDefinition(LaneType.SHOULDER, shoulderWidth))
|
||||
repeat(lanesPerSide) {
|
||||
add(LaneDefinition(LaneType.DRIVING, laneWidth))
|
||||
}
|
||||
}
|
||||
|
||||
return RoadProfile(
|
||||
lanesLeft = sideLanes.reversed(),
|
||||
lanesRight = sideLanes,
|
||||
median = LaneDefinition(medianType, medianWidth)
|
||||
)
|
||||
}
|
||||
|
||||
fun withPublicTransport(
|
||||
drivingLanes: Int = 1,
|
||||
laneWidth: Double = 3.5,
|
||||
busLaneWidth: Double = 3.5,
|
||||
pedestrianWidth: Double = 2.0
|
||||
): RoadProfile {
|
||||
val leftLanes = buildList {
|
||||
add(LaneDefinition(LaneType.PEDESTRIAN, pedestrianWidth))
|
||||
add(LaneDefinition(LaneType.PUBLIC_TRANSPORT, busLaneWidth))
|
||||
repeat(drivingLanes) {
|
||||
add(LaneDefinition(LaneType.DRIVING, laneWidth))
|
||||
}
|
||||
}
|
||||
|
||||
val rightLanes = buildList {
|
||||
repeat(drivingLanes) {
|
||||
add(LaneDefinition(LaneType.DRIVING, laneWidth))
|
||||
}
|
||||
add(LaneDefinition(LaneType.PUBLIC_TRANSPORT, busLaneWidth))
|
||||
add(LaneDefinition(LaneType.PEDESTRIAN, pedestrianWidth))
|
||||
}
|
||||
|
||||
return RoadProfile(
|
||||
lanesLeft = leftLanes.reversed(),
|
||||
lanesRight = rightLanes,
|
||||
median = LaneDefinition(LaneType.MEDIAN_GRASS, 2.0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
149
src/main/kotlin/com/bnair/roadrunner/road/RoadSegment.kt
Normal file
149
src/main/kotlin/com/bnair/roadrunner/road/RoadSegment.kt
Normal file
@@ -0,0 +1,149 @@
|
||||
package com.bnair.roadrunner.road
|
||||
|
||||
import com.bnair.roadrunner.math.BezierCurve
|
||||
import com.bnair.roadrunner.math.Vec3d
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
data class RoadSegment(
|
||||
val curve: BezierCurve,
|
||||
val profile: RoadProfile,
|
||||
val id: String = java.util.UUID.randomUUID().toString()
|
||||
) {
|
||||
fun generateBlocks(samplesPerBlock: Double = 0.5): List<RoadBlock> {
|
||||
val blocks = mutableListOf<RoadBlock>()
|
||||
val arcLen = curve.arcLength()
|
||||
val numSamples = (arcLen / samplesPerBlock).toInt().coerceAtLeast(10)
|
||||
|
||||
for (i in 0..numSamples) {
|
||||
val t = i.toDouble() / numSamples
|
||||
val centerPoint = curve.evaluate(t)
|
||||
val direction = curve.direction(t)
|
||||
val perpendicular = direction.perpendicularXZ()
|
||||
|
||||
val curvature = curve.curvature(t)
|
||||
val bankingAngle = if (profile.bankingEnabled) {
|
||||
calculateBankingAngle(curvature, profile.maxBankingAngle)
|
||||
} else 0.0
|
||||
|
||||
profile.median?.let { median ->
|
||||
val medianBlocks = generateLaneBlocks(
|
||||
centerPoint,
|
||||
perpendicular,
|
||||
0.0,
|
||||
median,
|
||||
bankingAngle
|
||||
)
|
||||
blocks.addAll(medianBlocks)
|
||||
}
|
||||
|
||||
profile.lanesLeft.forEachIndexed { index, lane ->
|
||||
val offset = profile.getOffsetForLane(RoadProfile.Side.LEFT, index)
|
||||
val laneBlocks = generateLaneBlocks(
|
||||
centerPoint,
|
||||
perpendicular,
|
||||
offset,
|
||||
lane,
|
||||
bankingAngle
|
||||
)
|
||||
blocks.addAll(laneBlocks)
|
||||
}
|
||||
|
||||
profile.lanesRight.forEachIndexed { index, lane ->
|
||||
val offset = profile.getOffsetForLane(RoadProfile.Side.RIGHT, index)
|
||||
val laneBlocks = generateLaneBlocks(
|
||||
centerPoint,
|
||||
perpendicular,
|
||||
offset,
|
||||
lane,
|
||||
bankingAngle
|
||||
)
|
||||
blocks.addAll(laneBlocks)
|
||||
}
|
||||
}
|
||||
|
||||
return blocks.distinctBy { Triple(it.x, it.y, it.z) }
|
||||
}
|
||||
|
||||
private fun generateLaneBlocks(
|
||||
centerPoint: Vec3d,
|
||||
perpendicular: Vec3d,
|
||||
offset: Double,
|
||||
lane: LaneDefinition,
|
||||
bankingAngle: Double
|
||||
): List<RoadBlock> {
|
||||
val blocks = mutableListOf<RoadBlock>()
|
||||
val halfWidth = lane.width / 2.0
|
||||
|
||||
val bankingOffset = if (bankingAngle != 0.0) {
|
||||
abs(offset) * sin(Math.toRadians(bankingAngle))
|
||||
} else 0.0
|
||||
|
||||
val widthSteps = (lane.width).toInt().coerceAtLeast(1)
|
||||
for (w in 0 until widthSteps) {
|
||||
val widthOffset = -halfWidth + (w + 0.5) * (lane.width / widthSteps)
|
||||
val totalOffset = offset + widthOffset
|
||||
|
||||
val blockPos = centerPoint + perpendicular * totalOffset
|
||||
val adjustedY = blockPos.y + profile.elevationOffset + bankingOffset
|
||||
|
||||
val (bx, by, bz) = Vec3d(blockPos.x, adjustedY, blockPos.z).toBlockPos()
|
||||
|
||||
blocks.add(
|
||||
RoadBlock(
|
||||
x = bx,
|
||||
y = by,
|
||||
z = bz,
|
||||
material = lane.material,
|
||||
laneType = lane.type
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
private fun calculateBankingAngle(curvature: Double, maxAngle: Double): Double {
|
||||
val normalizedCurvature = (curvature * 100).coerceIn(0.0, 1.0)
|
||||
return normalizedCurvature * maxAngle
|
||||
}
|
||||
}
|
||||
|
||||
data class RoadBlock(
|
||||
val x: Int,
|
||||
val y: Int,
|
||||
val z: Int,
|
||||
val material: String,
|
||||
val laneType: LaneType
|
||||
)
|
||||
|
||||
data class RoadNetwork(
|
||||
val segments: MutableList<RoadSegment> = mutableListOf()
|
||||
) {
|
||||
fun addSegment(segment: RoadSegment) {
|
||||
segments.add(segment)
|
||||
}
|
||||
|
||||
fun removeSegment(id: String) {
|
||||
segments.removeIf { it.id == id }
|
||||
}
|
||||
|
||||
fun findSegmentAt(pos: Vec3d, tolerance: Double = 5.0): RoadSegment? {
|
||||
return segments.find { segment ->
|
||||
val samples = segment.curve.sample(20)
|
||||
samples.any { (it - pos).length() < tolerance }
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllBlocks(): List<RoadBlock> {
|
||||
return segments.flatMap { it.generateBlocks() }
|
||||
}
|
||||
|
||||
fun estimateMaterialCost(): Map<String, Int> {
|
||||
return getAllBlocks()
|
||||
.groupBy { it.material }
|
||||
.mapValues { it.value.size }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
modLoader = "javafml"
|
||||
loaderVersion = "*"
|
||||
issueTrackerURL = "https://github.com/ThePandaOliver/Multiloader-Template/issues"
|
||||
issueTrackerURL = "https://gitea.thefetagroup.com/bnair/RoadBuildMC/issues"
|
||||
license = "${mod_license}"
|
||||
|
||||
[[mods]]
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
"description": "${mod_description}",
|
||||
"authors": [${fabric_mod_authors}],
|
||||
"contact": {
|
||||
"issues": "https://github.com/ThePandaOliver/Multiloader-Template/issues",
|
||||
"sources": "https://github.com/ThePandaOliver/Multiloader-Template"
|
||||
"issues": "https://gitea.thefetagroup.com/bnair/RoadBuildMC/issues",
|
||||
"sources": "https://gitea.thefetagroup.com/bnair/RoadBuildMC"
|
||||
},
|
||||
"license": "${mod_license}",
|
||||
"icon": "assets/${mod_id}/icon.png",
|
||||
"environment": "*",
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"com.example.template.TemplateModFabric"
|
||||
"com.bnair.roadrunner.RoadRunnerFabric"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"required": true,
|
||||
"package": "com.example.template.mixin",
|
||||
"package": "com.bnair.roadrunner.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user