Merge branch 'isse/sittable-sprites' into 'master'

Sprites on airships, and mountable sprites

See merge request veloren/veloren!3886
This commit is contained in:
Isse 2023-05-10 13:55:51 +00:00
commit 2bc63d22ae
75 changed files with 2949 additions and 971 deletions

View File

@ -46,6 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added accessibility settings tab.
- Setting to enable subtitles describing sfx.
- Item drops that are spatially close and compatible will now merge with one-another to reduce performance problems.
- Airships can now have sprites, which can be interacted with.
- Some sprites can be sat on.
### Changed

View File

@ -1,94 +1,96 @@
({
DefaultAirship: (
bone0: (
//offset: (3.0, 7.0, 1.0),
//offset: (-20.75, -34.75, 1.25),
//offset: (0.0, 0.0, 0.0),
offset: (-17.5, -39.0, 1.0),
//phys_offset: (0.25, 0.25, 0.25),
phys_offset: (0.0, 0.0, 0.0),
central: ("airship_human.structure"),
),
bone1: (
offset: (-8.5, -2.0, -8.5),
phys_offset: (0.0, 0.0, 0.0),
central: ("airship_human.propeller-l"),
),
bone2: (
offset: (-8.5, -2.0, -8.5),
phys_offset: (0.0, 0.0, 0.0),
central: ("airship_human.propeller-r"),
),
bone3: (
offset: (-1.5, -11.0, -5.5),
phys_offset: (0.0, 0.0, 0.0),
offset: (-1.5, -10.0, -4.5),
central: ("airship_human.rudder"),
),
custom_indices: {
1: Air(ChairSingle, 4),
2: Air(Helm, 0),
3: Air(DoorWide, 4),
8: Air(DoorWide, 0),
9: Air(CraftingBench, 0),
11: Air(RepairBench, 0),
12: Air(DismantlingBench, 4),
15: Air(Anvil, 2),
17: Air(CookingPot, 0),
18: Air(WallLamp, 4),
23: Air(FireBowlGround, 4),
},
),
AirBalloon: (
bone0: (
offset: (-14.5, -16.0, 0.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("air_balloon.structure"),
),
bone1: (
offset: (0.0, 0.0, 0.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("empty"),
),
bone2: (
offset: (0.0, 0.0, 0.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("empty"),
),
bone3: (
offset: (-1.5, -6.0, -5.0),
phys_offset: (0.0, 0.0, 0.0),
offset: (-1.5, -6.0, -6.0),
central: ("air_balloon.rudder"),
),
custom_indices: {
1: Air(Helm),
},
),
SailBoat: (
bone0: (
offset: (-6.5, -15.5, 0.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("sail_boat.structure"),
),
bone1: (
offset: (0.0, 0.0, 0.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("empty"),
),
bone2: (
offset: (0.0, 0.0, 0.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("empty"),
),
bone3: (
offset: (-1.5, -6.0, -5.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("empty"),
),
),
Galleon: (
bone0: (
offset: (-16, -16, -3.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("galleon.structure"),
),
bone1: (
offset: (0.0, 0.0, 0.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("empty"),
),
bone2: (
offset: (0.0, 0.0, 0.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("empty"),
),
bone3: (
offset: (0.0, 0.0, 0.0),
phys_offset: (0.0, 0.0, 0.0),
central: ("empty"),
),
custom_indices: {
1: Air(Helm, 0),
},
),
})

Binary file not shown.

Binary file not shown.

BIN
assets/common/voxel/galleon/structure.vox (Stored with Git LFS)

Binary file not shown.

View File

@ -112,3 +112,4 @@ common-material-stone = Stone
common-material-cloth = Cloth
common-material-hide = Hide
common-sprite-chest = Chest
common-sprite-chair = Chair

View File

@ -52,3 +52,4 @@ hud-talk = Talk
hud-trade = Trade
hud-mount = Mount
hud-sit = Sit
hud-steer = Steer

View File

@ -167,7 +167,9 @@ void main() {
// If the figure is large enough to be 'terrain-like', we apply a noise effect to it
#ifndef EXPERIMENTAL_NONOISE
if (scale >= 0.5) {
float noise = hash(vec4(floor(m_pos * 3.0 - f_norm * 0.5), 0));
// TODO: Fix this, it isn't cprrect to use `f_norm` here. Would need something like
// `m_norm` which is a normal relative to the figure.
float noise = hash(vec4(floor(m_pos * 3.0 - vec3(0.5, 0, 0) - f_norm * 0.1), 0));
const float A = 0.055;
const float W_INV = 1 / (1 + A);

View File

@ -40,9 +40,9 @@ layout(location = 2) in vec2 f_vel;
layout(std140, set = 2, binding = 0)
uniform u_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
ivec4 atlas_offs;
float load_time;
};
layout(location = 0) out vec4 tgt_color;

View File

@ -42,9 +42,9 @@ layout(location = 2) in vec2 f_vel;
layout(std140, set = 2, binding = 0)
uniform u_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
ivec4 atlas_offs;
float load_time;
};
layout(location = 0) out vec4 tgt_color;

View File

@ -26,9 +26,9 @@ layout(location = 1) in uint v_vel;
layout(std140, set = 2, binding = 0)
uniform u_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
ivec4 atlas_offs;
float load_time;
};
// struct ShadowLocals {
@ -51,7 +51,9 @@ layout(location = 2) out vec2 f_vel;
const float EXTRA_NEG_Z = 65536.0/*65536.1*/;
void main() {
f_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0x1FFFFu) - EXTRA_NEG_Z) + model_offs - focus_off.xyz;
vec3 rel_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0x1FFFFu) - EXTRA_NEG_Z);
f_pos = (model_mat * vec4(rel_pos, 1.0)).xyz - focus_off.xyz;
f_vel = vec2(
(float(v_vel & 0xFFFFu) - 32768.0) / 1000.0,
(float((v_vel >> 16u) & 0xFFFFu) - 32768.0) / 1000.0
@ -68,7 +70,7 @@ void main() {
#endif
#endif
float pull_down = pow(distance(focus_pos.xy, f_pos.xy) / (view_distance.x * 0.95), 20.0) * 0.7;
// float pull_down = pow(distance(focus_pos.xy, f_pos.xy) / (view_distance.x * 0.95), 20.0) * 0.7;
//f_pos.z -= pull_down;
#ifdef EXPERIMENTAL_CURVEDWORLD

View File

@ -44,9 +44,9 @@ layout(location = 0) in uint v_pos_norm;
// Light projection matrices.
layout (std140, set = 1, binding = 0)
uniform u_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
ivec4 atlas_offs;
float load_time;
};
// out vec4 shadowMapCoord;
@ -55,11 +55,9 @@ const float EXTRA_NEG_Z = 32768.0;
void main() {
vec3 f_chunk_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
vec3 f_pos = f_chunk_pos + (model_offs - focus_off.xyz);
vec3 f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
// f_pos = v_pos;
// vec3 f_pos = f_chunk_pos + model_offs;
// gl_Position = v_pos + vec4(model_offs, 0.0);
gl_Position = /*all_mat * */shadowMatrices * vec4(f_pos/*, 1.0*/, /*float(((f_pos_norm >> 29) & 0x7u) ^ 0x1)*//*uintBitsToFloat(v_pos_norm)*/1.0);
// gl_Position.z = -gl_Position.z;
// gl_Position.z = clamp(gl_Position.z, -abs(gl_Position.w), abs(gl_Position.w));

View File

@ -33,9 +33,9 @@ layout(location = 1) in uint v_pos_norm;
// Light projection matrices.
layout (std140, set = 1, binding = 0)
uniform u_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
ivec4 atlas_offs;
float load_time;
};
// out vec4 shadowMapCoord;
@ -44,11 +44,9 @@ const int EXTRA_NEG_Z = 32768;
void main() {
vec3 f_chunk_pos = vec3(ivec3((uvec3(v_pos_norm) >> uvec3(0, 6, 12)) & uvec3(0x3Fu, 0x3Fu, 0xFFFFu)) - ivec3(0, 0, EXTRA_NEG_Z));
vec3 f_pos = f_chunk_pos + model_offs - focus_off.xyz;
vec3 f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
// f_pos = v_pos;
// vec3 f_pos = f_chunk_pos + model_offs;
// gl_Position = v_pos + vec4(model_offs, 0.0);
gl_Position = /*all_mat * */vec4(f_pos/*, 1.0*/, /*float(((f_pos_norm >> 29) & 0x7u) ^ 0x1)*//*uintBitsToFloat(v_pos_norm)*/1.0);
// shadowMapCoord = lights[gl_InstanceID].light_pos * gl_Vertex;
// vec4(v_pos, 0.0, 1.0);

View File

@ -34,9 +34,9 @@ layout(location = 0) in uint v_pos_norm;
// Light projection matrices.
layout (std140, set = 1, binding = 0)
uniform u_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
ivec4 atlas_offs;
float load_time;
};
// out vec4 shadowMapCoord;
@ -49,11 +49,9 @@ layout( push_constant ) uniform PointLightMatrix {
void main() {
vec3 f_chunk_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
vec3 f_pos = f_chunk_pos + model_offs - focus_off.xyz;
vec3 f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
// f_pos = v_pos;
// vec3 f_pos = f_chunk_pos + model_offs;
// gl_Position = v_pos + vec4(model_offs, 0.0);
// gl_Position = /*all_mat * */vec4(f_pos/*, 1.0*/, /*float(((f_pos_norm >> 29) & 0x7u) ^ 0x1)*//*uintBitsToFloat(v_pos_norm)*/1.0);
// shadowMapCoord = lights[gl_InstanceID].light_pos * gl_Vertex;
// vec4(v_pos, 0.0, 1.0);

View File

@ -48,9 +48,9 @@ layout(location = 0) in uint v_pos_norm;
// Light projection matrices.
layout (std140, set = 1, binding = 0)
uniform u_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
ivec4 atlas_offs;
float load_time;
};
// out vec4 shadowMapCoord;
@ -59,7 +59,7 @@ const float EXTRA_NEG_Z = 32768.0;
void main() {
vec3 f_chunk_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
vec3 f_pos = f_chunk_pos + (model_offs - focus_off.xyz);
vec3 f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
gl_Position = rain_occlusion_matrices * vec4(f_pos, 1.0);
}

View File

@ -37,9 +37,9 @@ layout(set = 0, binding = 15) restrict readonly buffer sprite_verts {
layout (std140, set = 3, binding = 0)
uniform u_terrain_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
ivec4 atlas_offs;
float load_time;
};
// TODO: consider grouping into vec4's
@ -94,10 +94,12 @@ void main() {
inst_mat[0] = inst_mat0;
inst_mat[1] = inst_mat1;
inst_mat[2] = inst_mat2;
inst_mat[3] = inst_mat3;
inst_mat[3] = inst_mat3;// + vec4(-14.5, -16.5, 0.0, 0.0);
inst_mat = model_mat * inst_mat;
// Worldpos of the chunk that this sprite is in
vec3 chunk_offs = model_offs - focus_off.xyz;
vec3 chunk_offs = -focus_off.xyz;
f_inst_light = vec2(inst_light, inst_glow);
@ -116,15 +118,14 @@ void main() {
// Position of the sprite block in the chunk
// Used for highlighting the selected sprite, and for opening doors
vec3 inst_chunk_pos = vec3(inst_pos_ori_door & 0x3Fu, (inst_pos_ori_door >> 6) & 0x3Fu, float((inst_pos_ori_door >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
vec3 sprite_pos = inst_chunk_pos + chunk_offs;
vec3 sprite_pos = inst_mat[3].xyz + chunk_offs;
float sprite_ori = (inst_pos_ori_door >> 29) & 0x7u;
#ifndef EXPERIMENTAL_BAREMINIMUM
if((inst_pos_ori_door & (1 << 28)) != 0) {
const float MIN_OPEN_DIST = 0.2;
const float MAX_OPEN_DIST = 1.5;
float min_entity_dist = nearest_entity(sprite_pos + 0.5, 1.0).w;
float min_entity_dist = nearest_entity(sprite_pos, 1.0).w;
if (min_entity_dist < MAX_OPEN_DIST) {
float flip = sprite_ori <= 3 ? 1.0 : -1.0;

View File

@ -52,9 +52,9 @@ uniform sampler s_col_light;
layout (std140, set = 3, binding = 0)
uniform u_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
ivec4 atlas_offs;
float load_time;
};
layout(location = 0) out vec4 tgt_color;
@ -452,7 +452,8 @@ void main() {
// light_reflection_factorplight_reflection_factor
// vec3 surf_color = illuminate(srgb_to_linear(f_col), light, diffuse_light, ambient_light);
vec3 f_chunk_pos = f_pos - (model_offs - focus_off.xyz);
vec3 f_chunk_pos = f_pos - (model_mat[3].xyz - focus_off.xyz);
#ifdef EXPERIMENTAL_NONOISE
float noise = 0.0;
#else

View File

@ -30,10 +30,10 @@ layout(location = 1) in uint v_atlas_pos;
layout (std140, set = 3, binding = 0)
uniform u_locals {
vec3 model_offs;
float load_time;
mat4 model_mat;
// TODO: consider whether these need to be signed
ivec4 atlas_offs;
float load_time;
};
//struct ShadowLocals {
@ -73,7 +73,7 @@ void main() {
// over it (if this vertex to see if it intersects.
// f_chunk_pos = vec3(ivec3((uvec3(v_pos_norm) >> uvec3(0, 6, 12)) & uvec3(0x3Fu, 0x3Fu, 0xFFFFu)) - ivec3(0, 0, EXTRA_NEG_Z));
vec3 f_chunk_pos = vec3(v_pos_norm & 0x3Fu, (v_pos_norm >> 6) & 0x3Fu, float((v_pos_norm >> 12) & 0xFFFFu) - EXTRA_NEG_Z);
f_pos = f_chunk_pos + model_offs - focus_off.xyz;
f_pos = (model_mat * vec4(f_chunk_pos, 1.0)).xyz - focus_off.xyz;
f_load_time = load_time;

BIN
assets/voxygen/voxel/sprite/door/door-wide.vox (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/voxel/sprite/furniture/helm.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -1871,6 +1871,16 @@ DoorDark: Some((
],
wind_sway: 0.0,
)),
DoorWide: Some((
variations: [
(
model: "voxygen.voxel.sprite.door.door-wide",
offset: (-5.5, -5.5, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
],
wind_sway: 0.0,
)),
// Bed
Bed: Some((
variations: [
@ -1924,6 +1934,17 @@ ChairDouble: Some((
],
wind_sway: 0.0,
)),
// Helm
Helm: Some((
variations: [
(
model: "voxygen.voxel.sprite.furniture.helm",
offset: (-5.5, -5.5, 0.0),
lod_axes: (1.0, 1.0, 1.0),
),
],
wind_sway: 0.0,
)),
// CoatRack
CoatRack: Some((
variations: [

View File

@ -37,7 +37,7 @@ use common::{
grid::Grid,
link::Is,
lod,
mounting::Rider,
mounting::{Rider, VolumePos, VolumeRider},
outcome::Outcome,
recipe::{ComponentRecipeBook, RecipeBook, RepairRecipeBook},
resources::{GameMode, PlayerEntity, Time, TimeOfDay},
@ -1185,7 +1185,7 @@ impl Client {
&mut self,
recipe: &str,
slots: Vec<(u32, InvSlotId)>,
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
craft_sprite: Option<(VolumePos, SpriteKind)>,
amount: u32,
) -> bool {
let (can_craft, required_sprite) = self.can_craft_recipe(recipe, amount);
@ -1217,7 +1217,7 @@ impl Client {
/// Salvage the item in the given inventory slot. `salvage_pos` should be
/// the location of a relevant crafting station within range of the player.
pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: Vec3<i32>) -> bool {
pub fn salvage_item(&mut self, slot: InvSlotId, salvage_pos: VolumePos) -> bool {
let is_salvageable = self.can_salvage_item(slot);
if is_salvageable {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
@ -1239,7 +1239,7 @@ impl Client {
&mut self,
primary_component: InvSlotId,
secondary_component: InvSlotId,
sprite_pos: Option<Vec3<i32>>,
sprite_pos: Option<VolumePos>,
) -> bool {
let inventories = self.inventories();
let inventory = inventories.get(self.entity());
@ -1288,7 +1288,7 @@ impl Client {
material: InvSlotId,
modifier: Option<InvSlotId>,
slots: Vec<(u32, InvSlotId)>,
sprite_pos: Option<Vec3<i32>>,
sprite_pos: Option<VolumePos>,
) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::InventoryEvent(
InventoryEvent::CraftRecipe {
@ -1309,7 +1309,7 @@ impl Client {
&mut self,
item: Slot,
slots: Vec<(u32, InvSlotId)>,
sprite_pos: Vec3<i32>,
sprite_pos: VolumePos,
) -> bool {
let is_repairable = {
let inventories = self.inventories();
@ -1448,6 +1448,12 @@ impl Client {
.read_storage::<Is<Rider>>()
.get(self.entity())
.is_some()
|| self
.state
.ecs()
.read_storage::<Is<VolumeRider>>()
.get(self.entity())
.is_some()
}
pub fn is_lantern_enabled(&self) -> bool {
@ -1464,6 +1470,13 @@ impl Client {
}
}
/// Mount a block at a `VolumePos`.
pub fn mount_volume(&mut self, volume_pos: VolumePos) {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::MountVolume(
volume_pos,
)));
}
pub fn unmount(&mut self) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Unmount)); }
pub fn respawn(&mut self) {

View File

@ -34,6 +34,7 @@ macro_rules! synced_components {
group: Group,
is_mount: IsMount,
is_rider: IsRider,
is_volume_rider: IsVolumeRider,
mass: Mass,
density: Density,
collider: Collider,
@ -72,7 +73,7 @@ macro_rules! reexport_comps {
mod inner {
pub use common::comp::*;
use common::link::Is;
use common::mounting::{Mount, Rider};
use common::mounting::{Mount, Rider, VolumeRider};
// We alias these because the identifier used for the
// component's type is reused as an enum variant name
@ -82,6 +83,7 @@ macro_rules! reexport_comps {
// we can't just re-export all the types directly from `common::comp`.
pub type IsMount = Is<Mount>;
pub type IsRider = Is<Rider>;
pub type IsVolumeRider = Is<VolumeRider>;
}
// Re-export all the component types. So that uses of `synced_components!` outside this
@ -178,6 +180,10 @@ impl NetSync for IsRider {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for IsVolumeRider {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Mass {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}

View File

@ -461,6 +461,7 @@ impl ServerChatCommand {
Float("x", 0.0, Required),
Float("y", 0.0, Required),
Float("z", 0.0, Required),
Boolean("Dismount from ship", "true".to_string(), Optional),
],
"Teleport to a position",
Some(Admin),
@ -505,6 +506,7 @@ impl ServerChatCommand {
Float("x", 0.0, Required),
Float("y", 0.0, Required),
Float("z", 0.0, Required),
Boolean("Dismount from ship", "true".to_string(), Optional),
],
"Offset your current position",
Some(Admin),
@ -637,7 +639,10 @@ impl ServerChatCommand {
// Uses Message because site names can contain spaces,
// which would be assumed to be separators otherwise
ServerChatCommand::Site => cmd(
vec![SiteName(Required)],
vec![
SiteName(Required),
Boolean("Dismount from ship", "true".to_string(), Optional),
],
"Teleport to a site",
Some(Moderator),
),
@ -681,12 +686,18 @@ impl ServerChatCommand {
Some(Admin),
),
ServerChatCommand::Tp => cmd(
vec![PlayerName(Optional)],
vec![
PlayerName(Optional),
Boolean("Dismount from ship", "true".to_string(), Optional),
],
"Teleport to another player",
Some(Moderator),
),
ServerChatCommand::RtsimTp => cmd(
vec![Integer("npc index", 0, Required)],
vec![
Integer("npc index", 0, Required),
Boolean("Dismount from ship", "true".to_string(), Optional),
],
"Teleport to an rtsim npc",
Some(Moderator),
),
@ -737,9 +748,11 @@ impl ServerChatCommand {
"Send messages to everyone on the server",
None,
),
ServerChatCommand::MakeVolume => {
cmd(vec![], "Create a volume (experimental)", Some(Admin))
},
ServerChatCommand::MakeVolume => cmd(
vec![Integer("size", 15, Optional)],
"Create a volume (experimental)",
Some(Admin),
),
ServerChatCommand::Location => {
cmd(vec![Any("name", Required)], "Teleport to a location", None)
},

View File

@ -100,7 +100,7 @@ impl Body {
pub fn density(&self) -> Density {
match self {
Body::DefaultAirship | Body::AirBalloon | Body::Volume => Density(AIR_DENSITY),
_ => Density(AIR_DENSITY * 0.8 + WATER_DENSITY * 0.2), // Most boats should be buoyant
_ => Density(AIR_DENSITY * 0.2 + WATER_DENSITY * 0.8), // Most boats should be buoyant
}
}
@ -146,17 +146,17 @@ pub const AIRSHIP_SCALE: f32 = 11.0;
pub mod figuredata {
use crate::{
assets::{self, AssetExt, AssetHandle, DotVoxAsset, Ron},
figure::cell::Cell,
figure::TerrainSegment,
terrain::{
block::{Block, BlockKind},
sprite::SpriteKind,
structure::load_base_structure,
},
volumes::dyna::{ColumnAccess, Dyna},
};
use hashbrown::HashMap;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use vek::Vec3;
use vek::{Rgb, Vec3};
#[derive(Deserialize)]
pub struct VoxSimple(pub String);
@ -164,18 +164,46 @@ pub mod figuredata {
#[derive(Deserialize)]
pub struct ShipCentralSpec(pub HashMap<super::Body, SidedShipCentralVoxSpec>);
#[derive(Deserialize)]
pub enum DeBlock {
Block(BlockKind),
Air(SpriteKind, #[serde(default)] u8),
Water(SpriteKind, #[serde(default)] u8),
}
impl DeBlock {
fn to_block(&self, color: Rgb<u8>) -> Block {
match *self {
DeBlock::Block(block) => Block::new(block, color),
DeBlock::Air(sprite, ori) => {
let block = Block::new(BlockKind::Air, color).with_sprite(sprite);
block.with_ori(ori).unwrap_or(block)
},
DeBlock::Water(sprite, ori) => {
let block = Block::new(BlockKind::Water, color).with_sprite(sprite);
block.with_ori(ori).unwrap_or(block)
},
}
}
}
#[derive(Deserialize)]
pub struct SidedShipCentralVoxSpec {
pub bone0: ShipCentralSubSpec,
pub bone1: ShipCentralSubSpec,
pub bone2: ShipCentralSubSpec,
pub bone3: ShipCentralSubSpec,
// TODO: Use StructureBlock here instead. Which would require passing `IndexRef` and
// `Calendar` when loading the voxel colliders, which wouldn't work while it's stored in a
// static.
#[serde(default)]
pub custom_indices: HashMap<u8, DeBlock>,
}
#[derive(Deserialize)]
pub struct ShipCentralSubSpec {
pub offset: [f32; 3],
pub phys_offset: [f32; 3],
pub central: VoxSimple,
#[serde(default)]
pub model_index: u32,
@ -190,7 +218,7 @@ pub mod figuredata {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct VoxelCollider {
pub(super) dyna: Dyna<Block, (), ColumnAccess>,
pub(super) dyna: TerrainSegment,
pub translation: Vec3<f32>,
/// This value should be incremented every time the volume is mutated
/// and can be used to keep track of volume changes.
@ -200,13 +228,13 @@ pub mod figuredata {
impl VoxelCollider {
pub fn from_fn<F: FnMut(Vec3<i32>) -> Block>(sz: Vec3<u32>, f: F) -> Self {
Self {
dyna: Dyna::from_fn(sz, (), f),
dyna: TerrainSegment::from_fn(sz, (), f),
translation: -sz.map(|e| e as f32) / 2.0,
mut_count: 0,
}
}
pub fn volume(&self) -> &Dyna<Block, (), ColumnAccess> { &self.dyna }
pub fn volume(&self) -> &TerrainSegment { &self.dyna }
}
impl assets::Compound for ShipSpec {
@ -218,27 +246,32 @@ pub mod figuredata {
AssetExt::load("common.manifests.ship_manifest")?;
let mut colliders = HashMap::new();
for (_, spec) in (manifest.read().0).0.iter() {
for bone in [&spec.bone0, &spec.bone1, &spec.bone2].iter() {
for (index, bone) in [&spec.bone0, &spec.bone1, &spec.bone2, &spec.bone3]
.iter()
.enumerate()
{
// TODO: Currently both client and server load models and manifests from
// "common.voxel.". In order to support CSG procedural airships, we probably
// need to load them in the server and sync them as an ECS resource.
let vox =
cache.load::<DotVoxAsset>(&["common.voxel.", &bone.central.0].concat())?;
let dyna = Dyna::<Cell, (), ColumnAccess>::from_vox(
&vox.read().0,
false,
bone.model_index as usize,
);
let dyna = dyna.map_into(|cell| {
if let Some(rgb) = cell.get_color() {
Block::new(BlockKind::Misc, rgb)
let base_structure = load_base_structure(&vox.read().0, |col| col);
let dyna = base_structure.vol.map_into(|cell| {
if let Some(i) = cell {
let color = base_structure.palette[u8::from(i) as usize];
if let Some(block) = spec.custom_indices.get(&i.get()) && index == 0 {
block.to_block(color)
} else {
Block::new(BlockKind::Misc, color)
}
} else {
Block::air(SpriteKind::Empty)
Block::empty()
}
});
let collider = VoxelCollider {
dyna,
translation: Vec3::from(bone.offset) + Vec3::from(bone.phys_offset),
translation: Vec3::from(bone.offset),
mut_count: 0,
};
colliders.insert(bone.central.0.clone(), collider);

View File

@ -9,6 +9,7 @@ use crate::{
invite::{InviteKind, InviteResponse},
BuffKind,
},
mounting::VolumePos,
trade::{TradeAction, TradeId},
uid::Uid,
util::Dir,
@ -28,7 +29,7 @@ pub enum InventoryEvent {
Sort,
CraftRecipe {
craft_event: CraftEvent,
craft_sprite: Option<Vec3<i32>>,
craft_sprite: Option<VolumePos>,
},
}
@ -57,7 +58,7 @@ pub enum InventoryManip {
Sort,
CraftRecipe {
craft_event: CraftEvent,
craft_sprite: Option<Vec3<i32>>,
craft_sprite: Option<VolumePos>,
},
SwapEquippedWeapons,
}
@ -143,6 +144,7 @@ pub enum ControlEvent {
InviteResponse(InviteResponse),
PerformTradeAction(TradeId, TradeAction),
Mount(Uid),
MountVolume(VolumePos),
Unmount,
InventoryEvent(InventoryEvent),
GroupManip(GroupManip),

View File

@ -1,4 +1,4 @@
use super::{Fluid, Ori};
use super::{ship::figuredata::ShipSpec, Fluid, Ori};
use crate::{
comp::{body::ship::figuredata::VoxelCollider, inventory::item::armor::Friction},
consts::WATER_DENSITY,
@ -127,6 +127,14 @@ pub enum Collider {
impl Collider {
pub fn is_voxel(&self) -> bool { matches!(self, Collider::Voxel { .. } | Collider::Volume(_)) }
pub fn get_vol<'a>(&'a self, ship_spec: &'a ShipSpec) -> Option<&'a VoxelCollider> {
match self {
Collider::Voxel { id } => ship_spec.colliders.get(id),
Collider::Volume(vol) => Some(&**vol),
_ => None,
}
}
pub fn bounding_radius(&self) -> f32 {
match self {
Collider::Voxel { .. } | Collider::Volume(_) => 1.0,

View File

@ -1,6 +1,7 @@
// The limit on distance between the entity and a collectible (squared)
pub const MAX_PICKUP_RANGE: f32 = 5.0;
pub const MAX_MOUNT_RANGE: f32 = 5.0;
pub const MAX_SPRITE_MOUNT_RANGE: f32 = 2.0;
pub const MAX_TRADE_RANGE: f32 = 20.0;
pub const GRAVITY: f32 = 25.0;

View File

@ -8,6 +8,7 @@ use crate::{
DisconnectReason, Ori, Pos,
},
lottery::LootSpec,
mounting::VolumePos,
outcome::Outcome,
rtsim::{RtSimEntity, RtSimVehicle},
terrain::SpriteKind,
@ -195,6 +196,7 @@ pub enum ServerEvent {
InitiateInvite(EcsEntity, Uid, InviteKind),
ProcessTradeAction(EcsEntity, TradeId, TradeAction),
Mount(EcsEntity, EcsEntity),
MountVolume(EcsEntity, VolumePos),
Unmount(EcsEntity),
Possess(Uid, Uid),
/// Inserts default components for a character when loading into the game

View File

@ -1,6 +1,6 @@
use std::num::NonZeroU8;
use crate::vol::Vox;
use crate::vol::FilledVox;
use vek::*;
const GLOWY: u8 = 1 << 1;
@ -72,15 +72,10 @@ impl Cell {
}
}
impl Vox for Cell {
fn empty() -> Self { Cell::Empty }
impl FilledVox for Cell {
fn default_non_filled() -> Self { Cell::Empty }
fn is_empty(&self) -> bool {
match self {
Cell::Filled(_) => false,
Cell::Empty => true,
}
}
fn is_filled(&self) -> bool { matches!(self, Cell::Filled(_)) }
}
#[cfg(test)]

View File

@ -1,5 +1,5 @@
use super::cell::CellData;
use crate::vol::Vox;
use crate::vol::FilledVox;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Material {
@ -22,10 +22,10 @@ pub enum MatCell {
Normal(CellData),
}
impl Vox for MatCell {
fn empty() -> Self { MatCell::None }
impl FilledVox for MatCell {
fn default_non_filled() -> Self { MatCell::None }
fn is_empty(&self) -> bool { matches!(self, MatCell::None) }
fn is_filled(&self) -> bool { !matches!(self, MatCell::None) }
}
#[cfg(test)]

View File

@ -9,12 +9,32 @@ pub use self::{
};
use crate::{
vol::{IntoFullPosIterator, IntoFullVolIterator, ReadVol, SizedVol, Vox, WriteVol},
terrain::{Block, BlockKind, SpriteKind},
vol::{FilledVox, IntoFullPosIterator, IntoFullVolIterator, ReadVol, SizedVol, WriteVol},
volumes::dyna::Dyna,
};
use dot_vox::DotVoxData;
use vek::*;
pub type TerrainSegment = Dyna<Block, ()>;
impl From<Segment> for TerrainSegment {
fn from(value: Segment) -> Self {
TerrainSegment::from_fn(value.sz, (), |pos| match value.get(pos) {
Err(_) | Ok(Cell::Empty) => Block::air(SpriteKind::Empty),
Ok(cell) => {
if cell.is_hollow() {
Block::air(SpriteKind::Empty)
} else if cell.is_glowy() {
Block::new(BlockKind::GlowingRock, cell.get_color().unwrap())
} else {
Block::new(BlockKind::Misc, cell.get_color().unwrap())
}
},
})
}
}
/// A type representing a volume that may be part of an animated figure.
///
/// Figures are used to represent things like characters, NPCs, mobs, etc.
@ -45,7 +65,7 @@ impl Segment {
let mut segment = Segment::filled(
Vec3::new(model.size.x, model.size.y, model.size.z),
Cell::empty(),
Cell::Empty,
(),
);
@ -76,7 +96,7 @@ impl Segment {
segment
} else {
Segment::filled(Vec3::zero(), Cell::empty(), ())
Segment::filled(Vec3::zero(), Cell::Empty, ())
}
}
@ -110,9 +130,9 @@ impl Segment {
// TODO: move
/// A `Dyna` builder that combines Dynas
pub struct DynaUnionizer<V: Vox>(Vec<(Dyna<V, ()>, Vec3<i32>)>);
pub struct DynaUnionizer<V: FilledVox>(Vec<(Dyna<V, ()>, Vec3<i32>)>);
impl<V: Vox + Copy> DynaUnionizer<V> {
impl<V: FilledVox + Copy> DynaUnionizer<V> {
#[allow(clippy::new_without_default)]
pub fn new() -> Self { DynaUnionizer(Vec::new()) }
@ -134,7 +154,10 @@ impl<V: Vox + Copy> DynaUnionizer<V> {
pub fn unify_with(self, mut f: impl FnMut(V) -> V) -> (Dyna<V, ()>, Vec3<i32>) {
if self.0.is_empty() {
return (Dyna::filled(Vec3::zero(), V::empty(), ()), Vec3::zero());
return (
Dyna::filled(Vec3::zero(), V::default_non_filled(), ()),
Vec3::zero(),
);
}
// Determine size of the new Dyna
@ -147,12 +170,12 @@ impl<V: Vox + Copy> DynaUnionizer<V> {
}
let new_size = (max_point - min_point).map(|e| e as u32);
// Allocate new segment
let mut combined = Dyna::filled(new_size, V::empty(), ());
let mut combined = Dyna::filled(new_size, V::default_non_filled(), ());
// Copy segments into combined
let origin = min_point.map(|e| -e);
for (dyna, offset) in self.0 {
for (pos, vox) in dyna.full_vol_iter() {
if !vox.is_empty() {
if vox.is_filled() {
combined.set(origin + offset + pos, f(*vox)).unwrap();
}
}
@ -166,7 +189,7 @@ pub type MatSegment = Dyna<MatCell, ()>;
impl MatSegment {
pub fn to_segment(&self, map: impl Fn(Material) -> Rgb<u8>) -> Segment {
let mut vol = Dyna::filled(self.size(), Cell::empty(), ());
let mut vol = Dyna::filled(self.size(), Cell::Empty, ());
for (pos, vox) in self.full_vol_iter() {
let data = match vox {
MatCell::None => continue,
@ -216,7 +239,7 @@ impl MatSegment {
let mut vol = Dyna::filled(
Vec3::new(model.size.x, model.size.y, model.size.z),
MatCell::empty(),
MatCell::None,
(),
);
@ -262,7 +285,7 @@ impl MatSegment {
vol
} else {
Dyna::filled(Vec3::zero(), MatCell::empty(), ())
Dyna::filled(Vec3::zero(), MatCell::None, ())
}
}
}

View File

@ -1,11 +1,16 @@
use crate::{
comp,
comp::{self, ship::figuredata::VOXEL_COLLIDER_MANIFEST},
link::{Is, Link, LinkHandle, Role},
terrain::TerrainGrid,
terrain::{Block, TerrainGrid},
uid::{Uid, UidAllocator},
vol::ReadVol,
};
use hashbrown::HashSet;
use serde::{Deserialize, Serialize};
use specs::{saveload::MarkerAllocator, Entities, Read, ReadExpect, ReadStorage, WriteStorage};
use specs::{
saveload::MarkerAllocator, storage::GenericWriteStorage, Component, DenseVecStorage, Entities,
Entity, Read, ReadExpect, ReadStorage, Write, WriteStorage,
};
use vek::*;
#[derive(Serialize, Deserialize, Debug)]
@ -39,6 +44,7 @@ impl Link for Mounting {
Read<'a, UidAllocator>,
WriteStorage<'a, Is<Mount>>,
WriteStorage<'a, Is<Rider>>,
ReadStorage<'a, Is<VolumeRider>>,
);
type DeleteData<'a> = (
Read<'a, UidAllocator>,
@ -59,7 +65,7 @@ impl Link for Mounting {
fn create(
this: &LinkHandle<Self>,
(uid_allocator, mut is_mounts, mut is_riders): Self::CreateData<'_>,
(uid_allocator, mut is_mounts, mut is_riders, is_volume_rider): Self::CreateData<'_>,
) -> Result<(), Self::Error> {
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
@ -67,8 +73,11 @@ impl Link for Mounting {
// Forbid self-mounting
Err(MountingError::NotMountable)
} else if let Some((mount, rider)) = entity(this.mount).zip(entity(this.rider)) {
let can_mount_with =
|entity| is_mounts.get(entity).is_none() && is_riders.get(entity).is_none();
let can_mount_with = |entity| {
!is_mounts.contains(entity)
&& !is_riders.contains(entity)
&& !is_volume_rider.contains(entity)
};
// Ensure that neither mount or rider are already part of a mounting
// relationship
@ -145,3 +154,260 @@ impl Link for Mounting {
});
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct VolumeRider;
impl Role for VolumeRider {
type Link = VolumeMounting;
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Volume {
Terrain,
Entity(Uid),
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub struct VolumePos {
pub kind: Volume,
pub pos: Vec3<i32>,
}
impl VolumePos {
pub fn terrain(block_pos: Vec3<i32>) -> Self {
Self {
kind: Volume::Terrain,
pos: block_pos,
}
}
pub fn entity(block_pos: Vec3<i32>, uid: Uid) -> Self {
Self {
kind: Volume::Entity(uid),
pos: block_pos,
}
}
}
impl VolumePos {
/// Retrieves the block and matrix transformation for this `VolumeBlock`
///
/// The transform is located in the blocks minimum position relative to the
/// volume.
pub fn get_block_and_transform(
&self,
terrain: &TerrainGrid,
uid_allocator: &UidAllocator,
mut read_pos_and_ori: impl FnMut(Entity) -> Option<(comp::Pos, comp::Ori)>,
colliders: &ReadStorage<comp::Collider>,
) -> Option<(Mat4<f32>, Block)> {
match self.kind {
Volume::Terrain => Some((
Mat4::translation_3d(self.pos.as_()),
*terrain.get(self.pos).ok()?,
)),
Volume::Entity(uid) => {
uid_allocator
.retrieve_entity_internal(uid.0)
.and_then(|entity| {
let collider = colliders.get(entity)?;
let (pos, ori) = read_pos_and_ori(entity)?;
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
let voxel_collider = collider.get_vol(&voxel_colliders_manifest)?;
let block = *voxel_collider.volume().get(self.pos).ok()?;
let local_translation = voxel_collider.translation + self.pos.as_();
let trans = Mat4::from(ori.to_quat()).translated_3d(pos.0)
* Mat4::<f32>::translation_3d(local_translation);
Some((trans, block))
})
},
}
}
/// Get the block at this `VolumePos`.
pub fn get_block(
&self,
terrain: &TerrainGrid,
uid_allocator: &UidAllocator,
colliders: &ReadStorage<comp::Collider>,
) -> Option<Block> {
match self.kind {
Volume::Terrain => Some(*terrain.get(self.pos).ok()?),
Volume::Entity(uid) => {
uid_allocator
.retrieve_entity_internal(uid.0)
.and_then(|entity| {
let collider = colliders.get(entity)?;
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
let voxel_collider = collider.get_vol(&voxel_colliders_manifest)?;
let block = *voxel_collider.volume().get(self.pos).ok()?;
Some(block)
})
},
}
}
}
#[derive(Default)]
pub struct VolumeRiders {
riders: HashSet<Vec3<i32>>,
}
impl Component for VolumeRiders {
type Storage = DenseVecStorage<Self>;
}
#[derive(Serialize, Deserialize, Debug)]
pub struct VolumeMounting {
pub pos: VolumePos,
pub block: Block,
pub rider: Uid,
}
impl Link for VolumeMounting {
type CreateData<'a> = (
Write<'a, VolumeRiders>,
WriteStorage<'a, VolumeRiders>,
WriteStorage<'a, Is<VolumeRider>>,
ReadStorage<'a, Is<Rider>>,
ReadStorage<'a, Is<Mount>>,
ReadExpect<'a, TerrainGrid>,
Read<'a, UidAllocator>,
ReadStorage<'a, comp::Collider>,
);
type DeleteData<'a> = (
Write<'a, VolumeRiders>,
WriteStorage<'a, VolumeRiders>,
WriteStorage<'a, Is<VolumeRider>>,
Read<'a, UidAllocator>,
);
type Error = MountingError;
type PersistData<'a> = (
Entities<'a>,
ReadStorage<'a, comp::Health>,
Read<'a, VolumeRiders>,
ReadStorage<'a, VolumeRiders>,
ReadStorage<'a, Is<VolumeRider>>,
ReadExpect<'a, TerrainGrid>,
Read<'a, UidAllocator>,
ReadStorage<'a, comp::Collider>,
);
fn create(
this: &LinkHandle<Self>,
(
mut terrain_riders,
mut volume_riders,
mut is_volume_riders,
is_riders,
is_mounts,
terrain_grid,
uid_allocator,
colliders,
): Self::CreateData<'_>,
) -> Result<(), Self::Error> {
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
let riders = match this.pos.kind {
Volume::Terrain => &mut *terrain_riders,
Volume::Entity(uid) => entity(uid)
.and_then(|entity| volume_riders.get_mut_or_default(entity))
.ok_or(MountingError::NoSuchEntity)?,
};
let rider = entity(this.rider).ok_or(MountingError::NoSuchEntity)?;
if !riders.riders.contains(&this.pos.pos)
&& !is_volume_riders.contains(rider)
&& !is_volume_riders.contains(rider)
&& !is_riders.contains(rider)
&& !is_mounts.contains(rider)
{
let block = this
.pos
.get_block(&terrain_grid, &uid_allocator, &colliders)
.ok_or(MountingError::NoSuchEntity)?;
if block == this.block {
let _ = is_volume_riders.insert(rider, this.make_role());
riders.riders.insert(this.pos.pos);
Ok(())
} else {
Err(MountingError::NotMountable)
}
} else {
Err(MountingError::NotMountable)
}
}
fn persist(
this: &LinkHandle<Self>,
(
entities,
healths,
terrain_riders,
volume_riders,
is_volume_riders,
terrain_grid,
uid_allocator,
colliders,
): Self::PersistData<'_>,
) -> bool {
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
let is_alive =
|entity| entities.is_alive(entity) && healths.get(entity).map_or(true, |h| !h.is_dead);
let riders = match this.pos.kind {
Volume::Terrain => &*terrain_riders,
Volume::Entity(uid) => {
let Some(riders) = entity(uid)
.filter(|entity| is_alive(*entity))
.and_then(|entity| volume_riders.get(entity)) else {
return false;
};
riders
},
};
let rider_exists = entity(this.rider).map_or(false, |rider| {
is_volume_riders.contains(rider) && is_alive(rider)
});
let mount_spot_exists = riders.riders.contains(&this.pos.pos);
let block_exists = this
.pos
.get_block(&terrain_grid, &uid_allocator, &colliders)
.map_or(false, |block| block == this.block);
rider_exists && mount_spot_exists && block_exists
}
fn delete(
this: &LinkHandle<Self>,
(mut terrain_riders, mut volume_riders, mut is_rider, uid_allocator): Self::DeleteData<'_>,
) {
let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into());
let riders = match this.pos.kind {
Volume::Terrain => Some(&mut *terrain_riders),
Volume::Entity(uid) => {
entity(uid).and_then(|entity| volume_riders.get_mut_or_default(entity))
},
};
if let Some(riders) = riders {
riders.riders.remove(&this.pos.pos);
}
if let Some(entity) = entity(this.rider) {
is_rider.remove(entity);
}
}
}

View File

@ -9,7 +9,7 @@ use crate::{
Stats, Vel,
},
link::Is,
mounting::Rider,
mounting::{Rider, VolumeRider},
resources::{DeltaTime, Time},
terrain::TerrainGrid,
uid::Uid,
@ -97,7 +97,7 @@ pub trait CharacterBehavior {
ControlAction::Sit => self.sit(data, output_events),
ControlAction::Dance => self.dance(data, output_events),
ControlAction::Sneak => {
if data.mount_data.is_none() {
if data.mount_data.is_none() && data.volume_mount_data.is_none() {
self.sneak(data, output_events)
} else {
self.stand(data, output_events)
@ -147,6 +147,7 @@ pub struct JoinData<'a> {
pub alignment: Option<&'a comp::Alignment>,
pub terrain: &'a TerrainGrid,
pub mount_data: Option<&'a Is<Rider>>,
pub volume_mount_data: Option<&'a Is<VolumeRider>>,
pub stance: Option<&'a Stance>,
}
@ -176,6 +177,7 @@ pub struct JoinStruct<'a> {
pub alignment: Option<&'a comp::Alignment>,
pub terrain: &'a TerrainGrid,
pub mount_data: Option<&'a Is<Rider>>,
pub volume_mount_data: Option<&'a Is<VolumeRider>>,
pub stance: Option<&'a Stance>,
}
@ -219,6 +221,7 @@ impl<'a> JoinData<'a> {
terrain: j.terrain,
active_abilities: j.active_abilities,
mount_data: j.mount_data,
volume_mount_data: j.volume_mount_data,
stance: j.stance,
}
}

View File

@ -357,6 +357,9 @@ pub fn handle_skating(data: &JoinData, update: &mut StateUpdate) {
/// Handles updating `Components` to move player based on state of `JoinData`
pub fn handle_move(data: &JoinData<'_>, update: &mut StateUpdate, efficiency: f32) {
if data.volume_mount_data.is_some() {
return;
}
let submersion = data
.physics
.in_liquid()
@ -854,6 +857,80 @@ pub fn attempt_swap_equipped_weapons(data: &JoinData<'_>, update: &mut StateUpda
}
}
/// Checks if a block can be reached from a position.
fn can_reach_block(
player_pos: Vec3<f32>,
block_pos: Vec3<i32>,
range: f32,
body: &Body,
terrain: &TerrainGrid,
) -> bool {
let block_pos_f32 = block_pos.map(|x| x as f32 + 0.5);
// Closure to check if distance between a point and the block is less than
// MAX_PICKUP_RANGE and the radius of the body
let block_range_check = |pos: Vec3<f32>| {
(block_pos_f32 - pos).magnitude_squared() < (range + body.max_radius()).powi(2)
};
// Checks if player's feet or head is near to block
let close_to_block = block_range_check(player_pos)
|| block_range_check(player_pos + Vec3::new(0.0, 0.0, body.height()));
if close_to_block {
// Do a check that a path can be found between sprite and entity
// interacting with sprite Use manhattan distance * 1.5 for number
// of iterations
let iters = (3.0 * (block_pos_f32 - player_pos).map(|x| x.abs()).sum()) as usize;
// Heuristic compares manhattan distance of start and end pos
let heuristic =
move |pos: &Vec3<i32>, _: &Vec3<i32>| (block_pos - pos).map(|x| x.abs()).sum() as f32;
let mut astar = Astar::new(
iters,
player_pos.map(|x| x.floor() as i32),
BuildHasherDefault::<FxHasher64>::default(),
);
// Transition uses manhattan distance as the cost, with a slightly lower cost
// for z transitions
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
};
// Neighbors are all neighboring blocks that are air
let neighbors = |pos: &Vec3<i32>| {
const DIRS: [Vec3<i32>; 6] = [
Vec3::new(1, 0, 0),
Vec3::new(-1, 0, 0),
Vec3::new(0, 1, 0),
Vec3::new(0, -1, 0),
Vec3::new(0, 0, 1),
Vec3::new(0, 0, -1),
];
let pos = *pos;
DIRS.iter()
.map(move |dir| {
let dest = dir + pos;
(dest, transition(pos, dest))
})
.filter(|(pos, _)| {
terrain
.get(*pos)
.ok()
.map_or(false, |block| !block.is_filled())
})
};
// Pathing satisfied when it reaches the sprite position
let satisfied = |pos: &Vec3<i32>| *pos == block_pos;
astar
.poll(iters, heuristic, neighbors, satisfied)
.into_path()
.is_some()
} else {
false
}
}
/// Handles inventory manipulations that affect the loadout
pub fn handle_manipulate_loadout(
data: &JoinData<'_>,
@ -895,90 +972,30 @@ pub fn handle_manipulate_loadout(
}
},
InventoryAction::Collect(sprite_pos) => {
let sprite_pos_f32 = sprite_pos.map(|x| x as f32 + 0.5);
// Closure to check if distance between a point and the sprite is less than
// MAX_PICKUP_RANGE and the radius of the body
let sprite_range_check = |pos: Vec3<f32>| {
(sprite_pos_f32 - pos).magnitude_squared()
< (MAX_PICKUP_RANGE + data.body.max_radius()).powi(2)
};
// Checks if player's feet or head is near to sprite
let close_to_sprite = sprite_range_check(data.pos.0)
|| sprite_range_check(data.pos.0 + Vec3::new(0.0, 0.0, data.body.height()));
if close_to_sprite {
// First, get sprite data for position, if there is a sprite
use sprite_interact::SpriteInteractKind;
let sprite_chunk_pos = TerrainGrid::chunk_offs(sprite_pos);
let sprite_cfg = data
.terrain
.pos_chunk(sprite_pos)
.and_then(|chunk| chunk.meta().sprite_cfg_at(sprite_chunk_pos));
let sprite_at_pos = data
.terrain
.get(sprite_pos)
.ok()
.copied()
.and_then(|b| b.get_sprite());
// Checks if position has a collectible sprite as well as what sprite is at the
// position
let sprite_interact = sprite_at_pos.and_then(Option::<SpriteInteractKind>::from);
if let Some(sprite_interact) = sprite_interact {
// Do a check that a path can be found between sprite and entity
// interacting with sprite Use manhattan distance * 1.5 for number
// of iterations
let iters =
(3.0 * (sprite_pos_f32 - data.pos.0).map(|x| x.abs()).sum()) as usize;
// Heuristic compares manhattan distance of start and end pos
let heuristic = move |pos: &Vec3<i32>, _: &Vec3<i32>| {
(sprite_pos - pos).map(|x| x.abs()).sum() as f32
};
let mut astar = Astar::new(
iters,
data.pos.0.map(|x| x.floor() as i32),
BuildHasherDefault::<FxHasher64>::default(),
);
// Transition uses manhattan distance as the cost, with a slightly lower cost
// for z transitions
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
};
// Neighbors are all neighboring blocks that are air
let neighbors = |pos: &Vec3<i32>| {
const DIRS: [Vec3<i32>; 6] = [
Vec3::new(1, 0, 0),
Vec3::new(-1, 0, 0),
Vec3::new(0, 1, 0),
Vec3::new(0, -1, 0),
Vec3::new(0, 0, 1),
Vec3::new(0, 0, -1),
];
let pos = *pos;
DIRS.iter()
.map(move |dir| {
let dest = dir + pos;
(dest, transition(pos, dest))
})
.filter(|(pos, _)| {
data.terrain
.get(*pos)
.ok()
.map_or(false, |block| !block.is_filled())
})
};
// Pathing satisfied when it reaches the sprite position
let satisfied = |pos: &Vec3<i32>| *pos == sprite_pos;
let not_blocked_by_terrain = astar
.poll(iters, heuristic, neighbors, satisfied)
.into_path()
.is_some();
// First, get sprite data for position, if there is a sprite
let sprite_at_pos = data
.terrain
.get(sprite_pos)
.ok()
.copied()
.and_then(|b| b.get_sprite());
// Checks if position has a collectible sprite as well as what sprite is at the
// position
let sprite_interact =
sprite_at_pos.and_then(Option::<sprite_interact::SpriteInteractKind>::from);
if let Some(sprite_interact) = sprite_interact {
if can_reach_block(
data.pos.0,
sprite_pos,
MAX_PICKUP_RANGE,
data.body,
data.terrain,
) {
let sprite_chunk_pos = TerrainGrid::chunk_offs(sprite_pos);
let sprite_cfg = data
.terrain
.pos_chunk(sprite_pos)
.and_then(|chunk| chunk.meta().sprite_cfg_at(sprite_chunk_pos));
let required_item =
sprite_at_pos.and_then(|s| match s.unlock_condition(sprite_cfg.cloned()) {
UnlockKind::Free => None,
@ -997,38 +1014,32 @@ pub fn handle_manipulate_loadout(
.map(|slot| Some((item_id, slot, consume))),
None => Some(None),
};
if let Some(required_item) = has_required_items {
// If the sprite is collectible, enter the sprite interaction character
// state TODO: Handle cases for sprite being
// interactible, but not collectible (none currently
// exist)
let (buildup_duration, use_duration, recover_duration) =
sprite_interact.durations();
// If path can be found between entity interacting with sprite and entity, start
// interaction with sprite
if not_blocked_by_terrain {
if let Some(required_item) = has_required_items {
// If the sprite is collectible, enter the sprite interaction character
// state TODO: Handle cases for sprite being
// interactible, but not collectible (none currently
// exist)
let (buildup_duration, use_duration, recover_duration) =
sprite_interact.durations();
update.character =
CharacterState::SpriteInteract(sprite_interact::Data {
static_data: sprite_interact::StaticData {
buildup_duration,
use_duration,
recover_duration,
sprite_pos,
sprite_kind: sprite_interact,
was_wielded: data.character.is_wield(),
was_sneak: data.character.is_stealthy(),
required_item,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
})
} else {
output_events.emit_local(LocalEvent::CreateOutcome(
Outcome::FailedSpriteUnlock { pos: sprite_pos },
));
}
update.character = CharacterState::SpriteInteract(sprite_interact::Data {
static_data: sprite_interact::StaticData {
buildup_duration,
use_duration,
recover_duration,
sprite_pos,
sprite_kind: sprite_interact,
was_wielded: data.character.is_wield(),
was_sneak: data.character.is_stealthy(),
required_item,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,
})
} else {
output_events.emit_local(LocalEvent::CreateOutcome(
Outcome::FailedSpriteUnlock { pos: sprite_pos },
));
}
}
}

View File

@ -4,6 +4,7 @@ use crate::{
consts::FRIC_GROUND,
lottery::LootSpec,
make_case_elim, rtsim,
vol::FilledVox,
};
use num_derive::FromPrimitive;
use num_traits::FromPrimitive;
@ -117,6 +118,12 @@ pub struct Block {
attr: [u8; 3],
}
impl FilledVox for Block {
fn default_non_filled() -> Self { Block::air(SpriteKind::Empty) }
fn is_filled(&self) -> bool { self.kind.is_filled() }
}
impl Deref for Block {
type Target = BlockKind;
@ -420,6 +427,19 @@ impl Block {
self.collectible_id().is_some() && self.mine_tool().is_none()
}
#[inline]
pub fn is_mountable(&self) -> bool { self.mount_offset().is_some() }
/// Get the position and direction to mount this block if any.
pub fn mount_offset(&self) -> Option<(Vec3<f32>, Vec3<f32>)> {
self.get_sprite().and_then(|sprite| sprite.mount_offset())
}
pub fn is_controller(&self) -> bool {
self.get_sprite()
.map_or(false, |sprite| sprite.is_controller())
}
#[inline]
pub fn is_bonkable(&self) -> bool {
match self.get_sprite() {

View File

@ -12,6 +12,7 @@ use num_derive::FromPrimitive;
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, fmt};
use strum::EnumIter;
use vek::Vec3;
make_case_elim!(
sprite_kind,
@ -241,6 +242,8 @@ make_case_elim!(
KeyDoor = 0xD8,
CommonLockedChest = 0xD9,
RepairBench = 0xDA,
Helm = 0xDB,
DoorWide = 0xDC,
}
);
@ -374,6 +377,7 @@ impl SpriteKind {
SpriteKind::Bamboo => 9.0 / 11.0,
SpriteKind::MagicalBarrier => 3.0,
SpriteKind::MagicalSeal => 1.0,
SpriteKind::Helm => 1.7,
_ => return None,
})
}
@ -474,6 +478,44 @@ impl SpriteKind {
matches!(self.collectible_id(), Some(Some(LootSpec::LootTable(_))))
}
/// Get the position and direction to mount this sprite if any.
#[inline]
pub fn mount_offset(&self) -> Option<(Vec3<f32>, Vec3<f32>)> {
match self {
SpriteKind::ChairSingle | SpriteKind::ChairDouble | SpriteKind::Bench => Some((
Vec3 {
x: 0.0,
y: 0.0,
z: 0.5,
},
-Vec3::unit_y(),
)),
SpriteKind::Helm => Some((
Vec3 {
x: 0.0,
y: -0.6,
z: 0.2,
},
Vec3::unit_y(),
)),
_ => None,
}
}
#[inline]
pub fn is_mountable(&self) -> bool { self.mount_offset().is_some() }
#[inline]
pub fn is_controller(&self) -> bool { matches!(self, SpriteKind::Helm) }
#[inline]
pub fn is_door(&self) -> bool {
matches!(
self,
SpriteKind::Door | SpriteKind::DoorWide | SpriteKind::DoorDark
)
}
/// Which tool (if any) is needed to collect this sprite?
#[inline]
pub fn mine_tool(&self) -> Option<ToolKind> {
@ -603,6 +645,8 @@ impl SpriteKind {
| SpriteKind::Grave
| SpriteKind::Gravestone
| SpriteKind::MagicalBarrier
| SpriteKind::Helm
| SpriteKind::DoorWide,
)
}
}

View File

@ -5,6 +5,7 @@ use crate::{
vol::{BaseVol, ReadVol, SizedVol, WriteVol},
volumes::dyna::{Dyna, DynaError},
};
use dot_vox::DotVoxData;
use hashbrown::HashMap;
use serde::Deserialize;
use std::{num::NonZeroU8, sync::Arc};
@ -44,6 +45,12 @@ make_case_elim!(
}
);
// We can't derive this because of the `make_case_elim` macro.
#[allow(clippy::derivable_impls)]
impl Default for StructureBlock {
fn default() -> Self { StructureBlock::None }
}
#[derive(Debug)]
pub enum StructureError {
OutOfBounds,
@ -52,14 +59,14 @@ pub enum StructureError {
#[derive(Clone, Debug)]
pub struct Structure {
center: Vec3<i32>,
base: Arc<BaseStructure>,
base: Arc<BaseStructure<StructureBlock>>,
custom_indices: [Option<StructureBlock>; 256],
}
#[derive(Debug)]
struct BaseStructure {
vol: Dyna<Option<NonZeroU8>, ()>,
palette: [StructureBlock; 256],
pub(crate) struct BaseStructure<B> {
pub(crate) vol: Dyna<Option<NonZeroU8>, ()>,
pub(crate) palette: [B; 256],
}
pub struct StructuresGroup(Vec<Structure>);
@ -79,7 +86,9 @@ impl assets::Compound for StructuresGroup {
.0
.iter()
.map(|sp| {
let base = cache.load::<Arc<BaseStructure>>(&sp.specifier)?.cloned();
let base = cache
.load::<Arc<BaseStructure<StructureBlock>>>(&sp.specifier)?
.cloned();
Ok(Structure {
center: Vec3::from(sp.center),
base,
@ -138,43 +147,51 @@ impl ReadVol for Structure {
}
}
impl assets::Compound for BaseStructure {
pub(crate) fn load_base_structure<B: Default>(
dot_vox_data: &DotVoxData,
mut to_block: impl FnMut(Rgb<u8>) -> B,
) -> BaseStructure<B> {
let mut palette = std::array::from_fn(|_| B::default());
if let Some(model) = dot_vox_data.models.get(0) {
for (i, col) in dot_vox_data
.palette
.iter()
.map(|col| Rgb::new(col.r, col.g, col.b))
.enumerate()
{
palette[(i + 1).min(255)] = to_block(col);
}
let mut vol = Dyna::filled(
Vec3::new(model.size.x, model.size.y, model.size.z),
None,
(),
);
for voxel in &model.voxels {
let _ = vol.set(
Vec3::new(voxel.x, voxel.y, voxel.z).map(i32::from),
Some(NonZeroU8::new(voxel.i + 1).unwrap()),
);
}
BaseStructure { vol, palette }
} else {
BaseStructure {
vol: Dyna::filled(Vec3::zero(), None, ()),
palette,
}
}
}
impl assets::Compound for BaseStructure<StructureBlock> {
fn load(cache: assets::AnyCache, specifier: &assets::SharedString) -> Result<Self, BoxedError> {
let dot_vox_data = cache.load::<DotVoxAsset>(specifier)?.read();
let dot_vox_data = &dot_vox_data.0;
if let Some(model) = dot_vox_data.models.get(0) {
let mut palette = std::array::from_fn(|_| StructureBlock::None);
for (i, col) in dot_vox_data
.palette
.iter()
.map(|col| Rgb::new(col.r, col.g, col.b))
.enumerate()
{
palette[(i + 1).min(255)] = StructureBlock::Filled(BlockKind::Misc, col);
}
let mut vol = Dyna::filled(
Vec3::new(model.size.x, model.size.y, model.size.z),
None,
(),
);
for voxel in &model.voxels {
let _ = vol.set(
Vec3::new(voxel.x, voxel.y, voxel.z).map(i32::from),
Some(NonZeroU8::new(voxel.i + 1).unwrap()),
);
}
Ok(BaseStructure { vol, palette })
} else {
Ok(BaseStructure {
vol: Dyna::filled(Vec3::zero(), None, ()),
palette: std::array::from_fn(|_| StructureBlock::None),
})
}
Ok(load_base_structure(dot_vox_data, |col| {
StructureBlock::Filled(BlockKind::Misc, col)
}))
}
}

View File

@ -149,6 +149,16 @@ impl FindDist<Vec3<f32>> for Cylinder {
}
}
impl FindDist<Cylinder> for Vec3<f32> {
#[inline]
fn approx_in_range(self, other: Cylinder, range: f32) -> bool {
other.approx_in_range(self, range)
}
#[inline]
fn min_distance(self, other: Cylinder) -> f32 { other.min_distance(self) }
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -13,12 +13,12 @@ pub trait RectVolSize: Clone {
}
/// A voxel.
pub trait Vox: Sized + Clone + PartialEq {
fn empty() -> Self;
fn is_empty(&self) -> bool;
pub trait FilledVox: Sized + Clone + PartialEq {
fn default_non_filled() -> Self;
fn is_filled(&self) -> bool;
#[must_use]
fn or(self, other: Self) -> Self { if self.is_empty() { other } else { self } }
fn or(self, other: Self) -> Self { if self.is_filled() { self } else { other } }
}
/// A volume that contains voxel data.
@ -241,6 +241,7 @@ where
/// Convenience iterator type that can be used to quickly implement
/// `IntoPosIterator`.
#[derive(Clone)]
pub struct DefaultPosIterator {
current: Vec3<i32>,
begin: Vec2<i32>,
@ -288,6 +289,7 @@ impl Iterator for DefaultPosIterator {
/// Convenience iterator type that can be used to quickly implement
/// `IntoVolIterator`.
#[derive(Clone)]
pub struct DefaultVolIterator<'a, T: ReadVol> {
vol: &'a T,
pos_iter: DefaultPosIterator,

View File

@ -1,4 +1,4 @@
use crate::vol::{BaseVol, ReadVol, SizedVol, Vox};
use crate::vol::{BaseVol, FilledVox, ReadVol, SizedVol};
use vek::*;
pub struct Scaled<V> {
@ -13,7 +13,7 @@ impl<V: BaseVol> BaseVol for Scaled<V> {
impl<V: ReadVol> ReadVol for Scaled<V>
where
V::Vox: Vox,
V::Vox: FilledVox,
{
#[inline(always)]
fn get(&self, pos: Vec3<i32>) -> Result<&Self::Vox, Self::Error> {
@ -43,7 +43,7 @@ where
})
.flatten()
.map(|offs| self.inner.get(pos + offs))
.find(|vox| vox.as_ref().map(|v| !v.is_empty()).unwrap_or(false))
.find(|vox| vox.as_ref().map_or(false, |v| v.is_filled()))
.unwrap_or_else(|| self.inner.get(pos))
}
}

View File

@ -9,7 +9,7 @@ use common::{
comp,
event::{EventBus, LocalEvent, ServerEvent},
link::Is,
mounting::{Mount, Rider},
mounting::{Mount, Rider, VolumeRider, VolumeRiders},
outcome::Outcome,
region::RegionMap,
resources::{
@ -201,6 +201,7 @@ impl State {
ecs.register::<comp::Scale>();
ecs.register::<Is<Mount>>();
ecs.register::<Is<Rider>>();
ecs.register::<Is<VolumeRider>>();
ecs.register::<comp::Mass>();
ecs.register::<comp::Density>();
ecs.register::<comp::Collider>();
@ -260,6 +261,7 @@ impl State {
ecs.register::<comp::invite::Invite>();
ecs.register::<comp::invite::PendingInvites>();
ecs.register::<comp::Beam>();
ecs.register::<VolumeRiders>();
// Register synced resources used by the ECS.
ecs.insert(TimeOfDay(0.0));
@ -294,6 +296,7 @@ impl State {
ecs.insert(PhysicsMetrics::default());
ecs.insert(Trades::default());
ecs.insert(PlayerPhysicsSettings::default());
ecs.insert(VolumeRiders::default());
// Load plugins from asset directory
#[cfg(feature = "plugins")]

View File

@ -14,7 +14,7 @@ use common::{
},
event::{EventBus, LocalEvent, ServerEvent},
link::Is,
mounting::Rider,
mounting::{Rider, VolumeRider},
outcome::Outcome,
resources::{DeltaTime, Time},
states::{
@ -43,6 +43,7 @@ pub struct ReadData<'a> {
beams: ReadStorage<'a, Beam>,
uids: ReadStorage<'a, Uid>,
is_riders: ReadStorage<'a, Is<Rider>>,
is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
stats: ReadStorage<'a, Stats>,
skill_sets: ReadStorage<'a, SkillSet>,
active_abilities: ReadStorage<'a, ActiveAbilities>,
@ -207,6 +208,7 @@ impl<'a> System<'a> for Sys {
alignment: read_data.alignments.get(entity),
terrain: &read_data.terrain,
mount_data: read_data.is_riders.get(entity),
volume_mount_data: read_data.is_volume_riders.get(entity),
stance: read_data.stances.get(entity),
};

View File

@ -2,16 +2,17 @@ use common::{
comp::{
ability::Stance,
agent::{Sound, SoundKind},
Body, BuffChange, ControlEvent, Controller, Pos, Scale,
Body, BuffChange, Collider, ControlEvent, Controller, Pos, Scale,
},
event::{EventBus, ServerEvent},
terrain::TerrainGrid,
uid::UidAllocator,
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{
saveload::{Marker, MarkerAllocator},
shred::ResourceId,
Entities, Join, Read, ReadStorage, SystemData, World, WriteStorage,
Entities, Join, Read, ReadExpect, ReadStorage, SystemData, World, WriteStorage,
};
use vek::*;
@ -20,9 +21,11 @@ pub struct ReadData<'a> {
entities: Entities<'a>,
uid_allocator: Read<'a, UidAllocator>,
server_bus: Read<'a, EventBus<ServerEvent>>,
terrain_grid: ReadExpect<'a, TerrainGrid>,
positions: ReadStorage<'a, Pos>,
bodies: ReadStorage<'a, Body>,
scales: ReadStorage<'a, Scale>,
colliders: ReadStorage<'a, Collider>,
}
#[derive(Default)]
@ -53,6 +56,17 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::Mount(entity, mountee_entity));
}
},
ControlEvent::MountVolume(volume) => {
if let Some(block) = volume.get_block(
&read_data.terrain_grid,
&read_data.uid_allocator,
&read_data.colliders,
) {
if block.is_mountable() {
server_emitter.emit(ServerEvent::MountVolume(entity, volume));
}
}
},
ControlEvent::RemoveBuff(buff_id) => {
server_emitter.emit(ServerEvent::Buff {
entity,

View File

@ -1,14 +1,16 @@
use common::{
comp::{Body, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel},
comp::{Body, Collider, ControlAction, Controller, InputKind, Ori, Pos, Scale, Vel},
link::Is,
mounting::Mount,
mounting::{Mount, VolumeRider},
terrain::TerrainGrid,
uid::UidAllocator,
};
use common_ecs::{Job, Origin, Phase, System};
use specs::{
saveload::{Marker, MarkerAllocator},
Entities, Join, Read, ReadStorage, WriteStorage,
Entities, Join, Read, ReadExpect, ReadStorage, WriteStorage,
};
use tracing::error;
use vek::*;
/// This system is responsible for controlling mounts
@ -17,14 +19,17 @@ pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Read<'a, UidAllocator>,
ReadExpect<'a, TerrainGrid>,
Entities<'a>,
WriteStorage<'a, Controller>,
ReadStorage<'a, Is<Mount>>,
ReadStorage<'a, Is<VolumeRider>>,
WriteStorage<'a, Pos>,
WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>,
ReadStorage<'a, Body>,
ReadStorage<'a, Scale>,
ReadStorage<'a, Collider>,
);
const NAME: &'static str = "mount";
@ -35,14 +40,17 @@ impl<'a> System<'a> for Sys {
_job: &mut Job<Self>,
(
uid_allocator,
terrain,
entities,
mut controllers,
is_mounts,
is_volume_riders,
mut positions,
mut velocities,
mut orientations,
bodies,
scales,
colliders,
): Self::SystemData,
) {
// For each mount...
@ -84,5 +92,86 @@ impl<'a> System<'a> for Sys {
controller.actions = actions;
}
}
// For each volume rider.
for (entity, is_volume_rider) in (&entities, &is_volume_riders).join() {
if let Some((mut mat, _)) = is_volume_rider.pos.get_block_and_transform(
&terrain,
&uid_allocator,
|e| positions.get(e).copied().zip(orientations.get(e).copied()),
&colliders,
) {
let Some((mount_offset, mount_dir)) = is_volume_rider.block.mount_offset() else {
error!("Mounted on unmountable block");
continue;
};
if let Some(ori) = is_volume_rider.block.get_ori() {
mat *= Mat4::identity()
.translated_3d(mount_offset)
.rotated_z(std::f32::consts::PI * 0.25 * ori as f32)
.translated_3d(Vec3::new(0.5, 0.5, 0.0));
} else {
mat *= Mat4::identity().translated_3d(mount_offset + Vec3::new(0.5, 0.5, 0.0));
}
if let Some(pos) = positions.get_mut(entity) {
pos.0 = mat.mul_point(Vec3::zero());
}
if let Some(ori) = orientations.get_mut(entity) {
*ori = Ori::from_unnormalized_vec(mat.mul_direction(mount_dir))
.unwrap_or_default();
}
}
let v = match is_volume_rider.pos.kind {
common::mounting::Volume::Terrain => Vec3::zero(),
common::mounting::Volume::Entity(uid) => {
if let Some(v) = uid_allocator
.retrieve_entity_internal(uid.into())
.and_then(|e| velocities.get(e))
{
v.0
} else {
Vec3::zero()
}
},
};
if let Some(vel) = velocities.get_mut(entity) {
vel.0 = v;
}
let inputs = controllers.get_mut(entity).map(|c| {
let actions: Vec<_> = c
.actions
.drain_filter(|action| match action {
ControlAction::StartInput { input: i, .. }
| ControlAction::CancelInput(i) => {
matches!(i, InputKind::Jump | InputKind::Fly | InputKind::Roll)
},
_ => false,
})
.collect();
let inputs = c.inputs.clone();
(actions, inputs)
});
if is_volume_rider.block.is_controller() {
if let Some((actions, inputs)) = inputs {
match is_volume_rider.pos.kind {
common::mounting::Volume::Entity(uid) => {
if let Some(controller) = uid_allocator
.retrieve_entity_internal(uid.into())
.and_then(|e| controllers.get_mut(e))
{
controller.inputs = inputs;
controller.actions = actions;
}
},
common::mounting::Volume::Terrain => {},
}
}
}
}
}
}

View File

@ -9,7 +9,7 @@ use common::{
consts::{AIR_DENSITY, FRIC_GROUND, GRAVITY},
event::{EventBus, ServerEvent},
link::Is,
mounting::Rider,
mounting::{Rider, VolumeRider},
outcome::Outcome,
resources::DeltaTime,
states,
@ -122,6 +122,7 @@ pub struct PhysicsRead<'a> {
masses: ReadStorage<'a, Mass>,
colliders: ReadStorage<'a, Collider>,
is_ridings: ReadStorage<'a, Is<Rider>>,
is_volume_ridings: ReadStorage<'a, Is<VolumeRider>>,
projectiles: ReadStorage<'a, Projectile>,
char_states: ReadStorage<'a, CharacterState>,
bodies: ReadStorage<'a, Body>,
@ -335,6 +336,7 @@ impl<'a> PhysicsData<'a> {
&read.masses,
&read.colliders,
read.is_ridings.maybe(),
read.is_volume_ridings.maybe(),
read.stickies.maybe(),
read.immovables.maybe(),
&mut write.physics_states,
@ -359,6 +361,7 @@ impl<'a> PhysicsData<'a> {
mass,
collider,
is_riding,
is_volume_riding,
sticky,
immovable,
physics,
@ -491,7 +494,9 @@ impl<'a> PhysicsData<'a> {
mass: *mass_other,
},
vel,
is_riding.is_some() || other_is_riding_maybe.is_some(),
is_riding.is_some()
|| is_volume_riding.is_some()
|| other_is_riding_maybe.is_some(),
);
}
},
@ -546,11 +551,7 @@ impl<'a> PhysicsData<'a> {
)
.join()
{
let vol = match collider {
Collider::Voxel { id } => voxel_colliders_manifest.colliders.get(id),
Collider::Volume(vol) => Some(&**vol),
_ => None,
};
let vol = collider.get_vol(&voxel_colliders_manifest);
if let Some(vol) = vol {
let sphere = voxel_collider_bounding_sphere(vol, pos, ori);
@ -587,6 +588,7 @@ impl<'a> PhysicsData<'a> {
!&write.pos_vel_ori_defers, // This is the one we are adding
write.previous_phys_cache.mask(),
!&read.is_ridings,
!&read.is_volume_ridings,
)
.join()
.map(|t| (t.0, *t.2, *t.3, *t.4))
@ -619,6 +621,7 @@ impl<'a> PhysicsData<'a> {
&read.densities,
read.scales.maybe(),
!&read.is_ridings,
!&read.is_volume_ridings,
)
.par_join()
.for_each_init(
@ -638,6 +641,7 @@ impl<'a> PhysicsData<'a> {
density,
scale,
_,
_,
)| {
let in_loaded_chunk = read
.terrain
@ -749,6 +753,7 @@ impl<'a> PhysicsData<'a> {
&mut write.pos_vel_ori_defers,
previous_phys_cache,
!&read.is_ridings,
!&read.is_volume_ridings,
)
.par_join()
.filter(|tuple| tuple.3.is_voxel() == terrain_like_entities)
@ -772,6 +777,7 @@ impl<'a> PhysicsData<'a> {
pos_vel_ori_defer,
previous_cache,
_,
_,
)| {
let mut land_on_ground = None;
let mut outcomes = Vec::new();
@ -1061,13 +1067,8 @@ impl<'a> PhysicsData<'a> {
return;
}
let voxel_collider = match collider_other {
Collider::Voxel { id } => {
voxel_colliders_manifest.colliders.get(id)
},
Collider::Volume(vol) => Some(&**vol),
_ => None,
};
let voxel_collider =
collider_other.get_vol(&voxel_colliders_manifest);
// use bounding cylinder regardless of our collider
// TODO: extract point-terrain collision above to its own

View File

@ -10,7 +10,7 @@ use common::{
SkillSet, Stance, Stats, Vel,
},
link::Is,
mounting::{Mount, Rider},
mounting::{Mount, Rider, VolumeRider},
path::TraversalConfig,
resources::{DeltaTime, Time, TimeOfDay},
rtsim::{Actor, RtSimEntity},
@ -236,6 +236,7 @@ pub struct ReadData<'a> {
pub bodies: ReadStorage<'a, Body>,
pub is_mounts: ReadStorage<'a, Is<Mount>>,
pub is_riders: ReadStorage<'a, Is<Rider>>,
pub is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
pub time_of_day: Read<'a, TimeOfDay>,
pub light_emitter: ReadStorage<'a, LightEmitter>,
#[cfg(feature = "worldgen")]

View File

@ -40,7 +40,7 @@ use common::{
event::{EventBus, ServerEvent},
generation::{EntityConfig, EntityInfo},
link::Is,
mounting::Rider,
mounting::{Rider, VolumeRider},
npc::{self, get_npc_name},
outcome::Outcome,
parse_cmd_args,
@ -227,21 +227,47 @@ fn position_mut<T>(
server: &mut Server,
entity: EcsEntity,
descriptor: &str,
dismount_volume: Option<bool>,
f: impl for<'a> FnOnce(&'a mut comp::Pos) -> T,
) -> CmdResult<T> {
let entity = server
.state
.ecs()
.read_storage::<Is<Rider>>()
.get(entity)
.and_then(|is_rider| {
server
let entity = if dismount_volume.unwrap_or(true) {
server
.state
.ecs()
.write_storage::<Is<VolumeRider>>()
.remove(entity);
entity
} else {
server
.state
.read_storage::<Is<Rider>>()
.get(entity)
.and_then(|is_rider| {
server
.state
.ecs()
.read_resource::<UidAllocator>()
.retrieve_entity_internal(is_rider.mount.into())
})
.or(server
.state
.ecs()
.read_resource::<UidAllocator>()
.retrieve_entity_internal(is_rider.mount.into())
})
.unwrap_or(entity);
.read_storage::<Is<VolumeRider>>()
.get(entity)
.and_then(|volume_rider| {
Some(match volume_rider.pos.kind {
common::mounting::Volume::Terrain => {
Err("Tried to move the world.".to_string())
},
common::mounting::Volume::Entity(uid) => Ok(server
.state
.ecs()
.read_resource::<UidAllocator>()
.retrieve_entity_internal(uid.into())?),
})
})
.transpose()?)
.unwrap_or(entity)
};
let mut maybe_pos = None;
@ -829,8 +855,9 @@ fn handle_jump(
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
if let (Some(x), Some(y), Some(z)) = parse_cmd_args!(args, f32, f32, f32) {
position_mut(server, target, "target", |current_pos| {
if let (Some(x), Some(y), Some(z), dismount_volume) = parse_cmd_args!(args, f32, f32, f32, bool)
{
position_mut(server, target, "target", dismount_volume, |current_pos| {
current_pos.0 += Vec3::new(x, y, z)
})
} else {
@ -845,8 +872,9 @@ fn handle_goto(
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
if let (Some(x), Some(y), Some(z)) = parse_cmd_args!(args, f32, f32, f32) {
position_mut(server, target, "target", |current_pos| {
if let (Some(x), Some(y), Some(z), dismount_volume) = parse_cmd_args!(args, f32, f32, f32, bool)
{
position_mut(server, target, "target", dismount_volume, |current_pos| {
current_pos.0 = Vec3::new(x, y, z)
})
} else {
@ -864,7 +892,7 @@ fn handle_site(
action: &ServerChatCommand,
) -> CmdResult<()> {
#[cfg(feature = "worldgen")]
if let Some(dest_name) = parse_cmd_args!(args, String) {
if let (Some(dest_name), dismount_volume) = parse_cmd_args!(args, String, bool) {
let site = server
.world
.civs()
@ -881,7 +909,7 @@ fn handle_site(
false,
);
position_mut(server, target, "target", |current_pos| {
position_mut(server, target, "target", dismount_volume, |current_pos| {
current_pos.0 = site_pos
})
} else {
@ -906,7 +934,7 @@ fn handle_respawn(
.ok_or("No waypoint set")?
.get_pos();
position_mut(server, target, "target", |current_pos| {
position_mut(server, target, "target", Some(true), |current_pos| {
current_pos.0 = waypoint;
})
}
@ -1205,7 +1233,8 @@ fn handle_tp(
args: Vec<String>,
action: &ServerChatCommand,
) -> CmdResult<()> {
let player = if let Some(alias) = parse_cmd_args!(args, String) {
let (player, dismount_volume) = parse_cmd_args!(args, String, bool);
let player = if let Some(alias) = player {
find_alias(server.state.ecs(), &alias)?.0
} else if client != target {
client
@ -1213,7 +1242,7 @@ fn handle_tp(
return Err(action.help_string());
};
let player_pos = position(server, player, "player")?;
position_mut(server, target, "target", |target_pos| {
position_mut(server, target, "target", dismount_volume, |target_pos| {
*target_pos = player_pos
})
}
@ -1226,7 +1255,8 @@ fn handle_rtsim_tp(
action: &ServerChatCommand,
) -> CmdResult<()> {
use crate::rtsim::RtSim;
let pos = if let Some(id) = parse_cmd_args!(args, u32) {
let (npc_index, dismount_volume) = parse_cmd_args!(args, u32, bool);
let pos = if let Some(id) = npc_index {
// TODO: Take some other identifier than an integer to this command.
server
.state
@ -1242,7 +1272,7 @@ fn handle_rtsim_tp(
} else {
return Err(action.help_string());
};
position_mut(server, target, "target", |target_pos| {
position_mut(server, target, "target", dismount_volume, |target_pos| {
target_pos.0 = pos;
})
}
@ -1689,7 +1719,7 @@ fn handle_make_volume(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
_args: Vec<String>,
args: Vec<String>,
_action: &ServerChatCommand,
) -> CmdResult<()> {
use comp::body::ship::figuredata::VoxelCollider;
@ -1697,7 +1727,11 @@ fn handle_make_volume(
//let () = parse_args!(args);
let pos = position(server, target, "target")?;
let ship = comp::ship::Body::Volume;
let sz = Vec3::new(15, 15, 15);
let sz = parse_cmd_args!(args, u32).unwrap_or(15);
if !(1..=127).contains(&sz) {
return Err("Size has to be between 1 and 127.".to_string());
};
let sz = Vec3::broadcast(sz);
let collider = {
let terrain = server.state().terrain();
comp::Collider::Volume(Arc::new(VoxelCollider::from_fn(sz, |rpos| {
@ -1711,7 +1745,7 @@ fn handle_make_volume(
server
.state
.create_ship(
comp::Pos(pos.0 + Vec3::unit_z() * 50.0),
comp::Pos(pos.0 + Vec3::unit_z() * (50.0 + sz.z as f32 / 2.0)),
comp::Ori::default(),
ship,
move |_| collider,
@ -3930,7 +3964,7 @@ fn handle_location(
if let Some(name) = parse_cmd_args!(args, String) {
let loc = server.state.ecs().read_resource::<Locations>().get(&name);
match loc {
Ok(loc) => position_mut(server, target, "target", |target_pos| {
Ok(loc) => position_mut(server, target, "target", Some(true), |target_pos| {
target_pos.0 = loc;
}),
Err(e) => Err(e.to_string()),

View File

@ -9,16 +9,18 @@ use common::{
aura::{Aura, AuraKind, AuraTarget},
beam,
buff::{BuffCategory, BuffData, BuffKind, BuffSource},
ship::figuredata::VOXEL_COLLIDER_MANIFEST,
shockwave, Alignment, BehaviorCapability, Body, ItemDrops, LightEmitter, Object, Ori, Pos,
Projectile, TradingBehavior, Vel, WaypointArea,
},
event::{EventBus, NpcBuilder, UpdateCharacterMetadata},
mounting::Mounting,
mounting::{Mounting, Volume, VolumeMounting, VolumePos},
outcome::Outcome,
resources::{Secs, Time},
rtsim::RtSimVehicle,
uid::Uid,
util::Dir,
vol::IntoFullVolIterator,
ViewDistances,
};
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
@ -200,9 +202,30 @@ pub fn handle_create_ship(
driver: Option<NpcBuilder>,
passengers: Vec<NpcBuilder>,
) {
let mut entity = server
.state
.create_ship(pos, ori, ship, |ship| ship.make_collider());
let collider = ship.make_collider();
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
// TODO: Find better solution for this, maybe something like a serverside block
// of interests.
let (mut steering, mut seats) = {
let mut steering = Vec::new();
let mut seats = Vec::new();
for (pos, block) in collider
.get_vol(&voxel_colliders_manifest)
.iter()
.flat_map(|voxel_collider| voxel_collider.volume().full_vol_iter())
{
match (block.is_controller(), block.is_mountable()) {
(true, true) => steering.push((pos, *block)),
(false, true) => seats.push((pos, *block)),
_ => {},
}
}
(steering.into_iter(), seats.into_iter())
};
let mut entity = server.state.create_ship(pos, ori, ship, |_| collider);
/*
if let Some(mut agent) = agent {
let (kp, ki, kd) = pid_coefficients(&Body::Ship(ship));
@ -221,10 +244,26 @@ pub fn handle_create_ship(
let npc_entity = handle_create_npc(server, pos, driver);
let uids = server.state.ecs().read_storage::<Uid>();
if let (Some(rider_uid), Some(mount_uid)) =
(uids.get(npc_entity).copied(), uids.get(entity).copied())
{
drop(uids);
let (rider_uid, mount_uid) = uids
.get(npc_entity)
.copied()
.zip(uids.get(entity).copied())
.expect("Couldn't get Uid from newly created ship and npc");
drop(uids);
if let Some((steering_pos, steering_block)) = steering.next() {
server
.state
.link(VolumeMounting {
pos: VolumePos {
kind: Volume::Entity(mount_uid),
pos: steering_pos,
},
block: steering_block,
rider: rider_uid,
})
.expect("Failed to link driver to ship");
} else {
server
.state
.link(Mounting {
@ -232,13 +271,32 @@ pub fn handle_create_ship(
rider: rider_uid,
})
.expect("Failed to link driver to ship");
} else {
panic!("Couldn't get Uid from newly created ship and npc");
}
}
for passenger in passengers {
handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passenger);
let npc_entity = handle_create_npc(server, Pos(pos.0 + Vec3::unit_z() * 5.0), passenger);
if let Some((rider_pos, rider_block)) = seats.next() {
let uids = server.state.ecs().read_storage::<Uid>();
let (rider_uid, mount_uid) = uids
.get(npc_entity)
.copied()
.zip(uids.get(entity).copied())
.expect("Couldn't get Uid from newly created ship and npc");
drop(uids);
server
.state
.link(VolumeMounting {
pos: VolumePos {
kind: Volume::Entity(mount_uid),
pos: rider_pos,
},
block: rider_block,
rider: rider_uid,
})
.expect("Failed to link passanger to ship");
}
}
}

View File

@ -14,10 +14,10 @@ use common::{
tool::{AbilityMap, ToolKind},
Inventory, LootOwner, Pos, SkillGroupKind,
},
consts::{MAX_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
consts::{MAX_MOUNT_RANGE, MAX_SPRITE_MOUNT_RANGE, SOUND_TRAVEL_DIST_PER_VOLUME},
event::EventBus,
link::Is,
mounting::{Mount, Mounting, Rider},
mounting::{Mounting, Rider, VolumeMounting, VolumePos, VolumeRider},
outcome::Outcome,
terrain::{Block, SpriteKind},
uid::Uid,
@ -103,53 +103,83 @@ pub fn handle_npc_interaction(
pub fn handle_mount(server: &mut Server, rider: EcsEntity, mount: EcsEntity) {
let state = server.state_mut();
if state.ecs().read_storage::<Is<Rider>>().get(rider).is_none() {
let not_mounting_yet = state.ecs().read_storage::<Is<Mount>>().get(mount).is_none();
let within_range = {
let positions = state.ecs().read_storage::<Pos>();
within_mounting_range(positions.get(rider), positions.get(mount))
};
let within_range = || {
let positions = state.ecs().read_storage::<Pos>();
within_mounting_range(positions.get(rider), positions.get(mount))
};
let healths = state.ecs().read_storage::<comp::Health>();
let alive = |e| healths.get(e).map_or(true, |h| !h.is_dead);
if not_mounting_yet && within_range() && alive(rider) && alive(mount) {
let uids = state.ecs().read_storage::<Uid>();
if let (Some(rider_uid), Some(mount_uid)) =
(uids.get(rider).copied(), uids.get(mount).copied())
{
let is_pet = matches!(
state
.ecs()
.read_storage::<comp::Alignment>()
.get(mount),
Some(comp::Alignment::Owned(owner)) if *owner == rider_uid,
);
let can_ride = state
if within_range {
let uids = state.ecs().read_storage::<Uid>();
if let (Some(rider_uid), Some(mount_uid)) =
(uids.get(rider).copied(), uids.get(mount).copied())
{
let is_pet = matches!(
state
.ecs()
.read_storage()
.get(mount)
.map_or(false, |mount_body| {
is_mountable(mount_body, state.ecs().read_storage().get(rider))
});
.read_storage::<comp::Alignment>()
.get(mount),
Some(comp::Alignment::Owned(owner)) if *owner == rider_uid,
);
if is_pet && can_ride {
drop(uids);
drop(healths);
let _ = state.link(Mounting {
mount: mount_uid,
rider: rider_uid,
});
}
let can_ride = state
.ecs()
.read_storage()
.get(mount)
.map_or(false, |mount_body| {
is_mountable(mount_body, state.ecs().read_storage().get(rider))
});
if is_pet && can_ride {
drop(uids);
let _ = state.link(Mounting {
mount: mount_uid,
rider: rider_uid,
});
}
}
}
}
pub fn handle_mount_volume(server: &mut Server, rider: EcsEntity, volume_pos: VolumePos) {
let state = server.state_mut();
let block_transform = volume_pos.get_block_and_transform(
&state.terrain(),
&state.ecs().read_resource(),
|e| {
state
.read_storage()
.get(e)
.copied()
.zip(state.read_storage().get(e).copied())
},
&state.read_storage(),
);
if let Some((mat, block)) = block_transform
&& let Some(mount_offset) = block.mount_offset() {
let mount_pos = (mat * mount_offset.0.with_w(1.0)).xyz();
let within_range = {
let positions = state.ecs().read_storage::<Pos>();
positions.get(rider).map_or(false, |pos| pos.0.distance_squared(mount_pos) < MAX_SPRITE_MOUNT_RANGE.powi(2))
};
let maybe_uid = state.ecs().read_storage::<Uid>().get(rider).copied();
if let Some(rider) = maybe_uid && within_range {
let _ = state.link(VolumeMounting {
pos: volume_pos,
block,
rider,
});
}
}
}
pub fn handle_unmount(server: &mut Server, rider: EcsEntity) {
let state = server.state_mut();
state.ecs().write_storage::<Is<Rider>>().remove(rider);
state.ecs().write_storage::<Is<VolumeRider>>().remove(rider);
}
fn within_mounting_range(player_position: Option<&Pos>, mount_position: Option<&Pos>) -> bool {

View File

@ -13,6 +13,7 @@ use common::{
InventoryUpdate,
},
consts::MAX_PICKUP_RANGE,
mounting::VolumePos,
recipe::{
self, default_component_recipe_book, default_recipe_book, default_repair_recipe_book,
},
@ -745,27 +746,42 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
let ability_map = &state.ecs().read_resource::<AbilityMap>();
let msm = state.ecs().read_resource::<MaterialStatManifest>();
let get_craft_sprite = |state, sprite_pos: Option<Vec3<i32>>| {
let get_craft_sprite = |state, sprite_pos: Option<VolumePos>| {
sprite_pos
.filter(|pos| {
let entity_cylinder = get_cylinder(state, entity);
let in_range = within_pickup_range(entity_cylinder, || {
Some(find_dist::Cube {
min: pos.as_(),
side_length: 1.0,
})
pos.get_block_and_transform(
&state.terrain(),
&state.ecs().read_resource(),
|e| {
state
.read_storage()
.get(e)
.copied()
.zip(state.read_storage().get(e).copied())
},
&state.read_storage(),
)
.map(|(mat, _)| mat.mul_point(Vec3::broadcast(0.5)))
});
if !in_range {
debug!(
?entity_cylinder,
"Failed to craft recipe as not within range of required sprite, \
sprite pos: {}",
sprite pos: {:?}",
pos
);
}
in_range
})
.and_then(|pos| state.terrain().get(pos).ok().copied())
.and_then(|pos| {
pos.get_block(
&state.terrain(),
&state.ecs().read_resource(),
&state.read_storage(),
)
})
.and_then(|block| block.get_sprite())
};

View File

@ -1,5 +1,7 @@
use crate::{
events::interaction::handle_tame_pet, persistence::PersistedComponents, state_ext::StateExt,
events::interaction::{handle_mount_volume, handle_tame_pet},
persistence::PersistedComponents,
state_ext::StateExt,
Server,
};
use common::event::{EventBus, ServerEvent, ServerEventDiscriminants};
@ -136,6 +138,9 @@ impl Server {
handle_process_trade_action(self, entity, trade_id, action);
},
ServerEvent::Mount(mounter, mountee) => handle_mount(self, mounter, mountee),
ServerEvent::MountVolume(mounter, volume) => {
handle_mount_volume(self, mounter, volume)
},
ServerEvent::Unmount(mounter) => handle_unmount(self, mounter),
ServerEvent::Possess(possessor_uid, possesse_uid) => {
handle_possess(self, possessor_uid, possesse_uid)

View File

@ -23,7 +23,7 @@ use common::{
},
effect::Effect,
link::{Link, LinkHandle},
mounting::Mounting,
mounting::{Mounting, VolumeMounting},
resources::{Secs, Time, TimeOfDay},
rtsim::{Actor, RtSimEntity},
slowjob::SlowJobPool,
@ -1099,6 +1099,7 @@ impl StateExt for State {
}
maintain_link::<Mounting>(self);
maintain_link::<VolumeMounting>(self);
}
fn delete_entity_recorded(

View File

@ -12,6 +12,7 @@ use common::{
Controller, Health, InputKind, Scale,
},
event::{EventBus, ServerEvent},
mounting::Volume,
path::TraversalConfig,
};
use common_base::prof_span;
@ -69,6 +70,7 @@ impl<'a> System<'a> for Sys {
read_data.rtsim_entities.maybe(),
!&read_data.is_mounts,
read_data.is_riders.maybe(),
read_data.is_volume_riders.maybe(),
)
.par_join()
.for_each_init(
@ -93,6 +95,7 @@ impl<'a> System<'a> for Sys {
rtsim_entity,
_,
is_rider,
is_volume_rider,
)| {
let mut event_emitter = event_bus.emitter();
let mut rng = thread_rng();
@ -104,6 +107,16 @@ impl<'a> System<'a> for Sys {
.uid_allocator
.retrieve_entity_internal(is_rider.mount.into())
})
.or_else(|| {
is_volume_rider.and_then(|is_volume_rider| {
match is_volume_rider.pos.kind {
Volume::Terrain => None,
Volume::Entity(uid) => {
read_data.uid_allocator.retrieve_entity_internal(uid.into())
},
}
})
})
.unwrap_or(entity);
let moving_body = read_data.bodies.get(moving_entity);

View File

@ -8,7 +8,7 @@ use common::{
},
event::{EventBus, ServerEvent},
link::Is,
mounting::Rider,
mounting::{Rider, VolumeRider},
resources::{PlayerPhysicsSetting, PlayerPhysicsSettings},
slowjob::SlowJobPool,
terrain::TerrainGrid,
@ -52,6 +52,7 @@ impl Sys {
terrain: &ReadExpect<'_, TerrainGrid>,
can_build: &ReadStorage<'_, CanBuild>,
is_rider: &ReadStorage<'_, Is<Rider>>,
is_volume_rider: &ReadStorage<'_, Is<VolumeRider>>,
force_updates: &ReadStorage<'_, ForceUpdate>,
skill_set: &mut Option<Cow<'_, SkillSet>>,
healths: &ReadStorage<'_, Health>,
@ -126,6 +127,7 @@ impl Sys {
&& force_updates.get(entity).map_or(true, |force_update| force_update.counter() == force_counter)
&& healths.get(entity).map_or(true, |h| !h.is_dead)
&& is_rider.get(entity).is_none()
&& is_volume_rider.get(entity).is_none()
&& player_physics_setting
.as_ref()
.map_or(true, |s| s.client_authoritative())
@ -322,6 +324,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, CanBuild>,
ReadStorage<'a, ForceUpdate>,
ReadStorage<'a, Is<Rider>>,
ReadStorage<'a, Is<VolumeRider>>,
WriteStorage<'a, SkillSet>,
ReadStorage<'a, Health>,
Write<'a, BlockChange>,
@ -353,6 +356,7 @@ impl<'a> System<'a> for Sys {
can_build,
force_updates,
is_rider,
is_volume_rider,
mut skill_sets,
healths,
mut block_changes,
@ -430,6 +434,7 @@ impl<'a> System<'a> for Sys {
&terrain,
&can_build,
&is_rider,
&is_volume_rider,
&force_updates,
&mut skill_set,
&healths,

View File

@ -25,6 +25,7 @@ use common::{
slot::{InvSlotId, Slot},
Inventory,
},
mounting::VolumePos,
recipe::{ComponentKey, Recipe, RecipeInput},
terrain::SpriteKind,
};
@ -123,7 +124,7 @@ pub enum Event {
pub struct CraftingShow {
pub crafting_tab: CraftingTab,
pub crafting_search_key: Option<String>,
pub craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
pub craft_sprite: Option<(VolumePos, SpriteKind)>,
pub salvage: bool,
pub initialize_repair: bool,
// TODO: Maybe try to do something that doesn't need to allocate?

View File

@ -107,7 +107,7 @@ use common::{
},
consts::MAX_PICKUP_RANGE,
link::Is,
mounting::Mount,
mounting::{Mount, VolumePos},
outcome::Outcome,
resources::{Secs, Time},
slowjob::SlowJobPool,
@ -710,27 +710,27 @@ pub enum Event {
CraftRecipe {
recipe_name: String,
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
craft_sprite: Option<(VolumePos, SpriteKind)>,
amount: u32,
},
SalvageItem {
slot: InvSlotId,
salvage_pos: Vec3<i32>,
salvage_pos: VolumePos,
},
CraftModularWeapon {
primary_slot: InvSlotId,
secondary_slot: InvSlotId,
craft_sprite: Option<Vec3<i32>>,
craft_sprite: Option<VolumePos>,
},
CraftModularWeaponComponent {
toolkind: ToolKind,
material: InvSlotId,
modifier: Option<InvSlotId>,
craft_sprite: Option<Vec3<i32>>,
craft_sprite: Option<VolumePos>,
},
RepairItem {
item: Slot,
sprite_pos: Vec3<i32>,
sprite_pos: VolumePos,
},
InviteMember(Uid),
AcceptInvite,
@ -995,7 +995,7 @@ impl Show {
pub fn open_crafting_tab(
&mut self,
tab: CraftingTab,
craft_sprite: Option<(Vec3<i32>, SpriteKind)>,
craft_sprite: Option<(VolumePos, SpriteKind)>,
) {
self.selected_crafting_tab(tab);
self.crafting(true);
@ -1289,7 +1289,7 @@ pub struct Hud {
item_imgs: ItemImgs,
fonts: Fonts,
rot_imgs: ImgsRot,
failed_block_pickups: HashMap<Vec3<i32>, CollectFailedData>,
failed_block_pickups: HashMap<VolumePos, CollectFailedData>,
failed_entity_pickups: HashMap<EcsEntity, CollectFailedData>,
new_loot_messages: VecDeque<LootMessage>,
new_messages: VecDeque<comp::ChatMsg>,
@ -2040,7 +2040,13 @@ impl Hud {
}
// Render overtime for an interactable block
if let Some(Interactable::Block(block, pos, interaction)) = interactable {
if let Some(Interactable::Block(block, pos, interaction)) = interactable
&& let Some((mat, _)) = pos.get_block_and_transform(
&ecs.read_resource(),
&ecs.read_resource(),
|e| ecs.read_storage::<vcomp::Interpolated>().get(e).map(|interpolated| (comp::Pos(interpolated.pos), interpolated.ori)),
&ecs.read_storage(),
) {
let overitem_id = overitem_walker.next(
&mut self.ids.overitems,
&mut ui_widgets.widget_id_generator(),
@ -2050,7 +2056,8 @@ impl Hud {
active: true,
pickup_failed_pulse: self.failed_block_pickups.get(pos).cloned(),
};
let pos = pos.map(|e| e as f32 + 0.5);
let pos = mat.mul_point(Vec3::broadcast(0.5));
let over_pos = pos + Vec3::unit_z() * 0.7;
let interaction_text = || match interaction {
@ -2107,6 +2114,13 @@ impl Hud {
}
}
},
BlockInteraction::Mount => {
let key = match block.get_sprite() {
Some(SpriteKind::Helm) => "hud-steer",
_ => "hud-sit",
};
vec![(Some(GameInput::Mount), i18n.get_msg(key).to_string())]
},
};
// This is only done once per frame, so it's not a performance issue
@ -4325,7 +4339,7 @@ impl Hud {
}
}
pub fn add_failed_block_pickup(&mut self, pos: Vec3<i32>, reason: HudCollectFailedReason) {
pub fn add_failed_block_pickup(&mut self, pos: VolumePos, reason: HudCollectFailedReason) {
self.failed_block_pickups
.insert(pos, CollectFailedData::new(self.pulse, reason));
}
@ -4706,16 +4720,38 @@ impl Hud {
.handle_event(conrod_core::event::Input::Text("\t".to_string()));
}
// Stop selecting a sprite to perform crafting with when out of range
// Stop selecting a sprite to perform crafting with when out of range or sprite
// has been removed
self.show.crafting_fields.craft_sprite =
self.show.crafting_fields.craft_sprite.filter(|(pos, _)| {
self.show.crafting
&& if let Some(player_pos) = client.position() {
pos.map(|e| e as f32 + 0.5).distance(player_pos) < MAX_PICKUP_RANGE
} else {
false
}
});
self.show
.crafting_fields
.craft_sprite
.filter(|(pos, sprite)| {
self.show.crafting
&& if let Some(player_pos) = client.position() {
pos.get_block_and_transform(
&client.state().terrain(),
&client.state().ecs().read_resource(),
|e| {
client
.state()
.read_storage::<vcomp::Interpolated>()
.get(e)
.map(|interpolated| {
(comp::Pos(interpolated.pos), interpolated.ori)
})
},
&client.state().read_storage(),
)
.map_or(false, |(mat, block)| {
block.get_sprite() == Some(*sprite)
&& mat.mul_point(Vec3::broadcast(0.5)).distance(player_pos)
< MAX_PICKUP_RANGE
})
} else {
false
}
});
// Optimization: skip maintaining UI when it's off.
if !self.show.ui {
@ -5130,6 +5166,7 @@ pub fn get_sprite_desc(sprite: SpriteKind, localized_strings: &Localization) ->
| SpriteKind::DungeonChest3
| SpriteKind::DungeonChest4
| SpriteKind::DungeonChest5 => "common-sprite-chest",
SpriteKind::ChairSingle | SpriteKind::ChairDouble => "common-sprite-chair",
sprite => return Some(Cow::Owned(format!("{:?}", sprite))),
};
Some(localized_strings.get_msg(i18n_key))

View File

@ -1,6 +1,7 @@
use crate::{
mesh::{
greedy::{self, GreedyConfig, GreedyMesh},
terrain::FaceKind,
MeshGen,
},
render::{Mesh, ParticleVertex, SpriteVertex, TerrainVertex},
@ -8,7 +9,8 @@ use crate::{
};
use common::{
figure::Cell,
vol::{BaseVol, ReadVol, SizedVol, Vox},
terrain::Block,
vol::{BaseVol, FilledVox, ReadVol, SizedVol},
};
use core::convert::TryFrom;
use vek::*;
@ -16,7 +18,7 @@ use vek::*;
// /// NOTE: bone_idx must be in [0, 15] (may be bumped to [0, 31] at some
// /// point).
// TODO: this function name...
pub fn generate_mesh_base_vol_terrain<'a: 'b, 'b, V: 'a>(
pub fn generate_mesh_base_vol_figure<'a: 'b, 'b, V: 'a>(
vol: V,
(greedy, opaque_mesh, offs, scale, bone_idx): (
&'b mut GreedyMesh<'a>,
@ -54,19 +56,14 @@ where
let draw_delta = lower_bound;
let get_light = |vol: &mut V, pos: Vec3<i32>| {
if vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true) {
1.0
} else {
0.0
}
vol.get(pos).map_or(true, |vox| !vox.is_filled()) as i32 as f32
};
let get_glow = |_vol: &mut V, _pos: Vec3<i32>| 0.0;
let get_opacity = |vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_empty());
let get_opacity =
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| !vox.is_filled());
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
should_draw_greedy(pos, delta, uv, |vox| {
vol.get(vox)
.map(|vox| *vox)
.unwrap_or_else(|_| Cell::empty())
vol.get(vox).map(|vox| *vox).unwrap_or_else(|_| Cell::Empty)
})
};
let create_opaque = |atlas_pos, pos, norm| {
@ -115,6 +112,114 @@ where
(Mesh::new(), Mesh::new(), Mesh::new(), bounds)
}
// /// NOTE: bone_idx must be in [0, 15] (may be bumped to [0, 31] at some
// /// point).
// TODO: this function name...
pub fn generate_mesh_base_vol_terrain<'a: 'b, 'b, V: 'a>(
vol: V,
(greedy, opaque_mesh, offs, scale, bone_idx): (
&'b mut GreedyMesh<'a>,
&'b mut Mesh<TerrainVertex>,
Vec3<f32>,
Vec3<f32>,
u8,
),
) -> MeshGen<TerrainVertex, TerrainVertex, TerrainVertex, math::Aabb<f32>>
where
V: BaseVol<Vox = Block> + ReadVol + SizedVol,
{
assert!(bone_idx <= 15, "Bone index for figures must be in [0, 15]");
let max_size = greedy.max_size();
// NOTE: Required because we steal two bits from the normal in the shadow uint
// in order to store the bone index. The two bits are instead taken out
// of the atlas coordinates, which is why we "only" allow 1 << 15 per
// coordinate instead of 1 << 16.
assert!(max_size.reduce_max() < 1 << 15);
let lower_bound = vol.lower_bound();
let upper_bound = vol.upper_bound();
assert!(
lower_bound.x <= upper_bound.x
&& lower_bound.y <= upper_bound.y
&& lower_bound.z <= upper_bound.z
);
// NOTE: Figure sizes should be no more than 512 along each axis.
let greedy_size = upper_bound - lower_bound + 1;
assert!(greedy_size.x <= 512 && greedy_size.y <= 512 && greedy_size.z <= 512);
// NOTE: Cast to usize is safe because of previous check, since all values fit
// into u16 which is safe to cast to usize.
let greedy_size = greedy_size.as_::<usize>();
let greedy_size_cross = greedy_size;
let draw_delta = lower_bound;
let get_light =
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_fluid()) as i32 as f32;
let get_ao = |vol: &mut V, pos: Vec3<i32>| {
vol.get(pos).map_or(false, |vox| vox.is_opaque()) as i32 as f32
};
let get_glow = |vol: &mut V, pos: Vec3<i32>| {
vol.get(pos)
.ok()
.and_then(|vox| vox.get_glow())
.unwrap_or(0) as f32
/ 255.0
};
let get_opacity = |vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_fluid());
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, _uv| {
super::terrain::should_draw_greedy(pos, delta, |vox| {
vol.get(vox)
.map(|vox| *vox)
.unwrap_or_else(|_| Block::empty())
})
};
let create_opaque = |atlas_pos, pos, norm| {
TerrainVertex::new_figure(atlas_pos, (pos + offs) * scale, norm, bone_idx)
};
greedy.push(GreedyConfig {
data: vol,
draw_delta,
greedy_size,
greedy_size_cross,
get_ao,
get_light,
get_glow,
get_opacity,
should_draw,
push_quad: |atlas_origin, dim, origin, draw_dim, norm, meta: &FaceKind| match meta {
FaceKind::Opaque(meta) => {
opaque_mesh.push_quad(greedy::create_quad(
atlas_origin,
dim,
origin,
draw_dim,
norm,
meta,
|atlas_pos, pos, norm, &_meta| create_opaque(atlas_pos, pos, norm),
));
},
FaceKind::Fluid => {},
},
make_face_texel: |vol: &mut V, pos, light, _, _| {
let block = vol.get(pos).ok();
let glowy = block.map(|c| c.get_glow().is_some()).unwrap_or_default();
let col = block
.and_then(|vox| vox.get_color())
.unwrap_or_else(Rgb::zero);
TerrainVertex::make_col_light_figure(light, glowy, false, col)
},
});
let bounds = math::Aabb {
// NOTE: Casts are safe since lower_bound and upper_bound both fit in a i16.
min: math::Vec3::from((lower_bound.as_::<f32>() + offs) * scale),
max: math::Vec3::from((upper_bound.as_::<f32>() + offs) * scale),
}
.made_valid();
(Mesh::new(), Mesh::new(), Mesh::new(), bounds)
}
pub fn generate_mesh_base_vol_sprite<'a: 'b, 'b, V: 'a>(
vol: V,
(greedy, opaque_mesh, vertical_stripes): (
@ -160,13 +265,13 @@ where
let (flat, flat_get) = {
let (w, h, d) = (greedy_size + 2).into_tuple();
let flat = {
let mut flat = vec![Cell::empty(); (w * h * d) as usize];
let mut flat = vec![Cell::Empty; (w * h * d) as usize];
let mut i = 0;
for x in -1..greedy_size.x + 1 {
for y in -1..greedy_size.y + 1 {
for z in -1..greedy_size.z + 1 {
let wpos = lower_bound + Vec3::new(x, y, z);
let block = vol.get(wpos).map(|b| *b).unwrap_or_else(|_| Cell::empty());
let block = vol.get(wpos).map(|b| *b).unwrap_or_else(|_| Cell::Empty);
flat[i] = block;
i += 1;
}
@ -193,18 +298,13 @@ where
let greedy_size_cross = greedy_size;
let draw_delta = Vec3::new(1, 1, 1);
let get_light = move |flat: &mut _, pos: Vec3<i32>| {
if flat_get(flat, pos).is_empty() {
1.0
} else {
0.0
}
};
let get_light =
move |flat: &mut _, pos: Vec3<i32>| !flat_get(flat, pos).is_filled() as i32 as f32;
let get_glow = |_flat: &mut _, _pos: Vec3<i32>| 0.0;
let get_color = move |flat: &mut _, pos: Vec3<i32>| {
flat_get(flat, pos).get_color().unwrap_or_else(Rgb::zero)
};
let get_opacity = move |flat: &mut _, pos: Vec3<i32>| flat_get(flat, pos).is_empty();
let get_opacity = move |flat: &mut _, pos: Vec3<i32>| !flat_get(flat, pos).is_filled();
let should_draw = move |flat: &mut _, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
should_draw_greedy_ao(vertical_stripes, pos, delta, uv, |vox| flat_get(flat, vox))
};
@ -281,11 +381,7 @@ where
let draw_delta = lower_bound;
let get_light = |vol: &mut V, pos: Vec3<i32>| {
if vol.get(pos).map(|vox| vox.is_empty()).unwrap_or(true) {
1.0
} else {
0.0
}
vol.get(pos).map_or(true, |vox| !vox.is_filled()) as i32 as f32
};
let get_glow = |_vol: &mut V, _pos: Vec3<i32>| 0.0;
let get_color = |vol: &mut V, pos: Vec3<i32>| {
@ -294,12 +390,11 @@ where
.and_then(|vox| vox.get_color())
.unwrap_or_else(Rgb::zero)
};
let get_opacity = |vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| vox.is_empty());
let get_opacity =
|vol: &mut V, pos: Vec3<i32>| vol.get(pos).map_or(true, |vox| !vox.is_filled());
let should_draw = |vol: &mut V, pos: Vec3<i32>, delta: Vec3<i32>, uv| {
should_draw_greedy(pos, delta, uv, |vox| {
vol.get(vox)
.map(|vox| *vox)
.unwrap_or_else(|_| Cell::empty())
vol.get(vox).map(|vox| *vox).unwrap_or_else(|_| Cell::Empty)
})
};
let create_opaque = |_atlas_pos, pos: Vec3<f32>, norm| ParticleVertex::new(pos, norm);
@ -342,8 +437,8 @@ fn should_draw_greedy(
) -> Option<(bool, /* u8 */ ())> {
let from = flat_get(pos - delta);
let to = flat_get(pos);
let from_opaque = !from.is_empty();
if from_opaque != to.is_empty() {
let from_opaque = from.is_filled();
if from_opaque != !to.is_filled() {
None
} else {
// If going from transparent to opaque, backward facing; otherwise, forward
@ -361,8 +456,8 @@ fn should_draw_greedy_ao(
) -> Option<(bool, bool)> {
let from = flat_get(pos - delta);
let to = flat_get(pos);
let from_opaque = !from.is_empty();
if from_opaque != to.is_empty() {
let from_opaque = from.is_filled();
if from_opaque != !to.is_filled() {
None
} else {
let faces_forward = from_opaque;

View File

@ -20,7 +20,7 @@ use tracing::error;
use vek::*;
#[derive(Clone, Copy, PartialEq)]
enum FaceKind {
pub enum FaceKind {
/// Opaque face that is facing something non-opaque; either
/// water (Opaque(true)) or something else (Opaque(false)).
Opaque(bool),
@ -537,7 +537,7 @@ pub fn generate_mesh<'a>(
/// NOTE: Make sure to reflect any changes to how meshing is performanced in
/// [scene::terrain::Terrain::skip_remesh].
fn should_draw_greedy(
pub fn should_draw_greedy(
pos: Vec3<i32>,
delta: Vec3<i32>,
flat_get: impl Fn(Vec3<i32>) -> Block,

View File

@ -137,17 +137,25 @@ impl VertexTrait for Vertex {
#[derive(Copy, Clone, Debug, Zeroable, Pod)]
// TODO: new function and private fields??
pub struct Locals {
model_offs: [f32; 3],
load_time: f32,
model_mat: [f32; 16],
atlas_offs: [i32; 4],
load_time: f32,
_dummy: [f32; 3],
}
impl Locals {
pub fn new(model_offs: Vec3<f32>, atlas_offs: Vec2<u32>, load_time: f32) -> Self {
pub fn new(
model_offs: Vec3<f32>,
ori: Quaternion<f32>,
atlas_offs: Vec2<u32>,
load_time: f32,
) -> Self {
let mat = Mat4::from(ori).translated_3d(model_offs);
Self {
model_offs: model_offs.into_array(),
model_mat: mat.into_col_array(),
load_time,
atlas_offs: Vec4::new(atlas_offs.x as i32, atlas_offs.y as i32, 0, 0).into_array(),
_dummy: [0.0; 3],
}
}
}
@ -155,9 +163,10 @@ impl Locals {
impl Default for Locals {
fn default() -> Self {
Self {
model_offs: [0.0; 3],
model_mat: Mat4::identity().into_col_array(),
load_time: 0.0,
atlas_offs: [0; 4],
_dummy: [0.0; 3],
}
}
}

View File

@ -1266,13 +1266,10 @@ impl Renderer {
}
/// Create a new set of instances with the provided values.
pub fn create_instances<T: Copy + bytemuck::Pod>(
&mut self,
vals: &[T],
) -> Result<Instances<T>, RenderError> {
pub fn create_instances<T: Copy + bytemuck::Pod>(&mut self, vals: &[T]) -> Instances<T> {
let mut instances = Instances::new(&self.device, vals.len());
instances.update(&self.queue, vals, 0);
Ok(instances)
instances
}
/// Ensure that the quad index buffer is large enough for a quad vertex

View File

@ -1,3 +1,5 @@
use crate::render::Bound;
use super::{
super::{
buffer::Buffer,
@ -1113,9 +1115,9 @@ pub struct SpriteDrawer<'pass_ref, 'pass: 'pass_ref> {
}
impl<'pass_ref, 'pass: 'pass_ref> SpriteDrawer<'pass_ref, 'pass> {
pub fn draw<'data: 'pass>(
pub fn draw<'data: 'pass, T>(
&mut self,
terrain_locals: &'data terrain::BoundLocals,
terrain_locals: &'data Bound<T>,
instances: &'data Instances<sprite::Instance>,
alt_indices: &'data AltIndices,
culling_mode: CullingMode,

View File

@ -1,8 +1,21 @@
use super::{load::BodySpec, FigureModelEntry};
use super::{
load::{BodySpec, ShipBoneMeshes},
FigureModelEntry, ModelEntry, TerrainModelEntry,
};
use crate::{
mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_terrain},
render::{BoneMeshes, ColLightInfo, FigureModel, Mesh, Renderer, TerrainVertex},
scene::camera::CameraMode,
mesh::{
greedy::GreedyMesh,
segment::{generate_mesh_base_vol_figure, generate_mesh_base_vol_terrain},
},
render::{
BoneMeshes, ColLightInfo, FigureModel, Instances, Mesh, Renderer, SpriteInstance,
TerrainVertex,
},
scene::{
camera::CameraMode,
terrain::{get_sprite_instances, BlocksOfInterest, SPRITE_LOD_LEVELS},
Terrain,
},
};
use anim::Skeleton;
use common::{
@ -15,36 +28,58 @@ use common::{
item::{item_key::ItemKey, modular, Item, ItemDefinitionId},
CharacterState,
},
figure::Segment,
figure::{Segment, TerrainSegment},
slowjob::SlowJobPool,
vol::BaseVol,
vol::{BaseVol, IntoVolIterator, ReadVol},
};
use core::{hash::Hash, ops::Range};
use crossbeam_utils::atomic;
use hashbrown::{hash_map::Entry, HashMap};
use serde::Deserialize;
use std::sync::Arc;
use std::{array::from_fn, sync::Arc};
use vek::*;
/// A type produced by mesh worker threads corresponding to the information
/// needed to mesh figures.
struct MeshWorkerResponse<const N: usize> {
pub struct MeshWorkerResponse<const N: usize> {
col_light: ColLightInfo,
opaque: Mesh<TerrainVertex>,
bounds: anim::vek::Aabb<f32>,
vertex_range: [Range<u32>; N],
}
/// A type produced by mesh worker threads corresponding to the information
/// needed to mesh figures.
pub struct TerrainMeshWorkerResponse<const N: usize> {
col_light: ColLightInfo,
opaque: Mesh<TerrainVertex>,
bounds: anim::vek::Aabb<f32>,
vertex_range: [Range<u32>; N],
sprite_instances: [Vec<SpriteInstance>; SPRITE_LOD_LEVELS],
blocks_of_interest: BlocksOfInterest,
blocks_offset: Vec3<f32>,
}
/// NOTE: To test this cell for validity, we currently first use
/// Arc::get_mut(), and then only if that succeeds do we call AtomicCell::take.
/// This way, we avoid all atomic updates for the fast path read in the "not yet
/// updated" case (though it would be faster without weak pointers); since once
/// it's updated, we switch from `Pending` to `Done`, this is only suboptimal
/// for one frame.
type MeshWorkerCell<const N: usize> = atomic::AtomicCell<Option<MeshWorkerResponse<N>>>;
pub type MeshWorkerCell<const N: usize> = atomic::AtomicCell<Option<MeshWorkerResponse<N>>>;
pub type TerrainMeshWorkerCell<const N: usize> =
atomic::AtomicCell<Option<TerrainMeshWorkerResponse<N>>>;
pub trait ModelEntryFuture<const N: usize> {
type ModelEntry: ModelEntry;
fn into_done(self) -> Option<Self::ModelEntry>;
fn get_done(&self) -> Option<&Self::ModelEntry>;
}
/// A future FigureModelEntryLod.
enum FigureModelEntryFuture<const N: usize> {
pub enum FigureModelEntryFuture<const N: usize> {
/// We can poll the future to see whether the figure model is ready.
// TODO: See if we can find away to either get rid of this Arc, or reuse Arcs across different
// figures. Updates to uvth for thread pool shared storage might obviate this requirement.
@ -53,9 +88,56 @@ enum FigureModelEntryFuture<const N: usize> {
Done(FigureModelEntry<N>),
}
impl<const N: usize> ModelEntryFuture<N> for FigureModelEntryFuture<N> {
type ModelEntry = FigureModelEntry<N>;
fn into_done(self) -> Option<Self::ModelEntry> {
match self {
Self::Pending(_) => None,
Self::Done(d) => Some(d),
}
}
fn get_done(&self) -> Option<&Self::ModelEntry> {
match self {
Self::Pending(_) => None,
Self::Done(d) => Some(d),
}
}
}
/// A future TerrainModelEntryLod.
pub enum TerrainModelEntryFuture<const N: usize> {
/// We can poll the future to see whether the figure model is ready.
// TODO: See if we can find away to either get rid of this Arc, or reuse Arcs across different
// figures. Updates to uvth for thread pool shared storage might obviate this requirement.
Pending(Arc<TerrainMeshWorkerCell<N>>),
/// Stores the already-meshed model.
Done(TerrainModelEntry<N>),
}
impl<const N: usize> ModelEntryFuture<N> for TerrainModelEntryFuture<N> {
type ModelEntry = TerrainModelEntry<N>;
fn into_done(self) -> Option<Self::ModelEntry> {
match self {
Self::Pending(_) => None,
Self::Done(d) => Some(d),
}
}
fn get_done(&self) -> Option<&Self::ModelEntry> {
match self {
Self::Pending(_) => None,
Self::Done(d) => Some(d),
}
}
}
const LOD_COUNT: usize = 3;
type FigureModelEntryLod<'b> = Option<&'b FigureModelEntry<LOD_COUNT>>;
type TerrainModelEntryLod<'b> = Option<&'b TerrainModelEntry<LOD_COUNT>>;
#[derive(Clone, Eq, Hash, PartialEq)]
/// TODO: merge item_key and extra field into an enum
@ -199,7 +281,16 @@ where
Skel: Skeleton,
Skel::Body: BodySpec,
{
models: HashMap<FigureKey<Skel::Body>, ((FigureModelEntryFuture<LOD_COUNT>, Skel::Attr), u64)>,
models: HashMap<
FigureKey<Skel::Body>,
(
(
<Skel::Body as BodySpec>::ModelEntryFuture<LOD_COUNT>,
Skel::Attr,
),
u64,
),
>,
manifests: <Skel::Body as BodySpec>::Manifests,
watcher: ReloadWatcher,
}
@ -240,7 +331,11 @@ where
camera_mode: CameraMode,
character_state: Option<&CharacterState>,
item_key: Option<ItemKey>,
) -> FigureModelEntryLod<'b> {
) -> Option<
&'b <<Skel::Body as BodySpec>::ModelEntryFuture<LOD_COUNT> as ModelEntryFuture<
LOD_COUNT,
>>::ModelEntry,
> {
// TODO: Use raw entries to avoid lots of allocation (among other things).
let key = FigureKey {
body,
@ -254,13 +349,44 @@ where
}),
};
if let Some(((FigureModelEntryFuture::Done(model), _), _)) = self.models.get(&key) {
if let Some(model) = self.models.get(&key).and_then(|d| d.0.0.get_done()) {
Some(model)
} else {
None
}
}
pub fn clear_models(&mut self) { self.models.clear(); }
pub fn clean(&mut self, col_lights: &mut super::FigureColLights, tick: u64)
where
<Skel::Body as BodySpec>::Spec: Clone,
{
// TODO: Don't hard-code this.
if tick % 60 == 0 {
self.models.retain(|_, ((model_entry, _), last_used)| {
// Wait about a minute at 60 fps before invalidating old models.
let delta = 60 * 60;
let alive = *last_used + delta > tick;
if !alive {
if let Some(model_entry) = model_entry.get_done() {
col_lights.atlas.deallocate(model_entry.allocation().id);
}
}
alive
});
}
}
}
impl<Skel: Skeleton> FigureModelCache<Skel>
where
Skel::Body: BodySpec<
BoneMesh = super::load::BoneMeshes,
ModelEntryFuture<LOD_COUNT> = FigureModelEntryFuture<LOD_COUNT>,
> + Eq
+ Hash,
{
#[allow(clippy::too_many_arguments)]
pub fn get_or_create_model<'c>(
&'c mut self,
@ -408,7 +534,7 @@ where
offset: Vec3<f32>,
bone_idx: u8,
) -> BoneMeshes {
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
let (opaque, _, _, bounds) = generate_mesh_base_vol_figure(
segment,
(greedy, opaque_mesh, offset, Vec3::one(), bone_idx),
);
@ -423,7 +549,7 @@ where
bone_idx: u8,
) -> BoneMeshes {
let lod_scale = 0.6;
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
let (opaque, _, _, bounds) = generate_mesh_base_vol_figure(
segment.scaled_by(Vec3::broadcast(lod_scale)),
(
greedy,
@ -444,7 +570,7 @@ where
bone_idx: u8,
) -> BoneMeshes {
let lod_scale = 0.3;
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
let (opaque, _, _, bounds) = generate_mesh_base_vol_figure(
segment.scaled_by(Vec3::broadcast(lod_scale)),
(
greedy,
@ -479,26 +605,322 @@ where
},
}
}
}
pub fn clear_models(&mut self) { self.models.clear(); }
pub fn clean(&mut self, col_lights: &mut super::FigureColLights, tick: u64)
impl<Skel: Skeleton> FigureModelCache<Skel>
where
Skel::Body: BodySpec<
BoneMesh = ShipBoneMeshes,
ModelEntryFuture<LOD_COUNT> = TerrainModelEntryFuture<LOD_COUNT>,
> + Eq
+ Hash,
{
#[allow(clippy::too_many_arguments)]
pub fn get_or_create_terrain_model<'c>(
&'c mut self,
renderer: &mut Renderer,
col_lights: &mut super::FigureColLights,
body: Skel::Body,
extra: <Skel::Body as BodySpec>::Extra,
tick: u64,
slow_jobs: &SlowJobPool,
terrain: &Terrain,
) -> (TerrainModelEntryLod<'c>, &'c Skel::Attr)
where
<Skel::Body as BodySpec>::Spec: Clone,
for<'a> &'a Skel::Body: Into<Skel::Attr>,
Skel::Body: Clone + Send + Sync + 'static,
<Skel::Body as BodySpec>::Spec: Send + Sync + 'static,
{
// TODO: Don't hard-code this.
if tick % 60 == 0 {
self.models.retain(|_, ((model_entry, _), last_used)| {
// Wait about a minute at 60 fps before invalidating old models.
let delta = 60 * 60;
let alive = *last_used + delta > tick;
if !alive {
if let FigureModelEntryFuture::Done(model_entry) = model_entry {
col_lights.atlas.deallocate(model_entry.allocation.id);
let skeleton_attr = (&body).into();
let key = FigureKey {
body,
item_key: None,
extra: None,
};
// TODO: Use raw entries to avoid significant performance overhead.
match self.models.entry(key) {
Entry::Occupied(o) => {
let ((model, skel), last_used) = o.into_mut();
*last_used = tick;
(
match model {
TerrainModelEntryFuture::Pending(recv) => {
if let Some(TerrainMeshWorkerResponse {
col_light,
opaque,
bounds,
vertex_range,
sprite_instances,
blocks_of_interest,
blocks_offset,
}) = Arc::get_mut(recv).take().and_then(|cell| cell.take())
{
let model_entry = col_lights.create_terrain(
renderer,
col_light,
(opaque, bounds),
vertex_range,
sprite_instances,
blocks_of_interest,
blocks_offset,
);
*model = TerrainModelEntryFuture::Done(model_entry);
// NOTE: Borrow checker isn't smart enough to figure this out.
if let TerrainModelEntryFuture::Done(model) = model {
Some(model)
} else {
unreachable!();
}
} else {
None
}
},
TerrainModelEntryFuture::Done(model) => Some(model),
},
skel,
)
},
Entry::Vacant(v) => {
let key = v.key().clone();
let slot = Arc::new(atomic::AtomicCell::new(None));
let manifests = self.manifests.clone();
let sprite_data = Arc::clone(&terrain.sprite_data);
let sprite_config = Arc::clone(&terrain.sprite_config);
let slot_ = Arc::clone(&slot);
slow_jobs.spawn("FIGURE_MESHING", move || {
// First, load all the base vertex data.
let meshes =
<Skel::Body as BodySpec>::bone_meshes(&key, &manifests, extra);
// Then, set up meshing context.
let mut greedy = FigureModel::make_greedy();
let mut opaque = Mesh::<TerrainVertex>::new();
// Choose the most conservative bounds for any LOD model.
let mut figure_bounds = anim::vek::Aabb {
min: anim::vek::Vec3::zero(),
max: anim::vek::Vec3::zero(),
};
// Meshes all bone models for this figure using the given mesh generation
// function, attaching it to the current greedy mesher and opaque vertex
// list. Returns the vertex bounds of the meshed model within the opaque
// mesh.
let mut make_model = |generate_mesh: for<'a, 'b> fn(
&mut GreedyMesh<'a>,
&'b mut _,
&'a _,
_,
_,
)
-> _| {
let vertex_start = opaque.vertices().len();
meshes
.iter()
.enumerate()
// NOTE: Cast to u8 is safe because i < 16.
.filter_map(|(i, bm)| bm.as_ref().map(|bm| (i as u8, bm)))
.for_each(|(i, (segment, offset))| {
// Generate this mesh.
let (_opaque_mesh, bounds) = generate_mesh(&mut greedy, &mut opaque, segment, *offset, i);
// Update the figure bounds to the largest granularity seen so far
// (NOTE: this is more than a little imperfect).
//
// FIXME: Maybe use the default bone position in the idle animation
// to figure this out instead?
figure_bounds.expand_to_contain(bounds);
});
// NOTE: vertex_start and vertex_end *should* fit in a u32, by the
// following logic:
//
// Our new figure maximum is constrained to at most 2^8 × 2^8 × 2^8.
// This uses at most 24 bits to store every vertex exactly once.
// Greedy meshing can store each vertex in up to 3 quads, we have 3
// greedy models, and we store 1.5x the vertex count, so the maximum
// total space a model can take up is 3 * 3 * 1.5 * 2^24; rounding
// up to 4 * 4 * 2^24 gets us to 2^28, which clearly still fits in a
// u32.
//
// (We could also, though we prefer not to, reason backwards from the
// maximum figure texture size of 2^15 × 2^15, also fits in a u32; we
// can also see that, since we can have at most one texture entry per
// vertex, any texture atlas of size 2^14 × 2^14 or higher should be
// able to store data for any figure. So the only reason we would fail
// here would be if the user's computer could not store a texture large
// enough to fit all the LOD models for the figure, not for fundamental
// reasons related to fitting in a u32).
//
// Therefore, these casts are safe.
vertex_start as u32..opaque.vertices().len() as u32
};
fn generate_mesh<'a>(
greedy: &mut GreedyMesh<'a>,
opaque_mesh: &mut Mesh<TerrainVertex>,
segment: &'a TerrainSegment,
offset: Vec3<f32>,
bone_idx: u8,
) -> BoneMeshes {
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
segment,
(greedy, opaque_mesh, offset, Vec3::one(), bone_idx),
);
(opaque, bounds)
}
}
alive
});
fn generate_mesh_lod_mid<'a>(
greedy: &mut GreedyMesh<'a>,
opaque_mesh: &mut Mesh<TerrainVertex>,
segment: &'a TerrainSegment,
offset: Vec3<f32>,
bone_idx: u8,
) -> BoneMeshes {
let lod_scale = 0.6;
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
segment.scaled_by(Vec3::broadcast(lod_scale)),
(
greedy,
opaque_mesh,
offset * lod_scale,
Vec3::one() / lod_scale,
bone_idx,
),
);
(opaque, bounds)
}
fn generate_mesh_lod_low<'a>(
greedy: &mut GreedyMesh<'a>,
opaque_mesh: &mut Mesh<TerrainVertex>,
segment: &'a TerrainSegment,
offset: Vec3<f32>,
bone_idx: u8,
) -> BoneMeshes {
let lod_scale = 0.3;
let (opaque, _, _, bounds) = generate_mesh_base_vol_terrain(
segment.scaled_by(Vec3::broadcast(lod_scale)),
(
greedy,
opaque_mesh,
offset * lod_scale,
Vec3::one() / lod_scale,
bone_idx,
),
);
(opaque, bounds)
}
let models = [
make_model(generate_mesh),
make_model(generate_mesh_lod_mid),
make_model(generate_mesh_lod_low),
];
let (dyna, offset) = &meshes[0].as_ref().unwrap();
let block_iter = dyna.vol_iter(Vec3::zero(), dyna.sz.as_()).map(|(pos, block)| (pos, *block));
slot_.store(Some(TerrainMeshWorkerResponse {
col_light: greedy.finalize(),
opaque,
bounds: figure_bounds,
vertex_range: models,
sprite_instances: {
let mut instances = from_fn(|_| Vec::new());
get_sprite_instances(
&mut instances,
|lod, instance, _| {
lod.push(instance);
},
block_iter.clone().map(|(pos, block)| (pos.as_() + *offset, block)),
|p| p.as_(),
|_| 1.0,
|pos| dyna.get(pos).ok().and_then(|block| block.get_glow()).map(|glow| glow as f32 / 255.0).unwrap_or(0.0),
&sprite_data,
&sprite_config,
);
instances
},
blocks_of_interest: BlocksOfInterest::from_blocks(block_iter, 0.0, 10.0, 0.0),
blocks_offset: *offset,
}));
});
let skel = &(v
.insert((
(TerrainModelEntryFuture::Pending(slot), skeleton_attr),
tick,
))
.0)
.1;
(None, skel)
},
}
}
pub fn get_blocks_of_interest(
&self,
body: Skel::Body,
) -> Option<(&BlocksOfInterest, Vec3<f32>)> {
let key = FigureKey {
body,
item_key: None,
extra: None,
};
self.models.get(&key).and_then(|((model, _), _)| {
let TerrainModelEntryFuture::Done(model) = model else {
return None;
};
Some((&model.blocks_of_interest, model.blocks_offset))
})
}
pub fn get_sprites(
&self,
body: Skel::Body,
) -> Option<&[Instances<SpriteInstance>; SPRITE_LOD_LEVELS]> {
let key = FigureKey {
body,
item_key: None,
extra: None,
};
self.models.get(&key).and_then(|((model, _), _)| {
let TerrainModelEntryFuture::Done(model) = model else {
return None;
};
Some(&model.sprite_instances)
})
}
/*
pub fn update_terrain_locals(
&mut self,
renderer: &mut Renderer,
entity: Entity,
body: Skel::Body,
pos: Vec3<f32>,
ori: Quaternion<f32>,
) {
let key = FigureKey {
body,
item_key: None,
extra: None,
};
if let Some(model) = self.models.get_mut(&key).and_then(|((model, _), _)| {
if let TerrainModelEntryFuture::Done(model) = model {
Some(model)
} else {
None
}
}) {
renderer.update_consts(&mut *model.terrain_locals, &[TerrainLocals::new(
pos,
ori,
Vec2::zero(),
0.0,
)])
}
}
*/
}

View File

@ -1,4 +1,6 @@
use super::cache::{FigureKey, ToolKey};
use super::cache::{
FigureKey, FigureModelEntryFuture, ModelEntryFuture, TerrainModelEntryFuture, ToolKey,
};
use common::{
assets::{self, AssetExt, AssetHandle, DotVoxAsset, ReloadWatcher, Ron},
comp::{
@ -19,12 +21,14 @@ use common::{
quadruped_small::{self, BodyType as QSBodyType, Species as QSSpecies},
ship::{
self,
figuredata::{ShipCentralSubSpec, ShipSpec},
figuredata::{ShipSpec, VoxelCollider},
},
theropod::{self, BodyType as TBodyType, Species as TSpecies},
},
figure::{Cell, DynaUnionizer, MatCell, MatSegment, Material, Segment},
vol::{IntoFullPosIterator, ReadVol, Vox},
terrain::Block,
vol::{IntoFullPosIterator, ReadVol},
volumes::dyna::Dyna,
};
use hashbrown::HashMap;
use serde::{Deserialize, Deserializer};
@ -108,6 +112,8 @@ pub trait BodySpec: Sized {
/// place it behind an [`Arc`].
type Manifests: Send + Sync + Clone;
type Extra: Send + Sync;
type BoneMesh;
type ModelEntryFuture<const N: usize>: ModelEntryFuture<N>;
/// Initialize all the specifications for this Body.
fn load_spec() -> Result<Self::Manifests, assets::Error>;
@ -126,7 +132,7 @@ pub trait BodySpec: Sized {
key: &FigureKey<Self>,
manifests: &Self::Manifests,
extra: Self::Extra,
) -> [Option<BoneMeshes>; anim::MAX_BONE_COUNT];
) -> [Option<Self::BoneMesh>; anim::MAX_BONE_COUNT];
}
macro_rules! make_vox_spec {
@ -152,6 +158,8 @@ macro_rules! make_vox_spec {
type Spec = $Spec;
type Manifests = AssetHandle<Self::Spec>;
type Extra = ();
type BoneMesh = BoneMeshes;
type ModelEntryFuture<const N: usize> = FigureModelEntryFuture<N>;
fn load_spec() -> Result<Self::Manifests, assets::Error> {
Self::Spec::load("")
@ -349,7 +357,7 @@ impl HumHeadSpec {
.maybe_add(beard)
.maybe_add(accessory)
.maybe_add(helmet)
.unify_with(|v| if v.is_hollow() { Cell::empty() } else { v });
.unify_with(|v| if v.is_hollow() { Cell::Empty } else { v });
(
head,
Vec3::from(spec.offset) + origin_offset.map(|e| e as f32 * -1.0),
@ -5258,30 +5266,31 @@ fn segment_center(segment: &Segment) -> Option<Vec3<f32>> {
}
}
fn mesh_ship_bone<K: fmt::Debug + Eq + Hash, V, F: Fn(&V) -> &ShipCentralSubSpec>(
pub type ShipBoneMeshes = (Dyna<Block, ()>, Vec3<f32>);
fn mesh_ship_bone<'a, K: fmt::Debug + Eq + Hash, V, F: Fn(&V) -> Option<&'a VoxelCollider>>(
map: &HashMap<K, V>,
obj: &K,
f: F,
) -> BoneMeshes {
) -> Option<ShipBoneMeshes> {
let spec = match map.get(obj) {
Some(spec) => spec,
None => {
error!("No specification exists for {:?}", obj);
return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5));
return None;
},
};
let bone = f(spec);
let central = graceful_load_segment_fullspec(
&format!("common.voxel.{}", &bone.central.0),
bone.model_index,
);
(central, Vec3::from(bone.offset))
bone.map(|bone| (bone.volume().clone(), bone.translation))
}
impl BodySpec for ship::Body {
type BoneMesh = ShipBoneMeshes;
type Extra = ();
type Manifests = AssetHandle<Self::Spec>;
type ModelEntryFuture<const N: usize> = TerrainModelEntryFuture<N>;
type Spec = ShipSpec;
fn load_spec() -> Result<Self::Manifests, assets::Error> { Self::Spec::load("") }
@ -5292,14 +5301,15 @@ impl BodySpec for ship::Body {
FigureKey { body, .. }: &FigureKey<Self>,
manifests: &Self::Manifests,
_: Self::Extra,
) -> [Option<BoneMeshes>; anim::MAX_BONE_COUNT] {
let spec = &*manifests.read();
let map = &(spec.central.read().0).0;
) -> [Option<Self::BoneMesh>; anim::MAX_BONE_COUNT] {
let spec = manifests.read();
let spec = &*spec;
let map = &spec.central.read().0.0;
[
Some(mesh_ship_bone(map, body, |spec| &spec.bone0)),
Some(mesh_ship_bone(map, body, |spec| &spec.bone1)),
Some(mesh_ship_bone(map, body, |spec| &spec.bone2)),
Some(mesh_ship_bone(map, body, |spec| &spec.bone3)),
mesh_ship_bone(map, body, |ship| spec.colliders.get(&ship.bone0.central.0)),
mesh_ship_bone(map, body, |ship| spec.colliders.get(&ship.bone1.central.0)),
mesh_ship_bone(map, body, |ship| spec.colliders.get(&ship.bone2.central.0)),
mesh_ship_bone(map, body, |ship| spec.colliders.get(&ship.bone3.central.0)),
None,
None,
None,

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,9 @@
use super::{
cache::FigureKey,
load::{BodySpec, BoneMeshes},
cache::{FigureKey, TerrainModelEntryFuture},
load::{BodySpec, ShipBoneMeshes},
EcsEntity,
};
use common::{
assets,
comp::ship::figuredata::VoxelCollider,
figure::{Cell, Segment},
vol::ReadVol,
};
use common::{assets, comp::ship::figuredata::VoxelCollider};
use std::{convert::TryFrom, sync::Arc};
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
@ -46,9 +41,7 @@ impl anim::Skeleton for VolumeKey {
buf: &mut [anim::FigureBoneData; anim::MAX_BONE_COUNT],
_: Self::Body,
) -> anim::Offsets {
let scale_mat = anim::vek::Mat4::scaling_3d(1.0 / 11.0);
let bone = base_mat * scale_mat;
let bone = base_mat;
*(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [
anim::make_bone(bone),
@ -68,8 +61,10 @@ impl anim::Skeleton for VolumeKey {
}
impl BodySpec for VolumeKey {
type BoneMesh = ShipBoneMeshes;
type Extra = Arc<VoxelCollider>;
type Manifests = ();
type ModelEntryFuture<const N: usize> = TerrainModelEntryFuture<N>;
type Spec = ();
fn load_spec() -> Result<Self::Manifests, assets::Error> { Ok(()) }
@ -82,16 +77,11 @@ impl BodySpec for VolumeKey {
_: &FigureKey<Self>,
_: &Self::Manifests,
collider: Self::Extra,
) -> [Option<BoneMeshes>; anim::MAX_BONE_COUNT] {
) -> [Option<Self::BoneMesh>; anim::MAX_BONE_COUNT] {
println!("Generating segment...");
[
Some((
Segment::from_fn(collider.volume().sz, (), |pos| {
match collider.volume().get(pos).unwrap().get_color() {
Some(col) => Cell::new(col, false, false, false),
None => Cell::Empty,
}
}),
collider.volume().clone(),
-collider.volume().sz.map(|e| e as f32) / 2.0,
)),
None,

View File

@ -138,9 +138,7 @@ impl Lod {
.into_iter()
.map(|(kind, instances)| {
(kind, ObjectGroup {
instances: renderer
.create_instances(&instances)
.expect("Renderer error?!"),
instances: renderer.create_instances(&instances),
z_range: z_range.clone(),
frustum_last_plane_index: 0,
visible: false,

View File

@ -31,7 +31,7 @@ use crate::{
use client::Client;
use common::{
calendar::Calendar,
comp,
comp::{self, ship::figuredata::VOXEL_COLLIDER_MANIFEST},
outcome::Outcome,
resources::DeltaTime,
terrain::{BlockKind, TerrainChunk, TerrainGrid},
@ -655,13 +655,19 @@ impl Scene {
lights.clear();
// Maintain the particles.
self.particle_mgr
.maintain(renderer, scene_data, &self.terrain, lights);
self.particle_mgr.maintain(
renderer,
scene_data,
&self.terrain,
&self.figure_mgr,
lights,
);
// Maintain the trails.
self.trail_mgr.maintain(renderer, scene_data);
// Update light constants
let max_light_dist = loaded_distance.powi(2) + LIGHT_DIST_RADIUS;
lights.extend(
(
&scene_data.state.ecs().read_storage::<comp::Pos>(),
@ -684,8 +690,7 @@ impl Scene {
.filter(|(pos, _, light_anim, h)| {
light_anim.col != Rgb::zero()
&& light_anim.strength > 0.0
&& pos.0.distance_squared(viewpoint_pos)
< loaded_distance.powi(2) + LIGHT_DIST_RADIUS
&& pos.0.distance_squared(viewpoint_pos) < max_light_dist
&& h.map_or(true, |h| !h.is_dead)
})
.map(|(pos, interpolated, light_anim, _)| {
@ -699,6 +704,54 @@ impl Scene {
.map(|el| el.light.with_strength((el.fadeout)(el.timeout))),
),
);
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
let figure_mgr = &self.figure_mgr;
lights.extend(
(
&scene_data.state.ecs().entities(),
&scene_data
.state
.read_storage::<crate::ecs::comp::Interpolated>(),
&scene_data.state.read_storage::<comp::Body>(),
&scene_data.state.read_storage::<comp::Collider>(),
)
.join()
.filter_map(|(entity, interpolated, body, collider)| {
let vol = collider.get_vol(&voxel_colliders_manifest)?;
let (blocks_of_interest, offset) =
figure_mgr.get_blocks_of_interest(entity, body, Some(collider))?;
let mat = Mat4::from(interpolated.ori.to_quat())
.translated_3d(interpolated.pos)
* Mat4::translation_3d(offset);
let p = mat.inverted().mul_point(viewpoint_pos);
let aabb = Aabb {
min: Vec3::zero(),
max: vol.volume().sz.as_(),
};
if aabb.contains_point(p) || aabb.distance_to_point(p) < max_light_dist {
Some(
blocks_of_interest
.lights
.iter()
.map(move |(block_offset, level)| {
let wpos = mat.mul_point(block_offset.as_() + 0.5);
(wpos, level)
})
.filter(move |(wpos, _)| {
wpos.distance_squared(viewpoint_pos) < max_light_dist
})
.map(|(wpos, level)| {
Light::new(wpos, Rgb::white(), *level as f32 / 7.0)
}),
)
} else {
None
}
})
.flatten(),
);
lights.sort_by_key(|light| light.get_pos().distance_squared(viewpoint_pos) as i32);
lights.truncate(MAX_LIGHT_COUNT);
renderer.update_consts(&mut self.data.lights, lights);
@ -1347,14 +1400,28 @@ impl Scene {
// Render the skybox.
first_pass.draw_skybox(&self.skybox.model);
// Draws translucent terrain and sprites
self.terrain.render_translucent(
&mut first_pass,
// Draws sprites
let mut sprite_drawer = first_pass.draw_sprites(
&self.terrain.sprite_globals,
&self.terrain.sprite_col_lights,
);
self.figure_mgr.render_sprites(
&mut sprite_drawer,
state,
cam_pos,
scene_data.sprite_render_distance,
);
self.terrain.render_sprites(
&mut sprite_drawer,
focus_pos,
cam_pos,
scene_data.sprite_render_distance,
culling_mode,
);
drop(sprite_drawer);
// Draws translucent
self.terrain.render_translucent(&mut first_pass, focus_pos);
// Render particle effects.
self.particle_mgr

View File

@ -1,4 +1,4 @@
use super::{terrain::BlocksOfInterest, SceneData, Terrain};
use super::{terrain::BlocksOfInterest, FigureMgr, SceneData, Terrain};
use crate::{
ecs::comp::Interpolated,
mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_particle},
@ -400,6 +400,7 @@ impl ParticleMgr {
renderer: &mut Renderer,
scene_data: &SceneData,
terrain: &Terrain<TerrainChunk>,
figure_mgr: &FigureMgr,
lights: &mut Vec<Light>,
) {
span!(_guard, "maintain", "ParticleMgr::maintain");
@ -415,7 +416,7 @@ impl ParticleMgr {
self.maintain_body_particles(scene_data);
self.maintain_char_state_particles(scene_data);
self.maintain_beam_particles(scene_data, lights);
self.maintain_block_particles(scene_data, terrain);
self.maintain_block_particles(scene_data, terrain, figure_mgr);
self.maintain_shockwave_particles(scene_data);
self.maintain_aura_particles(scene_data);
self.maintain_buff_particles(scene_data);
@ -1434,6 +1435,7 @@ impl ParticleMgr {
&mut self,
scene_data: &SceneData,
terrain: &Terrain<TerrainChunk>,
figure_mgr: &FigureMgr,
) {
span!(
_guard,
@ -1533,6 +1535,7 @@ impl ParticleMgr {
},
];
let ecs = scene_data.state.ecs();
let mut rng = thread_rng();
for particles in particles.iter() {
if !(particles.cond)(scene_data) {
@ -1564,6 +1567,46 @@ impl ParticleMgr {
})
});
}
for (entity, body, interpolated, collider) in (
&ecs.entities(),
&ecs.read_storage::<comp::Body>(),
&ecs.read_storage::<crate::ecs::comp::Interpolated>(),
ecs.read_storage::<comp::Collider>().maybe(),
)
.join()
{
if let Some((blocks_of_interest, offset)) =
figure_mgr.get_blocks_of_interest(entity, body, collider)
{
let mat = Mat4::from(interpolated.ori.to_quat())
.translated_3d(interpolated.pos)
* Mat4::translation_3d(offset);
let blocks = (particles.blocks)(blocks_of_interest);
let avg_particles = dt * blocks.len() as f32 * particles.rate;
let particle_count = avg_particles.trunc() as usize
+ (rng.gen::<f32>() < avg_particles.fract()) as usize;
self.particles
.resize_with(self.particles.len() + particle_count, || {
let rel_pos = blocks
.choose(&mut rng)
.copied()
.unwrap()
.map(|e: i32| e as f32 + rng.gen::<f32>()); // Can't fail
let wpos = mat.mul_point(rel_pos);
Particle::new(
Duration::from_secs_f32(particles.lifetime),
time,
particles.mode,
wpos,
)
})
}
}
}
// smoke is more complex as it comes with varying rate and color
{
@ -1945,9 +1988,7 @@ impl ParticleMgr {
.collect::<Vec<ParticleInstance>>();
// TODO: optimise buffer writes
let gpu_instances = renderer
.create_instances(&all_cpu_instances)
.expect("Failed to upload particle instances to the GPU!");
let gpu_instances = renderer.create_instances(&all_cpu_instances);
self.instances = gpu_instances;
}
@ -1972,9 +2013,7 @@ impl ParticleMgr {
fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
let empty_vec = Vec::new();
renderer
.create_instances(&empty_vec)
.expect("Failed to upload particle instances to the GPU!")
renderer.create_instances(&empty_vec)
}
const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";

View File

@ -1,5 +1,5 @@
use crate::{
mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_terrain},
mesh::{greedy::GreedyMesh, segment::generate_mesh_base_vol_figure},
render::{
create_skybox_mesh, BoneMeshes, Consts, FigureModel, FirstPassDrawer, GlobalModel, Globals,
GlobalsBindGroup, Light, LodData, Mesh, Model, PointLightMatrix, RainOcclusionLocals,
@ -34,6 +34,8 @@ use common::{
use vek::*;
use winit::event::MouseButton;
use super::figure::{ModelEntry, ModelEntryRef};
struct VoidVol;
impl BaseVol for VoidVol {
type Error = ();
@ -51,7 +53,7 @@ fn generate_mesh(
bone_idx: u8,
) -> BoneMeshes {
let (opaque, _, /* shadow */ _, bounds) =
generate_mesh_base_vol_terrain(segment, (greedy, mesh, offset, Vec3::one(), bone_idx));
generate_mesh_base_vol_figure(segment, (greedy, mesh, offset, Vec3::one(), bone_idx));
(opaque /* , shadow */, bounds)
}
@ -385,14 +387,22 @@ impl Scene {
if let Some((model, figure_state)) = model.zip(self.figure_state.as_ref()) {
if let Some(lod) = model.lod_model(0) {
figure_drawer.draw(lod, figure_state.bound(), self.col_lights.texture(model));
figure_drawer.draw(
lod,
figure_state.bound(),
self.col_lights.texture(ModelEntryRef::Figure(model)),
);
}
}
}
if let Some((model, state)) = &self.backdrop {
if let Some(lod) = model.lod_model(0) {
figure_drawer.draw(lod, state.bound(), self.col_lights.texture(model));
figure_drawer.draw(
lod,
state.bound(),
self.col_lights.texture(ModelEntryRef::Figure(model)),
);
}
}
drop(figure_drawer);

View File

@ -11,9 +11,9 @@ use crate::{
render::{
pipelines::{self, ColLights},
AltIndices, ColLightInfo, CullingMode, FirstPassDrawer, FluidVertex, GlobalModel,
Instances, LodData, Mesh, Model, RenderError, Renderer, SpriteGlobalsBindGroup,
SpriteInstance, SpriteVertex, SpriteVerts, TerrainLocals, TerrainShadowDrawer,
TerrainVertex, SPRITE_VERT_PAGE_SIZE,
Instances, LodData, Mesh, Model, RenderError, Renderer, SpriteDrawer,
SpriteGlobalsBindGroup, SpriteInstance, SpriteVertex, SpriteVerts, TerrainLocals,
TerrainShadowDrawer, TerrainVertex, SPRITE_VERT_PAGE_SIZE,
},
};
@ -45,7 +45,7 @@ use treeculler::{BVol, Frustum, AABB};
use vek::*;
const SPRITE_SCALE: Vec3<f32> = Vec3::new(1.0 / 11.0, 1.0 / 11.0, 1.0 / 11.0);
const SPRITE_LOD_LEVELS: usize = 5;
pub const SPRITE_LOD_LEVELS: usize = 5;
// For rain occlusion we only need to render the closest chunks.
/// How many chunks are maximally rendered for rain occlusion.
@ -182,7 +182,7 @@ struct SpriteConfig<Model> {
/// NOTE: Model is an asset path to the appropriate sprite .vox model.
#[derive(Deserialize)]
#[serde(try_from = "HashMap<SpriteKind, Option<SpriteConfig<String>>>")]
struct SpriteSpec([Option<SpriteConfig<String>>; 256]);
pub struct SpriteSpec([Option<SpriteConfig<String>>; 256]);
impl SpriteSpec {
fn get(&self, kind: SpriteKind) -> Option<&SpriteConfig<String>> {
@ -240,6 +240,76 @@ impl assets::Asset for SpriteSpec {
const EXTENSION: &'static str = "ron";
}
pub fn get_sprite_instances<'a, I: 'a>(
lod_levels: &'a mut [I; SPRITE_LOD_LEVELS],
set_instance: impl Fn(&mut I, SpriteInstance, Vec3<i32>),
blocks: impl Iterator<Item = (Vec3<f32>, Block)>,
mut to_wpos: impl FnMut(Vec3<f32>) -> Vec3<i32>,
mut light_map: impl FnMut(Vec3<i32>) -> f32,
mut glow_map: impl FnMut(Vec3<i32>) -> f32,
sprite_data: &HashMap<(SpriteKind, usize), [SpriteData; SPRITE_LOD_LEVELS]>,
sprite_config: &SpriteSpec,
) {
prof_span!("extract sprite_instances");
for (rel_pos, block) in blocks {
let Some(sprite) = block.get_sprite() else {
continue;
};
let Some(cfg) = sprite_config.get(sprite) else {
continue;
};
let wpos = to_wpos(rel_pos);
let seed = (wpos.x as u64)
.overflowing_mul(3)
.0
.overflowing_add((wpos.y as u64).overflowing_mul(7).0)
.0
.overflowing_add((wpos.x as u64).overflowing_mul(wpos.y as u64).0)
.0; // Awful PRNG
let ori = (block.get_ori().unwrap_or((seed % 4) as u8 * 2)) & 0b111;
let variation = seed as usize % cfg.variations.len();
let key = (sprite, variation);
// NOTE: Safe because we called sprite_config_for already.
// NOTE: Safe because 0 ≤ ori < 8
let light = light_map(wpos);
let glow = glow_map(wpos);
for (lod_level, sprite_data) in lod_levels.iter_mut().zip(&sprite_data[&key]) {
let mat = Mat4::identity()
// Scaling for different LOD resolutions
.scaled_3d(sprite_data.scale)
// Offset
.translated_3d(sprite_data.offset)
.scaled_3d(SPRITE_SCALE)
.rotated_z(f32::consts::PI * 0.25 * ori as f32)
.translated_3d(
rel_pos + Vec3::new(0.5, 0.5, 0.0)
);
// Add an instance for each page in the sprite model
for page in sprite_data.vert_pages.clone() {
// TODO: could be more efficient to create once and clone while
// modifying vert_page
let instance = SpriteInstance::new(
mat,
cfg.wind_sway,
sprite_data.scale.z,
rel_pos.as_(),
ori,
light,
glow,
page,
sprite.is_door(),
);
set_instance(lod_level, instance, wpos);
}
}
}
}
/// Function executed by worker threads dedicated to chunk meshing.
/// skip_remesh is either None (do the full remesh, including recomputing the
@ -257,7 +327,12 @@ fn mesh_worker(
sprite_config: &SpriteSpec,
) -> MeshWorkerResponse {
span!(_guard, "mesh_worker");
let blocks_of_interest = BlocksOfInterest::from_chunk(&chunk);
let blocks_of_interest = BlocksOfInterest::from_blocks(
chunk.iter_changed().map(|(pos, block)| (pos, *block)),
chunk.meta().river_velocity().magnitude_squared(),
chunk.meta().temp(),
chunk.meta().humidity(),
);
let mesh;
let (light_map, glow_map) = if let Some((light_map, glow_map)) = &skip_remesh {
@ -292,7 +367,9 @@ fn mesh_worker(
let mesh = mesh.as_ref().unwrap();
(&*mesh.light_map, &*mesh.glow_map)
};
let to_wpos = |rel_pos: Vec3<f32>| {
Vec3::from(pos * TerrainChunk::RECT_SIZE.map(|e: u32| e as i32)) + rel_pos.as_()
};
MeshWorkerResponse {
pos,
// Extract sprite locations from volume
@ -312,77 +389,31 @@ fn mesh_worker(
(c.meta().alt() - SHALLOW_ALT, c.meta().alt() - DEEP_ALT)
});
for x in 0..TerrainChunk::RECT_SIZE.x as i32 {
for y in 0..TerrainChunk::RECT_SIZE.y as i32 {
for z in z_bounds.0 as i32..z_bounds.1 as i32 + 1 {
let rel_pos = Vec3::new(x, y, z);
let wpos = Vec3::from(pos * TerrainChunk::RECT_SIZE.map(|e: u32| e as i32))
+ rel_pos;
let block = if let Ok(block) = volume.get(wpos) {
block
} else {
continue;
};
let sprite = if let Some(sprite) = block.get_sprite() {
sprite
} else {
continue;
};
if let Some(cfg) = sprite_config.get(sprite) {
let seed = wpos.x as u64 * 3
+ wpos.y as u64 * 7
+ wpos.x as u64 * wpos.y as u64; // Awful PRNG
let ori = (block.get_ori().unwrap_or((seed % 4) as u8 * 2)) & 0b111;
let variation = seed as usize % cfg.variations.len();
let key = (sprite, variation);
// NOTE: Safe because we called sprite_config_for already.
// NOTE: Safe because 0 ≤ ori < 8
let light = light_map(wpos);
let glow = glow_map(wpos);
for ((deep_level, shallow_level, surface_level), sprite_data) in
instances.iter_mut().zip(&sprite_data[&key])
{
let mat = Mat4::identity()
// Scaling for different LOD resolutions
.scaled_3d(sprite_data.scale)
// Offset
.translated_3d(sprite_data.offset)
.scaled_3d(SPRITE_SCALE)
.rotated_z(f32::consts::PI * 0.25 * ori as f32)
.translated_3d(
rel_pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)
);
// Add an instance for each page in the sprite model
for page in sprite_data.vert_pages.clone() {
// TODO: could be more efficient to create once and clone while
// modifying vert_page
let instance = SpriteInstance::new(
mat,
cfg.wind_sway,
sprite_data.scale.z,
rel_pos,
ori,
light,
glow,
page,
matches!(sprite, SpriteKind::Door | SpriteKind::DoorDark),
);
if (wpos.z as f32) < deep_alt {
deep_level.push(instance);
} else if wpos.z as f32 > underground_alt {
surface_level.push(instance);
} else {
shallow_level.push(instance);
}
}
}
}
get_sprite_instances(
&mut instances,
|(deep_level, shallow_level, surface_level), instance, wpos| {
if (wpos.z as f32) < deep_alt {
deep_level.push(instance);
} else if wpos.z as f32 > underground_alt {
surface_level.push(instance);
} else {
shallow_level.push(instance);
}
}
}
},
(0..TerrainChunk::RECT_SIZE.x as i32)
.flat_map(|x| {
(0..TerrainChunk::RECT_SIZE.y as i32).flat_map(move |y| {
(z_bounds.0 as i32..z_bounds.1 as i32)
.map(move |z| Vec3::new(x, y, z).as_())
})
})
.filter_map(|rel_pos| Some((rel_pos, *volume.get(to_wpos(rel_pos)).ok()?))),
to_wpos,
light_map,
glow_map,
sprite_data,
sprite_config,
);
instances.map(|(deep_level, shallow_level, surface_level)| {
let deep_end = deep_level.len();
@ -406,7 +437,7 @@ fn mesh_worker(
}
}
struct SpriteData {
pub struct SpriteData {
// Sprite vert page ranges that need to be drawn
vert_pages: core::ops::Range<u32>,
// Scale
@ -433,7 +464,7 @@ pub struct Terrain<V: RectRasterableVol = TerrainChunk> {
/// FIXME: This could possibly become an `AssetHandle<SpriteSpec>`, to get
/// hot-reloading for free, but I am not sure if sudden changes of this
/// value would break something
sprite_config: Arc<SpriteSpec>,
pub sprite_config: Arc<SpriteSpec>,
chunks: HashMap<Vec2<i32>, TerrainChunkData>,
/// Temporary storage for dead chunks that might still be shadowing chunks
/// in view. We wait until either the chunk definitely cannot be
@ -463,9 +494,9 @@ pub struct Terrain<V: RectRasterableVol = TerrainChunk> {
// GPU data
// Maps sprite kind + variant to data detailing how to render it
sprite_data: Arc<HashMap<(SpriteKind, usize), [SpriteData; SPRITE_LOD_LEVELS]>>,
sprite_globals: SpriteGlobalsBindGroup,
sprite_col_lights: Arc<ColLights<pipelines::sprite::Locals>>,
pub sprite_data: Arc<HashMap<(SpriteKind, usize), [SpriteData; SPRITE_LOD_LEVELS]>>,
pub sprite_globals: SpriteGlobalsBindGroup,
pub sprite_col_lights: Arc<ColLights<pipelines::sprite::Locals>>,
/// As stated previously, this is always the very latest texture into which
/// we allocate. Code cannot assume that this is the assigned texture
/// for any particular chunk; look at the `texture` field in
@ -1207,12 +1238,7 @@ impl<V: RectRasterableVol> Terrain<V> {
let sprite_instances =
response.sprite_instances.map(|(instances, alt_indices)| {
(
renderer
.create_instances(&instances)
.expect("Failed to upload chunk sprite instances to the GPU!"),
alt_indices,
)
(renderer.create_instances(&instances), alt_indices)
});
if let Some(mesh) = response.mesh {
@ -1282,6 +1308,7 @@ impl<V: RectRasterableVol> Terrain<V> {
e as f32 * sz as f32
}),
),
Quaternion::identity(),
atlas_offs,
load_time,
)]),
@ -1694,15 +1721,16 @@ impl<V: RectRasterableVol> Terrain<V> {
});
}
pub fn render_translucent<'a>(
pub fn render_sprites<'a>(
&'a self,
drawer: &mut FirstPassDrawer<'a>,
sprite_drawer: &mut SpriteDrawer<'_, 'a>,
focus_pos: Vec3<f32>,
cam_pos: Vec3<f32>,
sprite_render_distance: f32,
culling_mode: CullingMode,
) {
span!(_guard, "render_translucent", "Terrain::render_translucent");
span!(_guard, "render_sprites", "Terrain::render_sprites");
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
@ -1715,9 +1743,6 @@ impl<V: RectRasterableVol> Terrain<V> {
})
.take(self.chunks.len());
// Terrain sprites
// TODO: move to separate functions
span!(guard, "Terrain sprites");
let chunk_size = V::RECT_SIZE.map(|e| e as f32);
let sprite_low_detail_distance = sprite_render_distance * 0.75;
@ -1725,7 +1750,6 @@ impl<V: RectRasterableVol> Terrain<V> {
let sprite_hid_detail_distance = sprite_render_distance * 0.35;
let sprite_high_detail_distance = sprite_render_distance * 0.15;
let mut sprite_drawer = drawer.draw_sprites(&self.sprite_globals, &self.sprite_col_lights);
chunk_iter
.clone()
.filter(|(_, _, c)| c.visible.is_visible())
@ -1772,15 +1796,32 @@ impl<V: RectRasterableVol> Terrain<V> {
);
}
});
drop(sprite_drawer);
drop(guard);
}
pub fn render_translucent<'a>(
&'a self,
drawer: &mut FirstPassDrawer<'a>,
focus_pos: Vec3<f32>,
) {
span!(_guard, "render_translucent", "Terrain::render_translucent");
let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
// Avoid switching textures
let chunk_iter = Spiral2d::new()
.filter_map(|rpos| {
let pos = focus_chunk + rpos;
self.chunks.get(&pos).map(|c| (pos, c))
})
.take(self.chunks.len());
// Translucent
span!(guard, "Fluid chunks");
let mut fluid_drawer = drawer.draw_fluid();
chunk_iter
.filter(|(_, _, chunk)| chunk.visible.is_visible())
.filter_map(|(_, _, chunk)| {
.filter(|(_, chunk)| chunk.visible.is_visible())
.filter_map(|(_, chunk)| {
chunk
.fluid_model
.as_ref()

View File

@ -1,5 +1,5 @@
use crate::hud::CraftingTab;
use common::terrain::{BlockKind, SpriteKind, TerrainChunk};
use common::terrain::{Block, BlockKind, SpriteKind};
use common_base::span;
use rand::prelude::*;
use rand_chacha::ChaCha8Rng;
@ -11,6 +11,7 @@ pub enum Interaction {
/// twigs).
Collect,
Craft(CraftingTab),
Mount,
}
pub enum FireplaceType {
@ -57,7 +58,12 @@ pub struct BlocksOfInterest {
}
impl BlocksOfInterest {
pub fn from_chunk(chunk: &TerrainChunk) -> Self {
pub fn from_blocks(
blocks: impl Iterator<Item = (Vec3<i32>, Block)>,
river_speed_sq: f32,
temperature: f32,
humidity: f32,
) -> Self {
span!(_guard, "from_chunk", "BlocksOfInterest::from_chunk");
let mut leaves = Vec::new();
let mut drip = Vec::new();
@ -84,9 +90,7 @@ impl BlocksOfInterest {
let mut rng = ChaCha8Rng::from_seed(thread_rng().gen());
let river_speed_sq = chunk.meta().river_velocity().magnitude_squared();
chunk.iter_changed().for_each(|(pos, block)| {
blocks.for_each(|(pos, block)| {
match block.kind() {
BlockKind::Leaves if rng.gen_range(0..16) == 0 => leaves.push(pos),
BlockKind::WeakRock if rng.gen_range(0..6) == 0 => drip.push(pos),
@ -168,6 +172,7 @@ impl BlocksOfInterest {
Some(SpriteKind::RepairBench) => {
interactables.push((pos, Interaction::Craft(CraftingTab::All)))
},
_ if block.is_mountable() => interactables.push((pos, Interaction::Mount)),
_ => {},
},
}
@ -214,8 +219,8 @@ impl BlocksOfInterest {
frogs,
interactables,
lights,
temperature: chunk.meta().temp(),
humidity: chunk.meta().humidity(),
temperature,
humidity,
}
}
}

View File

@ -1,5 +1,5 @@
use ordered_float::OrderedFloat;
use specs::{Join, WorldExt};
use specs::{Join, ReadStorage, WorldExt};
use vek::*;
use super::{
@ -9,13 +9,15 @@ use super::{
use client::Client;
use common::{
comp,
comp::tool::ToolKind,
consts::MAX_PICKUP_RANGE,
comp::{ship::figuredata::VOXEL_COLLIDER_MANIFEST, tool::ToolKind, Collider},
consts::{MAX_PICKUP_RANGE, MAX_SPRITE_MOUNT_RANGE},
link::Is,
mounting::Mount,
mounting::{Mount, VolumePos, VolumeRider},
terrain::{Block, TerrainGrid, UnlockKind},
uid::{Uid, UidAllocator},
util::find_dist::{Cube, Cylinder, FindDist},
vol::ReadVol,
CachedSpatialGrid,
};
use common_base::span;
@ -32,11 +34,12 @@ pub enum BlockInteraction {
// TODO: mining blocks don't use the interaction key, so it might not be the best abstraction
// to have them here, will see how things turn out
Mine(ToolKind),
Mount,
}
#[derive(Clone, Debug)]
pub enum Interactable {
Block(Block, Vec3<i32>, BlockInteraction),
Block(Block, VolumePos, BlockInteraction),
Entity(specs::Entity),
}
@ -50,28 +53,34 @@ impl Interactable {
fn from_block_pos(
terrain: &TerrainGrid,
pos: Vec3<i32>,
uid_allocator: &UidAllocator,
colliders: &ReadStorage<Collider>,
volume_pos: VolumePos,
interaction: Interaction,
) -> Option<Self> {
let Ok(&block) = terrain.get(pos) else { return None };
let Some(block) = volume_pos.get_block(terrain, uid_allocator, colliders) else { return None };
let block_interaction = match interaction {
Interaction::Collect => {
// Check if this is an unlockable sprite
let unlock = block.get_sprite().and_then(|sprite| {
let Some(chunk) = terrain.pos_chunk(pos) else { return None };
let sprite_chunk_pos = TerrainGrid::chunk_offs(pos);
let sprite_cfg = chunk.meta().sprite_cfg_at(sprite_chunk_pos);
let unlock_condition = sprite.unlock_condition(sprite_cfg.cloned());
// HACK: No other way to distinguish between things that should be unlockable
// and regular sprites with the current unlock_condition method so we hack
// around that by saying that it is a regular collectible sprite if
// `unlock_condition` returns UnlockKind::Free and the cfg was `None`.
if sprite_cfg.is_some() || !matches!(&unlock_condition, UnlockKind::Free) {
Some(unlock_condition)
} else {
None
}
});
let unlock = match volume_pos.kind {
common::mounting::Volume::Terrain => block.get_sprite().and_then(|sprite| {
let Some(chunk) = terrain.pos_chunk(volume_pos.pos) else { return None };
let sprite_chunk_pos = TerrainGrid::chunk_offs(volume_pos.pos);
let sprite_cfg = chunk.meta().sprite_cfg_at(sprite_chunk_pos);
let unlock_condition = sprite.unlock_condition(sprite_cfg.cloned());
// HACK: No other way to distinguish between things that should be
// unlockable and regular sprites with the current
// unlock_condition method so we hack around that by
// saying that it is a regular collectible sprite if
// `unlock_condition` returns UnlockKind::Free and the cfg was `None`.
if sprite_cfg.is_some() || !matches!(&unlock_condition, UnlockKind::Free) {
Some(unlock_condition)
} else {
None
}
}),
common::mounting::Volume::Entity(_) => None,
};
if let Some(unlock) = unlock {
BlockInteraction::Unlock(unlock)
@ -82,8 +91,9 @@ impl Interactable {
}
},
Interaction::Craft(tab) => BlockInteraction::Craft(tab),
Interaction::Mount => BlockInteraction::Mount,
};
Some(Self::Block(block, pos, block_interaction))
Some(Self::Block(block, volume_pos, block_interaction))
}
}
@ -127,7 +137,11 @@ pub(super) fn select_interactable(
collect_target.and_then(|t| {
if Some(t.distance) == nearest_dist {
terrain.get(t.position_int()).ok().map(|&b| {
Interactable::Block(b, t.position_int(), BlockInteraction::Collect)
Interactable::Block(
b,
VolumePos::terrain(t.position_int()),
BlockInteraction::Collect,
)
})
} else {
None
@ -146,7 +160,7 @@ pub(super) fn select_interactable(
if let Some(mine_tool) = b.mine_tool() && b.is_air() {
Some(Interactable::Block(
b,
t.position_int(),
VolumePos::terrain(t.position_int()),
BlockInteraction::Mine(mine_tool),
))
} else {
@ -176,15 +190,19 @@ pub(super) fn select_interactable(
let items = ecs.read_storage::<comp::Item>();
let stats = ecs.read_storage::<comp::Stats>();
let player_char_state = char_states.get(player_entity);
let player_cylinder = Cylinder::from_components(
player_pos,
scales.get(player_entity).copied(),
colliders.get(player_entity),
char_states.get(player_entity),
player_char_state,
);
let closest_interactable_entity = (
&ecs.entities(),
let spacial_grid = ecs.read_resource::<CachedSpatialGrid>();
let entities = ecs.entities();
let mut entity_data = (
&entities,
&positions,
&bodies,
scales.maybe(),
@ -193,8 +211,11 @@ pub(super) fn select_interactable(
!&is_mount,
(stats.mask() | items.mask()).maybe(),
)
.join()
.filter(|&(e, _, _, _, _, _, _, _)| e != player_entity) // skip the player's entity
.join();
let closest_interactable_entity = spacial_grid.0.in_circle_aabr(player_pos.xy(), MAX_PICKUP_RANGE)
.filter(|&e| e != player_entity) // skip the player's entity
.filter_map(|e| entity_data.get(e, &entities))
.filter_map(|(e, p, b, s, c, cs, _, has_stats_or_item)| {
// Note, if this becomes expensive to compute do it after the distance check!
//
@ -228,6 +249,50 @@ pub(super) fn select_interactable(
});
let scene_terrain = scene.terrain();
let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
let volumes_data = (
&entities,
&ecs.read_storage::<Uid>(),
&ecs.read_storage::<comp::Body>(),
&ecs.read_storage::<crate::ecs::comp::Interpolated>(),
&ecs.read_storage::<comp::Collider>(),
);
let mut volumes_data = volumes_data.join();
let volumes = spacial_grid.0.in_circle_aabr(player_pos.xy(), search_dist)
.filter(|&e| e != player_entity) // skip the player's entity
.filter_map(|e| volumes_data.get(e, &entities))
.filter_map(|(entity, uid, body, interpolated, collider)| {
let vol = collider.get_vol(&voxel_colliders_manifest)?;
let (blocks_of_interest, offset) =
scene
.figure_mgr()
.get_blocks_of_interest(entity, body, Some(collider))?;
let mat = Mat4::from(interpolated.ori.to_quat()).translated_3d(interpolated.pos)
* Mat4::translation_3d(offset);
let p = mat.inverted().mul_point(player_pos);
let aabb = Aabb {
min: Vec3::zero(),
max: vol.volume().sz.as_(),
};
if aabb.contains_point(p) || aabb.distance_to_point(p) < search_dist {
Some(blocks_of_interest.interactables.iter().map(
move |(block_offset, interaction)| {
let wpos = mat.mul_point(block_offset.as_() + 0.5);
(wpos, VolumePos::entity(*block_offset, *uid), interaction)
},
))
} else {
None
}
})
.flatten();
let is_volume_rider = ecs.read_storage::<Is<VolumeRider>>();
// Find closest interactable block
// TODO: consider doing this one first?
let closest_interactable_block_pos = Spiral2d::new()
@ -248,26 +313,37 @@ pub(super) fn select_interactable(
.interactables
.iter()
.map(move |(block_offset, interaction)| (chunk_pos + block_offset, interaction))
.map(|(pos, interaction)| {
(pos.as_::<f32>() + 0.5, VolumePos::terrain(pos), interaction)
})
})
.map(|(block_pos, interaction)| (
block_pos,
block_pos.map(|e| e as f32 + 0.5)
.distance_squared(player_pos),
interaction,
))
.min_by_key(|(_, dist_sqr, _)| OrderedFloat(*dist_sqr))
.map(|(block_pos, _, interaction)| (block_pos, interaction));
.chain(volumes)
.filter(|(wpos, volume_pos, interaction)| {
match interaction {
Interaction::Mount => !is_volume_rider.contains(player_entity)
&& wpos.distance_squared(player_pos) < MAX_SPRITE_MOUNT_RANGE * MAX_SPRITE_MOUNT_RANGE
&& !is_volume_rider.join().any(|is_volume_rider| is_volume_rider.pos == *volume_pos),
_ => true,
}
})
.min_by_key(|(wpos, _, _)| OrderedFloat(wpos.distance_squared(player_pos)));
// Return the closest of the 2 closest
closest_interactable_block_pos
.filter(|(block_pos, _)| {
.filter(|(wpos, _, _)| {
player_cylinder.min_distance(Cube {
min: block_pos.as_(),
min: *wpos,
side_length: 1.0,
}) < search_dist
})
.and_then(|(block_pos, interaction)| {
Interactable::from_block_pos(&terrain, block_pos, *interaction)
.and_then(|(_, block_pos, interaction)| {
Interactable::from_block_pos(
&terrain,
&ecs.read_resource::<UidAllocator>(),
&ecs.read_storage(),
block_pos,
*interaction,
)
})
.or_else(|| closest_interactable_entity.map(|(e, _)| Interactable::Entity(e)))
}

View File

@ -25,7 +25,7 @@ use common::{
consts::MAX_MOUNT_RANGE,
event::UpdateCharacterMetadata,
link::Is,
mounting::Mount,
mounting::{Mount, VolumePos},
outcome::Outcome,
recipe,
terrain::{Block, BlockKind},
@ -350,7 +350,8 @@ impl SessionState {
match inv_event {
InventoryUpdateEvent::BlockCollectFailed { pos, reason } => {
self.hud.add_failed_block_pickup(
pos,
// TODO: Possibly support volumes.
VolumePos::terrain(pos),
HudCollectFailedReason::from_server_reason(
&reason,
client.state().ecs(),
@ -888,6 +889,16 @@ impl PlayState for SessionState {
if client.is_riding() {
client.unmount();
} else {
if let Some(interactable) = &self.interactable {
match interactable {
Interactable::Block(_, pos, interaction) => {
if matches!(interaction, BlockInteraction::Mount) {
client.mount_volume(*pos)
}
},
Interactable::Entity(entity) => client.mount(*entity),
}
}
let player_pos = client
.state()
.read_storage::<Pos>()
@ -921,15 +932,22 @@ impl PlayState for SessionState {
},
GameInput::Interact => {
if state {
let mut client = self.client.borrow_mut();
if let Some(interactable) = &self.interactable {
let mut client = self.client.borrow_mut();
match interactable {
Interactable::Block(block, pos, interaction) => {
match interaction {
BlockInteraction::Collect
| BlockInteraction::Unlock(_) => {
if block.is_collectible() {
client.collect_block(*pos);
match pos.kind {
common::mounting::Volume::Terrain => {
client.collect_block(pos.pos);
}
common::mounting::Volume::Entity(_) => {
// TODO: Do we want to implement this?
},
}
}
},
BlockInteraction::Craft(tab) => {
@ -938,7 +956,8 @@ impl PlayState for SessionState {
block.get_sprite().map(|s| (*pos, s)),
)
},
BlockInteraction::Mine(_) => {},
BlockInteraction::Mine(_)
| BlockInteraction::Mount => {},
}
},
Interactable::Entity(entity) => {

View File

@ -1,7 +1,7 @@
use common::{
figure::Segment,
util::{linear_to_srgba, srgb_to_linear},
vol::{IntoFullVolIterator, ReadVol, SizedVol, Vox},
vol::{FilledVox, IntoFullVolIterator, ReadVol, SizedVol},
};
use euc::{buffer::Buffer2d, rasterizer, Pipeline};
use image::{DynamicImage, RgbaImage};
@ -263,24 +263,24 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
if let Some(col) = vox.get_color() {
let col = col.map(|e| e as f32 / 255.0);
let is_empty = |pos| segment.get(pos).map(|v| v.is_empty()).unwrap_or(true);
let is_filled = |pos| segment.get(pos).map(|v| v.is_filled()).unwrap_or(false);
let occluders = |unit_x, unit_y, dir| {
// Would be nice to generate unit_x and unit_y from a given direction.
[
!is_empty(pos + dir - unit_x),
!is_empty(pos + dir - unit_x - unit_y),
!is_empty(pos + dir - unit_y),
!is_empty(pos + dir + unit_x - unit_y),
!is_empty(pos + dir + unit_x),
!is_empty(pos + dir + unit_x + unit_y),
!is_empty(pos + dir + unit_y),
!is_empty(pos + dir - unit_x + unit_y),
is_filled(pos + dir - unit_x),
is_filled(pos + dir - unit_x - unit_y),
is_filled(pos + dir - unit_y),
is_filled(pos + dir + unit_x - unit_y),
is_filled(pos + dir + unit_x),
is_filled(pos + dir + unit_x + unit_y),
is_filled(pos + dir + unit_y),
is_filled(pos + dir - unit_x + unit_y),
]
};
// -x
if is_empty(pos - Vec3::unit_x()) {
if !is_filled(pos - Vec3::unit_x()) {
vertices.extend_from_slice(&create_quad(
offs + pos.map(|e| e as f32) + Vec3::unit_y(),
-Vec3::unit_y(),
@ -291,7 +291,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
));
}
// +x
if is_empty(pos + Vec3::unit_x()) {
if !is_filled(pos + Vec3::unit_x()) {
vertices.extend_from_slice(&create_quad(
offs + pos.map(|e| e as f32) + Vec3::unit_x(),
Vec3::unit_y(),
@ -302,7 +302,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
));
}
// -y
if is_empty(pos - Vec3::unit_y()) {
if !is_filled(pos - Vec3::unit_y()) {
vertices.extend_from_slice(&create_quad(
offs + pos.map(|e| e as f32),
Vec3::unit_x(),
@ -313,7 +313,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
));
}
// +y
if is_empty(pos + Vec3::unit_y()) {
if !is_filled(pos + Vec3::unit_y()) {
vertices.extend_from_slice(&create_quad(
offs + pos.map(|e| e as f32) + Vec3::unit_y(),
Vec3::unit_z(),
@ -324,7 +324,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
));
}
// -z
if is_empty(pos - Vec3::unit_z()) {
if !is_filled(pos - Vec3::unit_z()) {
vertices.extend_from_slice(&create_quad(
offs + pos.map(|e| e as f32),
Vec3::unit_y(),
@ -335,7 +335,7 @@ fn generate_mesh(segment: &Segment, offs: Vec3<f32>) -> Vec<Vert> {
));
}
// +z
if is_empty(pos + Vec3::unit_z()) {
if !is_filled(pos + Vec3::unit_z()) {
vertices.extend_from_slice(&create_quad(
offs + pos.map(|e| e as f32) + Vec3::unit_z(),
Vec3::unit_x(),