1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-07-12 08:47:45 +00:00

Begin adding zone collision visualization

If you run kawari-navimesh and give it a zone ID, it can now show
you the loaded collision meshes. I only tested it in inn rooms so
far, but already works super well.
This commit is contained in:
Joshua Goins 2025-07-09 14:59:02 -04:00
parent f5f166c78f
commit 85cec9f092
3 changed files with 3453 additions and 22 deletions

3339
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -34,6 +34,10 @@ required-features = ["oodle"]
[[bin]]
name = "kawari-launcher"
[[bin]]
name = "kawari-navimesh"
required-features = ["visualizer"]
[profile.release]
lto = true
strip = true
@ -42,9 +46,15 @@ codegen-units = 1
panic = "abort"
[features]
# Default featureset
default = []
# Oodle compression
oodle = []
# Navmesh visualizer
visualizer = ["dep:bevy"]
[build-dependencies]
# Serialization of IPC opcodes
serde = { version = "1.0", features = ["derive"], default-features = false }
@ -74,6 +84,19 @@ bitflags = { version = "2.9", default-features = false }
# excel sheet data
icarus = { git = "https://github.com/redstrate/Icarus", branch = "ver/2025.06.28.0000.0000", features = ["Warp", "Tribe", "ClassJob", "World", "TerritoryType", "Race", "Aetheryte", "EquipSlotCategory", "Action", "WeatherRate", "PlaceName", "GilShopItem"], default-features = false }
# navimesh visualization
bevy = { version = "0.16", features = ["std",
"bevy_asset",
"bevy_color",
"bevy_pbr",
"bevy_render",
"bevy_scene",
"bevy_state",
"bevy_window",
"bevy_winit",
"tonemapping_luts",
"x11"], default-features = false, optional = true }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
# Used for the web servers
axum = { version = "0.8", features = ["json", "tokio", "http1", "form", "query", "multipart"], default-features = false }

View file

@ -1,20 +1,102 @@
use bevy::{
asset::RenderAssetUsages,
prelude::*,
render::mesh::{Indices, PrimitiveTopology},
};
use icarus::TerritoryType::TerritoryTypeSheet;
use kawari::config::get_config;
use physis::{
common::{Language, Platform},
layer::{LayerEntryData, LayerGroup},
layer::{LayerEntryData, LayerGroup, ModelCollisionType, Transformation},
lvb::Lvb,
pcb::{Pcb, ResourceNode},
resource::{Resource, SqPackResource},
};
#[derive(Resource)]
struct ZoneToLoad(u16);
fn main() {
tracing_subscriber::fmt::init();
let config = get_config();
let args: Vec<String> = std::env::args().collect();
let zone_id: u16 = args[1].parse().unwrap();
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.insert_resource(ZoneToLoad(zone_id))
.run();
}
/// Walk each node, add it's collision model to the scene.
fn walk_node(
node: &ResourceNode,
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
transform: &Transformation,
) {
if !node.vertices.is_empty() {
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD,
);
let mut positions = Vec::new();
for vec in &node.vertices {
positions.push(vec.clone());
}
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
let mut indices = Vec::new();
for polygon in &node.polygons {
let mut vec: Vec<u32> = Vec::from(&polygon.vertex_indices)
.iter()
.map(|x| *x as u32)
.collect();
assert!(vec.len() == 3);
indices.append(&mut vec);
}
mesh.insert_indices(Indices::U32(indices));
commands.spawn((
Mesh3d(meshes.add(mesh)),
MeshMaterial3d(materials.add(Color::srgb(
fastrand::f32(),
fastrand::f32(),
fastrand::f32(),
))),
Transform {
translation: Vec3::from_array(transform.translation),
rotation: Quat::from_euler(
EulerRot::XYZ,
transform.rotation[0],
transform.rotation[1],
transform.rotation[2],
),
scale: Vec3::from_array(transform.scale),
},
));
}
for child in &node.children {
walk_node(&child, commands, meshes, materials, transform);
}
}
/// Setup 3D scene.
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
zone_id: Res<ZoneToLoad>,
) {
let zone_id = zone_id.0;
let config = get_config();
tracing::info!("Generating navmesh for zone {zone_id}!");
let mut sqpack_resource =
@ -34,7 +116,7 @@ fn main() {
for path in &lvb.scns[0].header.path_layer_group_resources {
if path.contains("bg.lgb") {
tracing::info!("Processing {path}");
tracing::info!("Processing {path}...");
let lgb_file = sqpack_resource.read(path).unwrap();
let lgb = LayerGroup::from_existing(&lgb_file);
@ -52,8 +134,23 @@ fn main() {
if let LayerEntryData::BG(bg) = &object.data {
if !bg.collision_asset_path.value.is_empty() {
tracing::info!("Considering {} for navimesh", object.instance_id);
tracing::info!("- Loading {}", bg.collision_asset_path.value);
// NOTE: assert is here to find out the unknown
assert!(bg.collision_type == ModelCollisionType::Replace);
let pcb_file = sqpack_resource
.read(&bg.collision_asset_path.value)
.unwrap();
let pcb = Pcb::from_existing(&pcb_file).unwrap();
walk_node(
&pcb.root_node,
&mut commands,
&mut meshes,
&mut materials,
&object.transform,
);
}
}
}
@ -61,4 +158,10 @@ fn main() {
}
}
}
// camera
commands.spawn((
Camera3d::default(),
Transform::from_xyz(15.0, 15.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}