Skip to content

Implement MapEntity for ScriptValue to facilitate saving #459

@Ownezx

Description

@Ownezx

As I have started to combine saving with BMS, I realize that what seems to be used in Bevy to update Entity IDs throughout state change is the MapEntities and ReflectMapEntities traits.

Using these traits, bevy_save can very easily remap links to entities within components and allows the user to avoid having to update references manually, using the bevy EntityMapper instead.

Ideally, I would like ScriptValue to implement the MapEntity trait in order to allow users to update references to entities within script parameters.
This is already possible for components as we have access to ReflectBase::Component(Entity, ComponentId)
Adding a enum type ReflectBase::Entity(Entity) could allow to easily implement the MapEntity trait.

One challenge to consider is the behavior when a reflect of something else than a component or entity is mapped. I would assume the reference doesn't always stays valid?
If not we probably should panic to avoid undefined behavior, while allowing bypassing this by adding a no_map_panic feature?


As a proof of concept I implemented MapEntities successfully for components and used it with bevy_save to apply snapshot with the following logic:

  • Saving
    • player presses F5
    • on_save lua call script sends ScriptValue I save it in a resources
    • bevy_save does a snap shot that includes this resource.
  • Loading
    • player presses F9
    • bevy_save applies snapshot (this is where ReflectMapEntities is applied to remap to the new entities )
    • I send the data back to script with an on_load lua call.

Here are a few snippets for the MapEntity and Luau script used

#[derive(Default, Reflect, Debug, Clone)]
pub struct LocalScriptValue(pub ScriptValue);

impl MapEntities for LocalScriptValue {
    fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
        self.0 = match &self.0 {
            ScriptValue::Unit => ScriptValue::Unit,
            ScriptValue::Bool(val) => ScriptValue::Bool(*val),
            ScriptValue::Integer(val) => ScriptValue::Integer(*val),
            ScriptValue::Float(val) => ScriptValue::Float(*val),
            ScriptValue::String(cow) => ScriptValue::String(cow.clone()),
            ScriptValue::List(script_values) => {
                let mut vec = Vec::with_capacity(script_values.len());
                for val in script_values {
                    let mut temp = LocalScriptValue(val.clone());
                    temp.map_entities(entity_mapper);
                    vec.push(temp.0);
                }
                ScriptValue::List(vec)
            }
            ScriptValue::Map(hash_map) => {
                let mut map: HashMap<String, ScriptValue> = HashMap::with_capacity(hash_map.len());
                for (key, value) in hash_map.iter() {
                    let mut temp = LocalScriptValue(value.clone());
                    temp.map_entities(entity_mapper);
                    map.insert(key.to_string(), temp.0);
                }
                ScriptValue::Map(map)
            }
            ScriptValue::Reference(reflect_reference) => {
                match &reflect_reference.base.base_id {
                    ReflectBase::Component(entity, component_id) => {
                        let mapped = entity_mapper.map_entity(*entity);

                        let mut new_ref = reflect_reference.clone();
                        new_ref.base.base_id = ReflectBase::Component(mapped, *component_id);

                        ScriptValue::Reference(new_ref)
                    }
                    // resources and owned allocations don’t need entity remapping
                    _ => {
                        error!("Attempted to map reflect_reference, that was not component");
                        ScriptValue::Unit
                    }
                }
            }
            ScriptValue::FunctionMut(_) => {
                error!("Attempted to map dynamic_script_function_mut");
                ScriptValue::Unit
            }
            ScriptValue::Function(_) => {
                error!("Attempted to map dynamic_script_function");
                ScriptValue::Unit
            }
            ScriptValue::Error(interop_error) => ScriptValue::Error(interop_error.clone()),
        };
    }
}
local Template = require("./../library/spawnTemplate")
local simple = require("./../templates/FirstTemplates")

local scenario = "missile_test"

register_scenario(
    scenario,
    "Test",
    "Test scenario",
    {}
)

type SavedData = {
    ship: any,
    ended: boolean,
    vel: any,
} 
local s: SavedData;

function start_scenario(data)
    s =
    {
        missile = nil;
        ship = nil;
        ended = false;
    }
    s.ship = Template.spawnTemplate(simple.corvette, 0, 0);
    local velocity = construct(types.LinearVelocity, {})
    velocity["1"].x = 50
    velocity["1"].y = -50
    world.insert_component(s.ship, types.LinearVelocity, velocity)
    s.vel = world.get_component(s.ship, types.LinearVelocity)
end

function fixed_update(delta: number)
    if not s.ship and not s.ended then
        print("Ship dead")
    end
    if not s.ship then
        s.ended = true;
    end

    print(s.vel["1"].x)
end

function on_script_unloaded()
    return s
end

function on_script_reloaded(saved_data: SavedData)
    s = saved_data;
end

function save_scenario()
    print("save call")
    on_save_callback(s);
end

function load_scenario(saved_data: SavedData)
    print("load call")
    s = saved_data;
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions