kaosat-dev / Blenvy

Bevy Code & Blender addon for a simple workflow to add & edit Bevy components in Blender
Other
592 stars 55 forks source link

Blenvy for Bevy: the world sometimes does not get a `BlueprintReadyForFinalizing` even when all descendants with `BlueprintInfo` are `BlueprintInstanceReady`, resulting in the world stuck loading forever. #205

Closed janhohenheim closed 2 months ago

janhohenheim commented 3 months ago

This happens about 25% of the time on Wasm. The following hack brings this down to about 15%:

fn hack_loading(
    mut commands: Commands,
    q_world: Query<
        Entity,
        (
            With<GameWorldTag>,
            With<BlueprintSpawning>,
            Without<BlueprintReadyForFinalizing>,
            Without<BlueprintInstanceReady>,
        ),
    >,
    q_blueprints: Query<Has<BlueprintInstanceReady>, (With<BlueprintInfo>, Without<GameWorldTag>)>,
) {
    let world = single!(q_world);
    if !q_blueprints.is_empty() && q_blueprints.iter().all(|ready| ready) {
        commands.entity(world).insert(BlueprintReadyForFinalizing);
    }
}

and the following is a complete workaround:

#[derive(Debug, Resource, Deref, DerefMut)]
struct LoadTimer(Timer);

impl Default for LoadTimer {
    fn default() -> Self {
        Self(Timer::from_seconds(10.0, TimerMode::Once))
    }
}

/// Needed because `BlueprintReadyForFinalizing` is not inserted on `World` in about 25% of runs on Wasm
/// due to a bug that is probably coming from Blenvy
fn hack_loading(
    time: Res<Time>,
    mut commands: Commands,
    q_loading: Query<
        Entity,
        (
            With<BlueprintSpawning>,
            Without<BlueprintReadyForFinalizing>,
            Without<BlueprintInstanceReady>,
        ),
    >,
    q_children: Query<&Children>,
    q_blueprints: Query<Has<BlueprintInstanceReady>, With<BlueprintInfo>>,
    mut load_timer: ResMut<LoadTimer>,
) {
    if !load_timer.finished() {
        load_timer.tick(time.delta());
        return;
    }
    if q_loading.is_empty() {
        return;
    }
    let mut processed = HashSet::new();
    let mut ready_map = HashMap::new();
    for loading in q_loading.iter() {
        if processed.contains(&loading) {
            continue;
        }
        go_through_children(
            &mut commands,
            &q_children,
            &q_blueprints,
            loading,
            &mut ready_map,
        );
        processed.insert(loading);
    }
}

fn go_through_children(
    commands: &mut Commands,
    q_children: &Query<&Children>,
    q_blueprints: &Query<Has<BlueprintInstanceReady>, With<BlueprintInfo>>,
    entity: Entity,
    ready_map: &mut HashMap<Entity, bool>,
) -> bool {
    if q_blueprints.contains(entity) && !q_children.contains(entity) {
        ready_map.insert(entity, false);
        return false;
    }
    match q_children.get(entity) {
        Ok(children) => {
            let ready = children.iter().all(|child| {
                if let Some(ready) = ready_map.get(child) {
                    *ready
                } else {
                    let ready =
                        go_through_children(commands, q_children, q_blueprints, *child, ready_map);
                    ready_map.insert(*child, ready);
                    ready
                }
            });
            if ready {
                ready_map.insert(entity, true);
                commands.entity(entity).insert(BlueprintReadyForFinalizing);
            }
            ready
        }
        Err(_) => true,
    }
}

Due to this only happening on Wasm, where scheduling is single threaded, and only happening sometimes, I am pretty certain this is due to a scheduling ambiguity.