Match free-flight camera to native projection
Docs Deploy / Build and Deploy MkDocs (push) Failing after 2m47s
Docs Deploy / Build and Deploy MkDocs (push) Failing after 2m47s
This commit is contained in:
2 files changed
+161
-21
No files matched your search
+140
-21
@@ -185,7 +185,7 @@ struct FreeFlightCamera {
|
|||||||
position: [f32; 3],
|
position: [f32; 3],
|
||||||
yaw: f32,
|
yaw: f32,
|
||||||
pitch: f32,
|
pitch: f32,
|
||||||
vertical_fov: f32,
|
horizontal_fov: f32,
|
||||||
near_plane: f32,
|
near_plane: f32,
|
||||||
far_plane: f32,
|
far_plane: f32,
|
||||||
move_speed: f32,
|
move_speed: f32,
|
||||||
@@ -240,9 +240,9 @@ impl FreeFlightCamera {
|
|||||||
position,
|
position,
|
||||||
yaw,
|
yaw,
|
||||||
pitch,
|
pitch,
|
||||||
vertical_fov: std::f32::consts::FRAC_PI_3,
|
horizontal_fov: 1.3,
|
||||||
near_plane: (radius * 0.01).clamp(0.05, 0.1),
|
near_plane: 0.5,
|
||||||
far_plane: (radius * 32.0).max(100.0),
|
far_plane: 700.0,
|
||||||
// Keep the default traversal speed comfortable on a full mission
|
// Keep the default traversal speed comfortable on a full mission
|
||||||
// map; Shift/Ctrl still provide the coarse speed controls.
|
// map; Shift/Ctrl still provide the coarse speed controls.
|
||||||
move_speed: (radius * 0.03).clamp(4.0, 40.0),
|
move_speed: (radius * 0.03).clamp(4.0, 40.0),
|
||||||
@@ -335,6 +335,15 @@ impl FreeFlightCamera {
|
|||||||
normalize3(cross3(self.right(), self.forward())).unwrap_or([0.0, 0.0, 1.0])
|
normalize3(cross3(self.right(), self.forward())).unwrap_or([0.0, 0.0, 1.0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn vertical_fov(self, aspect: f32) -> f32 {
|
||||||
|
let aspect = if aspect.is_finite() && aspect > f32::EPSILON {
|
||||||
|
aspect
|
||||||
|
} else {
|
||||||
|
16.0 / 9.0
|
||||||
|
};
|
||||||
|
2.0 * ((self.horizontal_fov * 0.5).tan() / aspect).atan()
|
||||||
|
}
|
||||||
|
|
||||||
fn look_delta(&mut self, dx: f64, dy: f64) {
|
fn look_delta(&mut self, dx: f64, dy: f64) {
|
||||||
const SENSITIVITY: f32 = 0.0025;
|
const SENSITIVITY: f32 = 0.0025;
|
||||||
let dx = dx as f32;
|
let dx = dx as f32;
|
||||||
@@ -415,7 +424,7 @@ impl FreeFlightCamera {
|
|||||||
-dot3(forward, self.position),
|
-dot3(forward, self.position),
|
||||||
1.0,
|
1.0,
|
||||||
];
|
];
|
||||||
let half_fov = (self.vertical_fov * 0.5).clamp(0.05, 1.5);
|
let half_fov = self.vertical_fov(aspect) * 0.5;
|
||||||
let focal = half_fov.tan().recip();
|
let focal = half_fov.tan().recip();
|
||||||
let near_plane = self.near_plane.max(0.001);
|
let near_plane = self.near_plane.max(0.001);
|
||||||
let far_plane = self.far_plane.max(near_plane + 0.001);
|
let far_plane = self.far_plane.max(near_plane + 0.001);
|
||||||
@@ -449,7 +458,7 @@ impl FreeFlightCamera {
|
|||||||
forward: self.forward(),
|
forward: self.forward(),
|
||||||
right: self.right(),
|
right: self.right(),
|
||||||
up: self.up(),
|
up: self.up(),
|
||||||
vertical_fov: self.vertical_fov,
|
vertical_fov: self.vertical_fov(aspect),
|
||||||
near_plane: self.near_plane,
|
near_plane: self.near_plane,
|
||||||
far_plane: self.far_plane,
|
far_plane: self.far_plane,
|
||||||
clip_from_world: camera.clip_from_world,
|
clip_from_world: camera.clip_from_world,
|
||||||
@@ -1934,7 +1943,7 @@ fn screen_gradient_quad_vertices(
|
|||||||
let width = viewport[0].max(1.0);
|
let width = viewport[0].max(1.0);
|
||||||
let height = viewport[1].max(1.0);
|
let height = viewport[1].max(1.0);
|
||||||
let aspect = width / height;
|
let aspect = width / height;
|
||||||
let fov = camera.vertical_fov.clamp(0.05, 3.0);
|
let fov = camera.vertical_fov;
|
||||||
// The quad is placed close to the camera and drawn with depth disabled.
|
// The quad is placed close to the camera and drawn with depth disabled.
|
||||||
// It therefore covers the complete projected viewport while the later
|
// It therefore covers the complete projected viewport while the later
|
||||||
// dome, terrain, and object ranges provide their own depth-tested detail.
|
// dome, terrain, and object ranges provide their own depth-tested detail.
|
||||||
@@ -2797,7 +2806,7 @@ fn horizontal_fov(camera: &PreviewCameraFrame, aspect: f32) -> f32 {
|
|||||||
} else {
|
} else {
|
||||||
16.0 / 9.0
|
16.0 / 9.0
|
||||||
};
|
};
|
||||||
2.0 * ((camera.vertical_fov.clamp(0.05, 3.0) * 0.5).tan() * aspect).atan()
|
2.0 * ((camera.vertical_fov * 0.5).tan() * aspect).atan()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tests the camera-to-sun segment against the validated mission terrain.
|
/// Tests the camera-to-sun segment against the validated mission terrain.
|
||||||
@@ -2993,11 +3002,11 @@ fn project_direction_pixels(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let depth = dot3(camera.forward(), direction);
|
let depth = dot3(camera.forward(), direction);
|
||||||
if !depth.is_finite() || depth <= camera.near_plane.max(0.001) {
|
if !depth.is_finite() || depth <= 1.0e-4 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let aspect = width / height;
|
let aspect = width / height;
|
||||||
let half_vertical = (camera.vertical_fov.clamp(0.05, 3.0) * 0.5).tan();
|
let half_vertical = (camera.vertical_fov * 0.5).tan();
|
||||||
let half_horizontal = half_vertical * aspect;
|
let half_horizontal = half_vertical * aspect;
|
||||||
let ndc_x = dot3(camera.right(), direction) / (depth * half_horizontal);
|
let ndc_x = dot3(camera.right(), direction) / (depth * half_horizontal);
|
||||||
let ndc_y = dot3(camera.up(), direction) / (depth * half_vertical);
|
let ndc_y = dot3(camera.up(), direction) / (depth * half_vertical);
|
||||||
@@ -3021,7 +3030,7 @@ fn screen_ray_from_pixels(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let aspect = viewport[0] / viewport[1];
|
let aspect = viewport[0] / viewport[1];
|
||||||
let half_vertical = (camera.vertical_fov.clamp(0.05, 3.0) * 0.5).tan();
|
let half_vertical = (camera.vertical_fov * 0.5).tan();
|
||||||
let half_horizontal = half_vertical * aspect;
|
let half_horizontal = half_vertical * aspect;
|
||||||
let ndc_x = pixels[0] / viewport[0] * 2.0 - 1.0;
|
let ndc_x = pixels[0] / viewport[0] * 2.0 - 1.0;
|
||||||
let ndc_y = 1.0 - pixels[1] / viewport[1] * 2.0;
|
let ndc_y = 1.0 - pixels[1] / viewport[1] * 2.0;
|
||||||
@@ -3053,7 +3062,7 @@ fn sprite_quad_vertices_pixels(
|
|||||||
let width = viewport[0].max(1.0);
|
let width = viewport[0].max(1.0);
|
||||||
let height = viewport[1].max(1.0);
|
let height = viewport[1].max(1.0);
|
||||||
let aspect = width / height;
|
let aspect = width / height;
|
||||||
let vertical_fov = camera.vertical_fov.clamp(0.05, 3.0);
|
let vertical_fov = camera.vertical_fov;
|
||||||
let horizontal_fov_radians = horizontal_fov(camera, aspect);
|
let horizontal_fov_radians = horizontal_fov(camera, aspect);
|
||||||
let world_per_pixel_x = 2.0 * depth * (horizontal_fov_radians * 0.5).tan() / width;
|
let world_per_pixel_x = 2.0 * depth * (horizontal_fov_radians * 0.5).tan() / width;
|
||||||
let world_per_pixel_y = 2.0 * depth * (vertical_fov * 0.5).tan() / height;
|
let world_per_pixel_y = 2.0 * depth * (vertical_fov * 0.5).tan() / height;
|
||||||
@@ -3113,7 +3122,7 @@ fn world_from_ndc(
|
|||||||
camera.near_plane.max(0.001)
|
camera.near_plane.max(0.001)
|
||||||
};
|
};
|
||||||
let aspect = viewport[0].max(1.0) / viewport[1].max(1.0);
|
let aspect = viewport[0].max(1.0) / viewport[1].max(1.0);
|
||||||
let half_vertical = (camera.vertical_fov.clamp(0.05, 3.0) * 0.5).tan();
|
let half_vertical = (camera.vertical_fov * 0.5).tan();
|
||||||
let half_horizontal = half_vertical * aspect;
|
let half_horizontal = half_vertical * aspect;
|
||||||
let offset = add3(
|
let offset = add3(
|
||||||
scale3(camera.right(), ndc[0] * half_horizontal),
|
scale3(camera.right(), ndc[0] * half_horizontal),
|
||||||
@@ -6429,7 +6438,7 @@ mod tests {
|
|||||||
position: [0.0, 0.0, 0.0],
|
position: [0.0, 0.0, 0.0],
|
||||||
yaw: 0.0,
|
yaw: 0.0,
|
||||||
pitch: 0.0,
|
pitch: 0.0,
|
||||||
vertical_fov: std::f32::consts::FRAC_PI_2,
|
horizontal_fov: std::f32::consts::FRAC_PI_2,
|
||||||
near_plane: 1.0,
|
near_plane: 1.0,
|
||||||
far_plane: 11.0,
|
far_plane: 11.0,
|
||||||
move_speed: 1.0,
|
move_speed: 1.0,
|
||||||
@@ -6453,6 +6462,116 @@ mod tests {
|
|||||||
nearly_equal(far[2] / far[3], 1.0);
|
nearly_equal(far[2] / far[3], 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn free_flight_camera_uses_native_horizontal_fov_for_every_viewport_aspect() {
|
||||||
|
let horizontal_fov = 1.3_f32;
|
||||||
|
let camera = FreeFlightCamera::from_bounds([0.0; 3], [100.0; 3])
|
||||||
|
.expect("finite free-flight bounds");
|
||||||
|
assert_eq!(camera.horizontal_fov, horizontal_fov);
|
||||||
|
assert_eq!(camera.near_plane, 0.5);
|
||||||
|
assert_eq!(camera.far_plane, 700.0);
|
||||||
|
|
||||||
|
let assert_edges = |frame: PreviewCameraFrame, aspect: f32| {
|
||||||
|
let expected_vertical_fov =
|
||||||
|
2.0 * ((horizontal_fov * 0.5).tan() / aspect).atan();
|
||||||
|
assert!((frame.vertical_fov - expected_vertical_fov).abs() < 1.0e-5);
|
||||||
|
let depth = 10.0;
|
||||||
|
let center = add3(frame.position, scale3(frame.forward, depth));
|
||||||
|
let right_edge = add3(
|
||||||
|
center,
|
||||||
|
scale3(frame.right, depth * (horizontal_fov * 0.5).tan()),
|
||||||
|
);
|
||||||
|
let top_edge = add3(
|
||||||
|
center,
|
||||||
|
scale3(frame.up, depth * (expected_vertical_fov * 0.5).tan()),
|
||||||
|
);
|
||||||
|
let center_clip = row_vector_transform(
|
||||||
|
[center[0], center[1], center[2], 1.0],
|
||||||
|
frame.clip_from_world,
|
||||||
|
);
|
||||||
|
let right_clip = row_vector_transform(
|
||||||
|
[right_edge[0], right_edge[1], right_edge[2], 1.0],
|
||||||
|
frame.clip_from_world,
|
||||||
|
);
|
||||||
|
let top_clip = row_vector_transform(
|
||||||
|
[top_edge[0], top_edge[1], top_edge[2], 1.0],
|
||||||
|
frame.clip_from_world,
|
||||||
|
);
|
||||||
|
assert!(center_clip[3] > 0.0 && right_clip[3] > 0.0 && top_clip[3] > 0.0);
|
||||||
|
assert!(center_clip[0].abs() < 5.0e-4, "aspect {aspect}: {center_clip:?}");
|
||||||
|
assert!(center_clip[1].abs() < 5.0e-4, "aspect {aspect}: {center_clip:?}");
|
||||||
|
assert!(
|
||||||
|
(right_clip[0] / right_clip[3] - 1.0).abs() < 1.0e-4,
|
||||||
|
"aspect {aspect}: {right_clip:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(top_clip[1] / top_clip[3] + 1.0).abs() < 1.0e-4,
|
||||||
|
"aspect {aspect}: {top_clip:?}"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
for aspect in [0.05, 0.75, 4.0 / 3.0, 16.0 / 9.0, 20.0] {
|
||||||
|
assert_edges(camera.preview_frame(aspect), aspect);
|
||||||
|
|
||||||
|
let projection = LegacyD3d7Projection {
|
||||||
|
viewport: [0, 0, 1000, (1000.0 / aspect).round() as i32],
|
||||||
|
near_plane: 0.5,
|
||||||
|
far_plane: 700.0,
|
||||||
|
field_of_view_radians: horizontal_fov,
|
||||||
|
};
|
||||||
|
let raw_transform = RawCameraTransform {
|
||||||
|
words: [
|
||||||
|
1.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
1.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
1.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
0.0_f32.to_bits(),
|
||||||
|
1.0_f32.to_bits(),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let captured = PreviewCameraFrame::from_legacy_d3d7(raw_transform, projection)
|
||||||
|
.expect("valid D3D7 projection");
|
||||||
|
assert_edges(captured, projection.viewport[2] as f32 / projection.viewport[3] as f32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn direction_projection_accepts_front_offscreen_rays_but_rejects_horizon() {
|
||||||
|
let camera = FreeFlightCamera {
|
||||||
|
position: [0.0; 3],
|
||||||
|
yaw: 0.0,
|
||||||
|
pitch: 0.0,
|
||||||
|
horizontal_fov: 1.3,
|
||||||
|
near_plane: 0.5,
|
||||||
|
far_plane: 700.0,
|
||||||
|
move_speed: 1.0,
|
||||||
|
}
|
||||||
|
.preview_frame(16.0 / 9.0);
|
||||||
|
let viewport = [1280.0, 720.0];
|
||||||
|
let offscreen = project_direction_pixels(&camera, [0.2, 0.979_795_9, 0.0], viewport)
|
||||||
|
.expect("front-facing directions are not rejected by the near distance");
|
||||||
|
assert!(offscreen[0] < 0.0 || offscreen[0] > viewport[0]);
|
||||||
|
assert!(project_direction_pixels(&camera, [1.0e-5, 1.0, 0.0], viewport).is_none());
|
||||||
|
assert!(project_direction_pixels(&camera, [-1.0, 0.0, 0.0], viewport).is_none());
|
||||||
|
|
||||||
|
let ray = screen_ray_from_pixels(&camera, [viewport[0] * 1.1, viewport[1] * 0.5], viewport)
|
||||||
|
.expect("an offscreen pixel still has a view ray");
|
||||||
|
let ray_pixels = project_direction_pixels(&camera, ray, viewport)
|
||||||
|
.expect("the offscreen ray remains in the front hemisphere");
|
||||||
|
assert!((ray_pixels[0] - viewport[0] * 1.1).abs() < 1.0e-3);
|
||||||
|
assert!((ray_pixels[1] - viewport[1] * 0.5).abs() < 1.0e-3);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn native_sky_heading_selects_matching_gradient_sectors_for_both_camera_origins() {
|
fn native_sky_heading_selects_matching_gradient_sectors_for_both_camera_origins() {
|
||||||
use fparkan_fx::atmosphere::{PackedColor, SkySample, SKY_COLOR_COUNT};
|
use fparkan_fx::atmosphere::{PackedColor, SkySample, SKY_COLOR_COUNT};
|
||||||
@@ -6487,7 +6606,7 @@ mod tests {
|
|||||||
position: [0.0; 3],
|
position: [0.0; 3],
|
||||||
yaw: forward[1].atan2(forward[0]),
|
yaw: forward[1].atan2(forward[0]),
|
||||||
pitch: 0.0,
|
pitch: 0.0,
|
||||||
vertical_fov: std::f32::consts::FRAC_PI_3,
|
horizontal_fov: projection.field_of_view_radians,
|
||||||
near_plane: projection.near_plane,
|
near_plane: projection.near_plane,
|
||||||
far_plane: projection.far_plane,
|
far_plane: projection.far_plane,
|
||||||
move_speed: 1.0,
|
move_speed: 1.0,
|
||||||
@@ -6544,7 +6663,7 @@ mod tests {
|
|||||||
position: [10.0, 20.0, 30.0],
|
position: [10.0, 20.0, 30.0],
|
||||||
yaw: 0.0,
|
yaw: 0.0,
|
||||||
pitch: 0.0,
|
pitch: 0.0,
|
||||||
vertical_fov: 1.0,
|
horizontal_fov: 1.0,
|
||||||
near_plane: 0.1,
|
near_plane: 0.1,
|
||||||
far_plane: 100.0,
|
far_plane: 100.0,
|
||||||
move_speed: 1.0,
|
move_speed: 1.0,
|
||||||
@@ -6596,7 +6715,7 @@ mod tests {
|
|||||||
position: [0.25, 0.25, 34.0],
|
position: [0.25, 0.25, 34.0],
|
||||||
yaw: 0.0,
|
yaw: 0.0,
|
||||||
pitch: 0.0,
|
pitch: 0.0,
|
||||||
vertical_fov: 1.0,
|
horizontal_fov: 1.0,
|
||||||
near_plane: 0.1,
|
near_plane: 0.1,
|
||||||
far_plane: 100.0,
|
far_plane: 100.0,
|
||||||
move_speed: 1.0,
|
move_speed: 1.0,
|
||||||
@@ -6695,7 +6814,7 @@ mod tests {
|
|||||||
position: [0.0, 0.0, 0.0],
|
position: [0.0, 0.0, 0.0],
|
||||||
yaw: 0.0,
|
yaw: 0.0,
|
||||||
pitch: 0.0,
|
pitch: 0.0,
|
||||||
vertical_fov: 1.0,
|
horizontal_fov: 1.0,
|
||||||
near_plane: 0.1,
|
near_plane: 0.1,
|
||||||
far_plane: 100.0,
|
far_plane: 100.0,
|
||||||
move_speed: 2.0,
|
move_speed: 2.0,
|
||||||
@@ -6722,7 +6841,7 @@ mod tests {
|
|||||||
position: [0.0, 0.0, 0.0],
|
position: [0.0, 0.0, 0.0],
|
||||||
yaw: 0.0,
|
yaw: 0.0,
|
||||||
pitch: 0.6,
|
pitch: 0.6,
|
||||||
vertical_fov: 1.0,
|
horizontal_fov: 1.0,
|
||||||
near_plane: 0.1,
|
near_plane: 0.1,
|
||||||
far_plane: 100.0,
|
far_plane: 100.0,
|
||||||
move_speed: 2.0,
|
move_speed: 2.0,
|
||||||
@@ -7143,7 +7262,7 @@ mod tests {
|
|||||||
position: [10.0, 20.0, 30.0],
|
position: [10.0, 20.0, 30.0],
|
||||||
yaw: 0.0,
|
yaw: 0.0,
|
||||||
pitch: 0.0,
|
pitch: 0.0,
|
||||||
vertical_fov: std::f32::consts::FRAC_PI_2,
|
horizontal_fov: std::f32::consts::FRAC_PI_2,
|
||||||
near_plane: 0.1,
|
near_plane: 0.1,
|
||||||
far_plane: 100.0,
|
far_plane: 100.0,
|
||||||
move_speed: 1.0,
|
move_speed: 1.0,
|
||||||
@@ -7194,7 +7313,7 @@ mod tests {
|
|||||||
position: [0.0; 3],
|
position: [0.0; 3],
|
||||||
yaw: 0.0,
|
yaw: 0.0,
|
||||||
pitch: 0.0,
|
pitch: 0.0,
|
||||||
vertical_fov: 1.0,
|
horizontal_fov: 1.0,
|
||||||
near_plane: 0.1,
|
near_plane: 0.1,
|
||||||
far_plane: 100.0,
|
far_plane: 100.0,
|
||||||
move_speed: 1.0,
|
move_speed: 1.0,
|
||||||
|
|||||||
@@ -143,6 +143,27 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std
|
|||||||
`0x481C5`. Это преобразование применяется только на границе неба и не меняет
|
`0x481C5`. Это преобразование применяется только на границе неба и не меняет
|
||||||
рыскание или управление движением `FreeFlightCamera`.
|
рыскание или управление движением `FreeFlightCamera`.
|
||||||
|
|
||||||
|
## Камера свободного полёта
|
||||||
|
|
||||||
|
Начальный профиль использует `near = 0.5`, `far = 700` и горизонтальный FOV
|
||||||
|
`1.3` рад — параметры, наблюдавшиеся в проверенном контексте GOG Ngi32
|
||||||
|
([формула native projection](../tomes/05-render.md#камера-и-размещение-в-оригинале)).
|
||||||
|
Для отношения ширины к высоте `a` превью вычисляет вертикальный угол
|
||||||
|
```rust
|
||||||
|
fn vertical_fov(self, aspect: f32) -> f32 {
|
||||||
|
2.0 * ((self.horizontal_fov * 0.5).tan() / aspect).atan()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Код заменяет невалидный `aspect` на отношение 16:9; горизонтальный угол
|
||||||
|
остаётся `1.3` рад после изменения размера окна, а новый viewport и матрица
|
||||||
|
камеры применяются при следующей отрисовке.
|
||||||
|
|
||||||
|
Проекция направления не сравнивает косинус угла с ближней дистанцией: она
|
||||||
|
отбрасывает только лучи за камерой и почти параллельные плоскости изображения
|
||||||
|
(`forward · direction <= 1e-4`). Лучи в передней полусфере могут проецироваться
|
||||||
|
за край viewport; владелец спрайта отдельно решает, нужно ли его рисовать.
|
||||||
|
|
||||||
## Ограничения точного совпадения
|
## Ограничения точного совпадения
|
||||||
|
|
||||||
`VulkanStaticCamera::from_legacy_d3d7` выполняет восстановление вида и проекции
|
`VulkanStaticCamera::from_legacy_d3d7` выполняет восстановление вида и проекции
|
||||||
|
|||||||
Reference in new issue
Block a user