Skip to content

Commit 0734eba

Browse files
committed
feat!: migrate roadnetwork extension to pathetic library
The Hydrazine pathfinding engine had accumulated critical limitations. Being unmaintained, it couldn't support Minecraft's expanded world height limits from 1.17/1.18, restricting where pathfinding could operate. Entity capabilities were limited, and extending behavior required modifying core classes rather than composing new functionality. On top of this, many blocks weren't handled correctly, making it impossible to find paths in numerous scenarios. Another problem was the lack of debugging visibility. When paths failed, there was no way to understand why, making troubleshooting frustrating and time-consuming as the road network system grew more sophisticated. Pathetic's processor-based architecture solves these issues with fine-grained control through composable validators and cost functions. This allows us to model entity constraints while maintaining clean separation of concerns. The new debug visualization records pathfinding decisions step-by-step, making it much easier to understand why paths fail. Performance improvements to existing systems come from block property caching, physics result reuse, and smarter per-node edge recalculation strategies.
1 parent 45bb865 commit 0734eba

File tree

41 files changed

+6887
-2251
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+6887
-2251
lines changed

code_generator/entitylist.json

Lines changed: 1322 additions & 1308 deletions
Large diffs are not rendered by default.

code_generator/src/bin/entity_type_properties.rs

Lines changed: 136 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct Entity {
1818
#[derive(Debug, Deserialize, Serialize)]
1919
#[serde(untagged)]
2020
enum EntityDimension {
21+
NonExistant(String),
2122
Simple(f64),
2223
Complex(HashMap<String, f64>),
2324
}
@@ -43,49 +44,6 @@ fn format_property(property: &str, value: &str) -> String {
4344
}
4445
}
4546

46-
fn generate_kotlin_code<F>(entities: &[Entity], property_accessor: F) -> Vec<String>
47-
where
48-
F: Fn(&Entity) -> &EntityDimension,
49-
{
50-
entities
51-
.iter()
52-
.flat_map(|entity| generate_entity_code(entity, &property_accessor))
53-
.collect()
54-
}
55-
56-
fn generate_entity_code<F>(entity: &Entity, property_accessor: F) -> Vec<String>
57-
where
58-
F: Fn(&Entity) -> &EntityDimension,
59-
{
60-
let entity_type = format!("EntityTypes.{}", entity.id.to_uppercase());
61-
62-
match property_accessor(entity) {
63-
EntityDimension::Simple(value) => {
64-
vec![format!(
65-
"EntityDataMatcher({}) -> {}",
66-
entity_type,
67-
format_value(*value)
68-
)]
69-
}
70-
EntityDimension::Complex(map) => map
71-
.iter()
72-
.flat_map(|(key, value)| {
73-
key.split("<br>")
74-
.map(|condition_set| {
75-
let conditions = parse_conditions(condition_set);
76-
let matcher = format!(
77-
"EntityDataMatcher({}, {})",
78-
entity_type,
79-
conditions.join(", ")
80-
);
81-
format!("{} -> {}", matcher, format_value(*value))
82-
})
83-
.collect::<Vec<String>>()
84-
})
85-
.collect(),
86-
}
87-
}
88-
8947
fn parse_conditions(condition_set: &str) -> Vec<String> {
9048
condition_set
9149
.split(", ")
@@ -100,39 +58,150 @@ fn parse_conditions(condition_set: &str) -> Vec<String> {
10058
.collect()
10159
}
10260

103-
fn write_kotlin_file(file_name: &str, content: &[String]) -> std::io::Result<()> {
104-
let mut file = File::create(file_name)?;
105-
writeln!(
106-
file,
107-
"//<editor-fold desc=\"Entity {} by properties\">",
108-
file_name.split('.').next().unwrap()
109-
)?;
110-
for line in content {
111-
writeln!(file, " {}", line)?;
61+
fn should_use_default_eye_height(eye_height: Option<f64>, height: f64) -> bool {
62+
let Some(eye_height) = eye_height else {
63+
return true
64+
};
65+
66+
const TOLERANCE: f64 = 0.001;
67+
(eye_height - height * 0.85).abs() < TOLERANCE
68+
}
69+
70+
fn extract_dimension_map(dimension: &EntityDimension) -> HashMap<String, f64> {
71+
match dimension {
72+
EntityDimension::NonExistant(_) => HashMap::new(),
73+
EntityDimension::Simple(val) => {
74+
let mut map = HashMap::new();
75+
map.insert(String::new(), *val);
76+
map
77+
}
78+
EntityDimension::Complex(map) => map.clone(),
11279
}
113-
writeln!(file, "//</editor-fold>")?;
114-
Ok(())
11580
}
11681

117-
fn generate_property(
118-
entities: &[Entity],
119-
property_name: &str,
120-
accessor: impl Fn(&Entity) -> &EntityDimension,
121-
) -> String {
122-
let kotlin_code = generate_kotlin_code(&entities, accessor);
123-
let file_name = format!("Entity{}Property.kt", property_name);
124-
write_kotlin_file(&file_name, &kotlin_code).unwrap();
125-
println!("{} generated successfully!", file_name);
126-
file_name
82+
fn find_matching_value(map: &HashMap<String, f64>, target_condition: &str) -> Option<f64> {
83+
if let Some(value) = map.get(target_condition) {
84+
return Some(*value);
85+
}
86+
87+
for (key, value) in map {
88+
if conditions_match(key, target_condition) {
89+
return Some(*value);
90+
}
91+
}
92+
93+
if target_condition.is_empty() {
94+
map.get("")
95+
.copied()
96+
.or_else(|| map.values().next().copied())
97+
} else {
98+
None
99+
}
100+
}
101+
102+
fn conditions_match(key: &str, target: &str) -> bool {
103+
if key.contains(target) || target.contains(key) {
104+
return true;
105+
}
106+
107+
if !target.is_empty() && !key.is_empty() {
108+
let target_parts: std::collections::HashSet<_> = target.split(", ").collect();
109+
let key_parts: std::collections::HashSet<_> = key.split(", ").collect();
110+
target_parts.intersection(&key_parts).count() > 0
111+
} else {
112+
false
113+
}
114+
}
115+
116+
fn collect_all_conditions(
117+
width_map: &HashMap<String, f64>,
118+
height_map: &HashMap<String, f64>,
119+
eye_height_map: &HashMap<String, f64>,
120+
) -> std::collections::HashSet<String> {
121+
let mut all_conditions = std::collections::HashSet::new();
122+
123+
for key in width_map
124+
.keys()
125+
.chain(height_map.keys())
126+
.chain(eye_height_map.keys())
127+
{
128+
for condition_set in key.split("<br>") {
129+
all_conditions.insert(condition_set.to_string());
130+
}
131+
}
132+
133+
all_conditions
134+
}
135+
136+
fn generate_entity_kotlin_entries(entity: &Entity) -> Vec<String> {
137+
let entity_type = format!("EntityTypes.{}", entity.id.to_uppercase());
138+
139+
let width_map = extract_dimension_map(&entity.width);
140+
let height_map = extract_dimension_map(&entity.height);
141+
let eye_height_map = extract_dimension_map(&entity.eye_height);
142+
143+
let all_conditions = collect_all_conditions(&width_map, &height_map, &eye_height_map);
144+
145+
all_conditions
146+
.iter()
147+
.filter_map(|condition_set| {
148+
let width = find_matching_value(&width_map, condition_set)?;
149+
let height = find_matching_value(&height_map, condition_set)?;
150+
let eye_height = find_matching_value(&eye_height_map, condition_set);
151+
152+
let eye_height_str = if should_use_default_eye_height(eye_height, height) {
153+
String::new()
154+
} else {
155+
format!(", eyeHeight = {}", format_value(eye_height.expect("Should have used default value of eye height")))
156+
};
157+
158+
let matcher = if condition_set.is_empty() {
159+
format!("EntityDataMatcher({})", entity_type)
160+
} else {
161+
let conditions = parse_conditions(condition_set);
162+
format!(
163+
"EntityDataMatcher({}, {})",
164+
entity_type,
165+
conditions.join(", ")
166+
)
167+
};
168+
169+
Some(format!(
170+
"{} to EntityData(width = {}, height = {}{})",
171+
matcher,
172+
format_value(width),
173+
format_value(height),
174+
eye_height_str
175+
))
176+
})
177+
.collect()
178+
}
179+
180+
fn generate_kotlin_entries(entities: &[Entity]) -> Vec<String> {
181+
entities
182+
.iter()
183+
.flat_map(generate_entity_kotlin_entries)
184+
.collect()
185+
}
186+
187+
fn write_entity_data_entries(entries: &[String]) -> std::io::Result<()> {
188+
let mut file = File::create("EntityTypeProperty_generated.kt")?;
189+
writeln!(file, "//<editor-fold desc=\"Entity Data Map Entries\">")?;
190+
for entry in entries {
191+
writeln!(file, " {},", entry)?;
192+
}
193+
writeln!(file, "//</editor-fold>")?;
194+
Ok(())
127195
}
128196

129197
fn main() -> Result<(), Box<dyn std::error::Error>> {
130198
let json_str = std::fs::read_to_string("entitylist.json")?;
131199
let entities = parse_json(&json_str)?;
132200

133-
generate_property(&entities, "EyeHeight", |e: &Entity| &e.eye_height);
134-
generate_property(&entities, "Height", |e: &Entity| &e.height);
135-
generate_property(&entities, "Width", |e: &Entity| &e.width);
201+
let kotlin_entries = generate_kotlin_entries(&entities);
202+
write_entity_data_entries(&kotlin_entries)?;
203+
204+
println!("Generated {} entity data entries", kotlin_entries.len());
136205

137206
Ok(())
138207
}

engine/engine-core/src/main/kotlin/com/typewritermc/core/utils/Extensions.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.typewritermc.core.utils
22

3+
import kotlinx.coroutines.CancellationException
34
import java.util.Locale.getDefault
45

56
fun String.replaceAll(vararg pairs: Pair<String, String>): String {
@@ -30,7 +31,7 @@ fun tryCatch(error: (Exception) -> Unit = { it.printStackTrace() }, block: () ->
3031
suspend fun tryCatchSuspend(error: suspend (Exception) -> Unit = { it.printStackTrace() }, block: suspend () -> Unit) {
3132
try {
3233
block()
33-
} catch (e: Throwable) {
34+
} catch (_: CancellationException) {} catch (e: Throwable) {
3435
error(e)
3536
}
3637
}

engine/engine-core/src/main/kotlin/com/typewritermc/core/utils/point/Point.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,22 @@ interface Point<P : Point<P>> {
168168
return (this.x - x).squared() + (this.y - y).squared() + (this.z - z).squared()
169169
}
170170

171+
/**
172+
* Computes the squared distance with weighted Y-axis contribution.
173+
*
174+
* Use this when vertical distance should have different importance than horizontal distance.
175+
*
176+
* @param x the X coordinate
177+
* @param y the Y coordinate
178+
* @param z the Z coordinate
179+
* @param yWeight weight multiplier for Y-axis distance (default 0.5, range typically 0.0-1.0)
180+
* @return the weighted squared distance
181+
*/
182+
@Contract(pure = true)
183+
fun distanceSquaredWeightedY(x: Double, y: Double, z: Double, yWeight: Double = 0.5): Double {
184+
return (this.x - x).squared() + ((this.y - y).squared() * yWeight) + (this.z - z).squared()
185+
}
186+
171187
/**
172188
* Gets the squared distance between this point and another.
173189
*
@@ -179,6 +195,21 @@ interface Point<P : Point<P>> {
179195
return distanceSquared(point.x, point.y, point.z)
180196
}
181197

198+
/**
199+
* Computes the squared distance to another point with weighted Y-axis contribution.
200+
*
201+
* Use this when vertical distance should have different importance than horizontal distance.
202+
* See [distanceSquaredWeightedY] for details.
203+
*
204+
* @param point the other point
205+
* @param yWeight weight multiplier for Y-axis distance (default 0.5)
206+
* @return the weighted squared distance
207+
*/
208+
@Contract(pure = true)
209+
fun distanceSquaredWeightedY(point: Point<*>, yWeight: Double = 0.5): Double {
210+
return distanceSquaredWeightedY(point.x, point.y, point.z, yWeight)
211+
}
212+
182213
@Contract(pure = true)
183214
fun distance(x: Double, y: Double, z: Double): Double {
184215
return sqrt(distanceSquared(x, y, z))

engine/engine-core/src/main/kotlin/com/typewritermc/core/utils/point/Position.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,21 @@ fun <WP1, WP2> WP1.distanceSqrt(point: WP2): Double?
9191
return distanceSquared(point)
9292
}
9393

94+
/**
95+
* Computes the weighted Y-distance between two world positions.
96+
*
97+
* Use this when vertical distance should have less impact on calculations.
98+
*
99+
* @param point the position to measure distance to
100+
* @param yWeight weight applied to Y-axis distance (default 0.5)
101+
* @return squared weighted distance, or null if positions are in different worlds
102+
*/
103+
fun <WP1, WP2> WP1.distanceSquaredWeightedY(point: WP2, yWeight: Double = 0.5): Double?
104+
where WP1 : Point<WP1>, WP1 : WorldHolder<WP1>, WP2 : Point<WP2>, WP2 : WorldHolder<WP2> {
105+
if (this.world != point.world) return null
106+
return distanceSquaredWeightedY(point, yWeight)
107+
}
108+
94109
fun Position.toBlockPosition(): Position {
95110
return Position(world, blockX.toDouble(), blockY.toDouble(), blockZ.toDouble(), 0f, 0f)
96111
}

engine/engine-core/src/main/kotlin/com/typewritermc/core/utils/point/Vector.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ data class Vector(
6060
return Vector(this.x / x, this.y / y, this.z / z)
6161
}
6262

63+
/**
64+
* Computes the dot product between this vector and another vector.
65+
*
66+
* Use this to calculate projections, determine if vectors are perpendicular (result = 0),
67+
* or measure similarity in direction (positive = same direction, negative = opposite).
68+
*
69+
* @param other the vector to compute the dot product with
70+
* @return the dot product value
71+
*/
72+
fun dot(other: Vector): Double {
73+
return x * other.x + y * other.y + z * other.z
74+
}
75+
6376
fun normalize(): Vector {
6477
val length = length
6578
return if (length < EPSILON) {

engine/engine-paper/src/main/kotlin/com/typewritermc/engine/paper/entry/entity/FakeEntity.kt

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,47 @@ open class EntityState(
8282
override fun toString(): String {
8383
return "EntityState(eyeHeight=$eyeHeight, speed=$speed)"
8484
}
85-
}
85+
}
86+
87+
/**
88+
* Encapsulates the movement and interaction capabilities of an entity for which the pathfinding is being performed.
89+
*
90+
* @param height The entity's height in blocks, used for vertical clearance checks.
91+
* @param width The entity's width in blocks, used for horizontal clearance checks.
92+
* @param maxStepHeight The maximum height in blocks the entity can step up without jumping.
93+
* @param maxJumpHeight The maximum height in blocks the entity can jump. This is measured
94+
* from the surface of the starting block to the surface of the destination block.
95+
* @param maxFallDistance The maximum vertical distance in blocks the entity will pathfind
96+
* downwards in a single move.
97+
* @param jumpStrength The entity jump strength, used for jump velocity calculation
98+
* @param canJump Determines whether the entity can perform jumps.
99+
* @param canInteract Determines whether the entity can open interactable blocks like wooden
100+
* doors, fence gates, and trapdoors.
101+
* @param canSwim Determines whether the entity can move through liquids.
102+
* @param canClimb Determines whether the entity can climb objects like ladders and vines.
103+
*/
104+
data class EntityPathingCapabilities(
105+
val width: Double = 0.6,
106+
val height: Double = 1.8,
107+
val maxStepHeight: Double = 0.6,
108+
val maxJumpHeight: Double = 1.25,
109+
val maxFallDistance: Double = 2.1,
110+
val jumpStrength: Double = 0.42,
111+
val canJump: Boolean = true,
112+
val canInteract: Boolean = false,
113+
val canSwim: Boolean = false,
114+
val canClimb: Boolean = false
115+
) {
116+
companion object {
117+
val DEFAULT = EntityPathingCapabilities()
118+
}
119+
120+
init {
121+
require(width > 0) { "Entity width must be positive, but was $width" }
122+
require(height > 0) { "Entity height must be positive, but was $height" }
123+
require(maxStepHeight >= 0) { "Max step height must be non-negative, but was $maxStepHeight" }
124+
require(maxJumpHeight >= 0) { "Max jump height must be non-negative, but was $maxJumpHeight" }
125+
require(maxFallDistance >= 0) { "Max fall distance must be non-negative, but was $maxFallDistance" }
126+
require(jumpStrength >= 0) { "Entity jump strength must be non-negative, but was $jumpStrength" }
127+
}
128+
}

0 commit comments

Comments
 (0)