mirror of
https://codeberg.org/icewind/vmdl.git
synced 2026-06-03 16:44:11 +02:00
finaly got orientation figured out I think
This commit is contained in:
parent
06cbe395eb
commit
2dffd8d7cd
9 changed files with 171 additions and 75 deletions
|
|
@ -1,11 +1,11 @@
|
|||
use cgmath::Euler;
|
||||
use cgmath::{Euler, Matrix4};
|
||||
use std::env::args;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use vmdl::mdl::Mdl;
|
||||
use vmdl::vtx::Vtx;
|
||||
use vmdl::vvd::Vvd;
|
||||
use vmdl::Model;
|
||||
use vmdl::{Model, Quaternion};
|
||||
|
||||
fn main() -> Result<(), vmdl::ModelError> {
|
||||
let mut args = args();
|
||||
|
|
@ -19,25 +19,33 @@ fn main() -> Result<(), vmdl::ModelError> {
|
|||
let data = fs::read(path.with_extension("vvd"))?;
|
||||
let vvd = Vvd::read(&data)?;
|
||||
|
||||
// dbg!(&mdl.header2);
|
||||
// dbg!(&mdl.header);
|
||||
|
||||
// for bone in &mdl.bones {
|
||||
// println!(
|
||||
// "{}: from {} at\n\t{:?}\n\t{:?}\n\t{:?}",
|
||||
// bone.name, bone.parent, bone.rot, bone.q_alignment, bone.pose_to_bone
|
||||
// "{}: from {} at\n\t{:?}\n\t{:?}\n\t{:?}\n\t{:?}",
|
||||
// bone.name, bone.parent, bone.rot, bone.rot_scale, bone.quaternion, bone.pose_to_bone
|
||||
// );
|
||||
// }
|
||||
dbg!(&mdl.local_animations[0]);
|
||||
// dbg!(&mdl.bones[0]);
|
||||
dbg!(&mdl.local_animations[0].animations[0]);
|
||||
let transform = mdl
|
||||
.local_animations
|
||||
.first()
|
||||
.get(0)
|
||||
.map(|a| a.animations[0].rotation(0))
|
||||
.unwrap();
|
||||
dbg!(transform);
|
||||
dbg!(Euler::from(cgmath::Quaternion::from(transform)));
|
||||
// dbg!(&mdl.body_table_by_name);
|
||||
|
||||
// dbg!(&mdl.attachments);
|
||||
let _model = Model::from_parts(mdl, vtx, vvd);
|
||||
let model = Model::from_parts(mdl, vtx, vvd);
|
||||
dbg!(model.animations().nth(1).unwrap().get_bone_transform(1, 0));
|
||||
// dbg!(model.root_transform());
|
||||
// dbg!(model.idle_transform());
|
||||
// dbg!(Euler::from(cgmath::Quaternion::from(
|
||||
// model.idle_transform()
|
||||
// )));
|
||||
// dbg!(Euler::from(Quaternion::from(model.root_transform())));
|
||||
// for strip in model.vertex_strips() {
|
||||
// for vertex in strip {
|
||||
|
|
|
|||
|
|
@ -250,9 +250,7 @@ fn model_to_model(model: &Model, loader: &Loader, skin: usize) -> CpuModel {
|
|||
|
||||
let skin = model.skin_tables().nth(skin).unwrap();
|
||||
|
||||
let transforms = model.root_transform();
|
||||
let transforms = dbg!(model.idle_transform());
|
||||
// let transforms = Matrix4::identity();
|
||||
let transforms = Matrix4::identity();
|
||||
|
||||
let geometries = model
|
||||
.meshes()
|
||||
|
|
|
|||
|
|
@ -43,11 +43,21 @@ pub struct Quaternion48 {
|
|||
z: u16,
|
||||
}
|
||||
|
||||
impl Default for Quaternion48 {
|
||||
fn default() -> Self {
|
||||
Quaternion48 {
|
||||
x: 32768,
|
||||
y: 32768,
|
||||
z: 16384,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadableRelative for Quaternion48 {}
|
||||
|
||||
fn calc_w(x: f32, y: f32, z: f32, w_neg: bool) -> f32 {
|
||||
let w_sign = if w_neg { -1.0 } else { 1.0 };
|
||||
f32::sqrt(1.0 - ((x * x) - (y * y) - (z * z))) * w_sign
|
||||
f32::sqrt(1.0 - (x * x) - (y * y) - (z * z)) * w_sign
|
||||
}
|
||||
|
||||
impl Quaternion48 {
|
||||
|
|
@ -92,14 +102,13 @@ pub struct Quaternion64(u64);
|
|||
impl ReadableRelative for Quaternion64 {}
|
||||
|
||||
impl Quaternion64 {
|
||||
const MASK_21_BIT: u64 = 0b111111111111111111111;
|
||||
const MASK_21_BIT: u64 = 0b11111_11111111_11111111;
|
||||
const W_NEG_MASK: u64 = 0x80_00_00_00_00_00_00_00;
|
||||
|
||||
fn val(&self, offset: i32) -> f32 {
|
||||
let raw = (self.0 >> offset) & Self::MASK_21_BIT;
|
||||
let raw = ((self.0) >> offset) & Self::MASK_21_BIT;
|
||||
(raw as f32 - 1048576.0) / 1048576.5
|
||||
}
|
||||
|
||||
pub fn x(&self) -> f32 {
|
||||
self.val(0)
|
||||
}
|
||||
|
|
@ -121,7 +130,7 @@ impl Quaternion64 {
|
|||
|
||||
impl From<Quaternion64> for Quaternion {
|
||||
fn from(value: Quaternion64) -> Self {
|
||||
let normalized = Vector4::new(value.x(), value.y(), value.z(), value.w()).normalize();
|
||||
let normalized = Vector4::new(value.x(), value.y(), value.z(), value.w());
|
||||
Quaternion {
|
||||
x: normalized.x,
|
||||
y: normalized.y,
|
||||
|
|
|
|||
56
src/lib.rs
56
src/lib.rs
|
|
@ -7,12 +7,12 @@ pub mod vtx;
|
|||
pub mod vvd;
|
||||
|
||||
pub use crate::mdl::Mdl;
|
||||
use crate::mdl::{Bone, ModelFlags, PoseParameterDescription, TextureInfo};
|
||||
use crate::mdl::{AnimationDescription, Bone, ModelFlags, PoseParameterDescription, TextureInfo};
|
||||
pub use crate::vtx::Vtx;
|
||||
use crate::vvd::Vertex;
|
||||
pub use crate::vvd::Vvd;
|
||||
use bytemuck::{pod_read_unaligned, Contiguous, Pod};
|
||||
use cgmath::{Matrix4, SquareMatrix};
|
||||
use cgmath::{Matrix4, SquareMatrix, Transform, Vector3};
|
||||
pub use error::*;
|
||||
pub use handle::Handle;
|
||||
use itertools::Either;
|
||||
|
|
@ -85,6 +85,10 @@ impl Model {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn animations(&self) -> impl Iterator<Item = &AnimationDescription> {
|
||||
self.mdl.local_animations.iter()
|
||||
}
|
||||
|
||||
pub fn meshes(&self) -> impl Iterator<Item = Mesh> {
|
||||
let mdl_meshes = self
|
||||
.mdl
|
||||
|
|
@ -140,9 +144,13 @@ impl Model {
|
|||
}
|
||||
|
||||
self.bones()
|
||||
.find(|bone| bone.name != "root")
|
||||
.map(|bone| bone.pose_to_bone)
|
||||
.map(Matrix4::from)
|
||||
.next()
|
||||
.map(|bone| {
|
||||
// let inv = Matrix4::from(bone.pose_to_bone)
|
||||
// .inverse_transform()
|
||||
// .unwrap();
|
||||
Matrix4::from(bone.rot)
|
||||
})
|
||||
.unwrap_or_else(Matrix4::identity)
|
||||
}
|
||||
|
||||
|
|
@ -153,8 +161,9 @@ impl Model {
|
|||
|
||||
self.mdl
|
||||
.local_animations
|
||||
.first()
|
||||
.and_then(|desc| desc.animations.first())
|
||||
.iter()
|
||||
.filter_map(|desc| desc.animations.iter().find(|animation| animation.bone == 0))
|
||||
.find(|anim| anim.rotation_looks_valid())
|
||||
.map(|animation| animation.rotation(0))
|
||||
.map(Matrix4::from)
|
||||
.unwrap_or_else(Matrix4::identity)
|
||||
|
|
@ -169,24 +178,23 @@ impl Model {
|
|||
}
|
||||
|
||||
pub fn vertex_to_world_space(&self, vertex: &Vertex) -> Vector {
|
||||
// vertex.position.transformed(self.root_transform())
|
||||
vertex.position.transformed(self.idle_transform())
|
||||
let transform = self.idle_transform() * self.root_transform();
|
||||
transform
|
||||
.transform_vector(Vector3::from(vertex.position))
|
||||
.into()
|
||||
}
|
||||
|
||||
// let mut pos = Vector3::from(vertex.position);
|
||||
// for weights in vertex.bone_weights.weights() {
|
||||
// if let Some(bone) = self.mdl.bones.get(weights.bone_id as usize) {
|
||||
// let transform = Quaternion::from(bone.rot);
|
||||
// if bone.parent == 0 {
|
||||
// if bone.name == "joint1" {
|
||||
// dbg!(&bone.name, bone.rot, transform);
|
||||
// }
|
||||
// let transform = Matrix4::from(transform);
|
||||
// pos = transform.transform_vector(pos);
|
||||
// // pos = bone.pose_to_bone.transform(pos);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// pos.into()
|
||||
fn bone_transform(
|
||||
&self,
|
||||
bone_id: u8,
|
||||
bone: &Bone,
|
||||
animation: &AnimationDescription,
|
||||
weight: f32,
|
||||
frame: usize,
|
||||
) -> Matrix4<f32> {
|
||||
let animation_transform = weight * animation.get_bone_transform(bone_id, frame);
|
||||
let bone_origin = Matrix4::from(bone.pose_to_bone);
|
||||
bone_origin.inverse_transform().unwrap() * animation_transform * bone_origin
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub struct Mdl {
|
|||
pub header2: Option<StudioHeader2>,
|
||||
pub bones: Vec<Bone>,
|
||||
pub bone_controllers: Vec<BoneController>,
|
||||
pub body_table_by_name: Vec<u8>,
|
||||
pub body_parts: Vec<BodyPart>,
|
||||
pub textures: Vec<TextureInfo>,
|
||||
pub texture_paths: Vec<String>,
|
||||
|
|
@ -60,6 +61,7 @@ impl Mdl {
|
|||
let skin_table = read_relative::<u16, _>(data, header.skin_reference_indexes())?;
|
||||
let bones = read_relative(data, header.bone_indexes())?;
|
||||
let bone_controllers = read_relative(data, header.bone_controller_indexes())?;
|
||||
let body_table_by_name = read_relative(data, header.bone_table_by_name_indexes())?;
|
||||
|
||||
let surface_prop = read_single(data, header.surface_prop_index)?;
|
||||
let key_values = (header.key_value_size > 0)
|
||||
|
|
@ -91,6 +93,7 @@ impl Mdl {
|
|||
name,
|
||||
bones,
|
||||
bone_controllers,
|
||||
body_table_by_name,
|
||||
body_parts: header
|
||||
.body_part_indexes()
|
||||
.map(|index| {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
};
|
||||
use bitflags::bitflags;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use cgmath::{Matrix4, SquareMatrix};
|
||||
use std::mem::size_of;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
|
||||
|
|
@ -84,10 +85,20 @@ static_assertions::const_assert_eq!(size_of::<AnimationDescriptionHeader>(), 100
|
|||
pub struct AnimationDescription {
|
||||
pub name: String,
|
||||
pub fps: f32,
|
||||
pub frame_count: i32,
|
||||
pub frame_count: usize,
|
||||
pub animations: Vec<Animation>,
|
||||
}
|
||||
|
||||
impl AnimationDescription {
|
||||
pub fn get_bone_transform(&self, bone: u8, frame: usize) -> Matrix4<f32> {
|
||||
let Some(animation) = self.animations.iter().find(|anim| anim.bone == bone) else {
|
||||
return Matrix4::identity();
|
||||
};
|
||||
Matrix4::from_translation(animation.position(frame).into())
|
||||
* Matrix4::from(animation.rotation(frame))
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadRelative for AnimationDescription {
|
||||
type Header = AnimationDescriptionHeader;
|
||||
|
||||
|
|
@ -110,7 +121,7 @@ impl ReadRelative for AnimationDescription {
|
|||
Ok(AnimationDescription {
|
||||
name: read_single(data, header.name_offset)?,
|
||||
fps: header.fps,
|
||||
frame_count: header.frame_count,
|
||||
frame_count: header.frame_count as usize,
|
||||
animations,
|
||||
})
|
||||
}
|
||||
|
|
@ -234,20 +245,43 @@ impl<'a> FrameValues<'a> {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RotationData {
|
||||
Quaternion48(Quaternion48),
|
||||
Quaternion64(Quaternion64),
|
||||
RotationValues(Vec<RadianEuler>),
|
||||
Quaternion48(Quaternion),
|
||||
Quaternion64(Quaternion),
|
||||
Animated(Vec<RadianEuler>),
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<Quaternion48> for RotationData {
|
||||
fn from(value: Quaternion48) -> Self {
|
||||
let q = Quaternion::from(value);
|
||||
RotationData::Quaternion48(q)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Quaternion64> for RotationData {
|
||||
fn from(value: Quaternion64) -> Self {
|
||||
let q = Quaternion::from(value);
|
||||
RotationData::Quaternion64(q)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<RadianEuler>> for RotationData {
|
||||
fn from(value: Vec<RadianEuler>) -> Self {
|
||||
// axis get fixed up when applying the scale
|
||||
RotationData::Animated(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl RotationData {
|
||||
pub fn rotation(&self, frame: usize) -> Quaternion {
|
||||
match self {
|
||||
RotationData::Quaternion48(q) => Quaternion::from(*q),
|
||||
RotationData::Quaternion64(q) => Quaternion::from(*q),
|
||||
RotationData::RotationValues(values) => {
|
||||
values.get(frame).copied().unwrap_or_default().into()
|
||||
}
|
||||
RotationData::Quaternion48(q) => *q,
|
||||
RotationData::Quaternion64(q) => *q,
|
||||
RotationData::Animated(values) => values
|
||||
.get(frame)
|
||||
.copied()
|
||||
.unwrap_or_else(|| values.last().copied().unwrap_or_default())
|
||||
.into(),
|
||||
RotationData::None => Quaternion::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -256,18 +290,19 @@ impl RotationData {
|
|||
match self {
|
||||
RotationData::Quaternion48(_) => size_of::<Quaternion48>(),
|
||||
RotationData::Quaternion64(_) => size_of::<Quaternion64>(),
|
||||
RotationData::RotationValues(_) => size_of::<AnimationValuePointer>(),
|
||||
RotationData::Animated(_) => size_of::<AnimationValuePointer>(),
|
||||
RotationData::None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_scale(&mut self, scale: Vector) {
|
||||
if let RotationData::RotationValues(values) = self {
|
||||
if let RotationData::Animated(values) = self {
|
||||
values.iter_mut().for_each(|value| {
|
||||
// scale and fixup the angles
|
||||
*value = RadianEuler {
|
||||
x: value.x * scale.x,
|
||||
y: value.y * scale.y,
|
||||
z: value.z * scale.z,
|
||||
y: value.x * scale.x,
|
||||
z: value.y * scale.y,
|
||||
x: value.z * scale.z,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -317,6 +352,10 @@ impl Animation {
|
|||
self.rotation_data.rotation(frame)
|
||||
}
|
||||
|
||||
pub(crate) fn rotation_looks_valid(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn position(&self, frame: usize) -> Vector {
|
||||
self.position_data.position(frame)
|
||||
}
|
||||
|
|
@ -343,17 +382,17 @@ fn read_animation(
|
|||
let offset = size_of::<AnimationHeader>();
|
||||
|
||||
let rotation_data = if header.flags.contains(AnimationFlags::STUDIO_ANIM_RAWROT) {
|
||||
RotationData::Quaternion48(read_single(data, offset)?)
|
||||
RotationData::from(read_single::<Quaternion48, _>(data, offset)?)
|
||||
} else if header.flags.contains(AnimationFlags::STUDIO_ANIM_RAWROT2) {
|
||||
RotationData::Quaternion64(read_single(data, offset)?)
|
||||
RotationData::from(read_single::<Quaternion64, _>(data, offset)?)
|
||||
} else if header.flags.contains(AnimationFlags::STUDIO_ANIM_ANIMROT) {
|
||||
let pointers: AnimationValuePointer = read_single(data, offset)?;
|
||||
let value_data = &data[offset..];
|
||||
let values = (0..frames)
|
||||
let values: Vec<RadianEuler> = (0..frames)
|
||||
.map(|frame| read_animation_values(value_data, frame, pointers))
|
||||
.map(|r| r.map(|[x, y, z]| RadianEuler { x, y, z }))
|
||||
.map(|r| r.map(|[x, y, z]| RadianEuler { x, z, y }))
|
||||
.collect::<Result<_, ModelError>>()?;
|
||||
RotationData::RotationValues(values)
|
||||
RotationData::from(values)
|
||||
} else {
|
||||
RotationData::None
|
||||
};
|
||||
|
|
@ -362,8 +401,8 @@ fn read_animation(
|
|||
let position_data = if header.flags.contains(AnimationFlags::STUDIO_ANIM_RAWPOS) {
|
||||
PositionData::Vector48(read_single(data, position_offset)?)
|
||||
} else if header.flags.contains(AnimationFlags::STUDIO_ANIM_ANIMPOS) {
|
||||
let pointers: AnimationValuePointer = read_single(data, offset)?;
|
||||
let value_data = &data[offset..];
|
||||
let pointers: AnimationValuePointer = read_single(data, position_offset)?;
|
||||
let value_data = &data[position_offset..];
|
||||
let values = (0..frames)
|
||||
.map(|frame| read_animation_values(value_data, frame, pointers))
|
||||
.map(|r| r.map(Vector::from))
|
||||
|
|
|
|||
|
|
@ -139,8 +139,7 @@ pub struct StudioHeader {
|
|||
|
||||
anim_block_model: i32, // Placeholder for mutable-void*
|
||||
|
||||
// Points to a series of bytes?
|
||||
bone_table_name_index: i32,
|
||||
bone_table_by_name_index: i32,
|
||||
|
||||
vertex_base: i32, // Placeholder for void*
|
||||
offset_base: i32, // Placeholder for void*
|
||||
|
|
@ -155,9 +154,9 @@ pub struct StudioHeader {
|
|||
num_allowed_root_lods: u8,
|
||||
|
||||
#[allow(dead_code)]
|
||||
unused0: u8, // ??
|
||||
unused0: u8,
|
||||
#[allow(dead_code)]
|
||||
unused1: i32, // ??
|
||||
unused1: i32,
|
||||
|
||||
flex_controller_ui_count: i32,
|
||||
flex_controller_ui_index: i32,
|
||||
|
|
@ -219,6 +218,14 @@ impl StudioHeader {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn bone_table_by_name_indexes(&self) -> impl Iterator<Item = usize> {
|
||||
index_range(
|
||||
self.bone_table_by_name_index,
|
||||
self.bone_count,
|
||||
size_of::<u8>(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn hitbox_set_indexes(&self) -> impl Iterator<Item = usize> {
|
||||
index_range(
|
||||
self.hitbox_set_offset,
|
||||
|
|
@ -235,10 +242,6 @@ impl StudioHeader {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn local_sequence_indexes(&self) -> impl Iterator<Item = usize> {
|
||||
index_range(self.local_seq_offset, self.local_seq_count, 1)
|
||||
}
|
||||
|
||||
pub fn texture_indexes(&self) -> impl Iterator<Item = usize> {
|
||||
index_range(
|
||||
self.texture_offset,
|
||||
|
|
|
|||
|
|
@ -130,10 +130,33 @@ impl From<cgmath::Quaternion<f32>> for Quaternion {
|
|||
|
||||
impl From<Quaternion> for cgmath::Matrix4<f32> {
|
||||
fn from(q: Quaternion) -> Self {
|
||||
// cgmath::Quaternion::from(Quaternion {
|
||||
// x: q.z,
|
||||
// y: -q.y,
|
||||
// z: q.x,
|
||||
// w: q.w,
|
||||
// })
|
||||
// .into()
|
||||
cgmath::Quaternion::from(q).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul for Quaternion {
|
||||
type Output = Quaternion;
|
||||
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
(cgmath::Quaternion::from(self) * cgmath::Quaternion::from(rhs)).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<RadianEuler> for Quaternion {
|
||||
type Output = Quaternion;
|
||||
|
||||
fn mul(self, rhs: RadianEuler) -> Self::Output {
|
||||
(cgmath::Quaternion::from(self) * cgmath::Quaternion::from(rhs)).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Zeroable, Pod, Default)]
|
||||
#[repr(C)]
|
||||
pub struct RadianEuler {
|
||||
|
|
@ -165,10 +188,9 @@ impl From<RadianEuler> for Euler<Deg<f32>> {
|
|||
impl From<RadianEuler> for cgmath::Quaternion<f32> {
|
||||
fn from(value: RadianEuler) -> Self {
|
||||
// angles are applied in roll, pitch, yaw order
|
||||
// additionally the access are remapped
|
||||
cgmath::Quaternion::from_angle_y(Rad(value.y))
|
||||
* cgmath::Quaternion::from_angle_x(Rad(-value.z))
|
||||
* cgmath::Quaternion::from_angle_z(Rad(value.x))
|
||||
* cgmath::Quaternion::from_angle_x(Rad(-value.x))
|
||||
* cgmath::Quaternion::from_angle_z(Rad(value.z))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +200,12 @@ impl From<RadianEuler> for Quaternion {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<RadianEuler> for Matrix4<f32> {
|
||||
fn from(value: RadianEuler) -> Self {
|
||||
cgmath::Quaternion::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed length, null-terminated string
|
||||
#[derive(Debug, Clone, Default, Copy)]
|
||||
pub struct FixedString<const LEN: usize>(ArrayString<LEN>);
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ pub struct BoneWeights {
|
|||
impl BoneWeights {
|
||||
pub fn weights(&self) -> impl Iterator<Item = BoneWeight> + '_ {
|
||||
(0..min(self.bone_count as usize, 3)).map(|i| BoneWeight {
|
||||
weight: self.weight[i],
|
||||
weight: self.weight[i] / self.bone_count as f32,
|
||||
bone_id: self.bone[i],
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue