makspll / makspll/bevy_mod_scripting

Implement MapEntity for ScriptValue to facilitate saving

Open
#459 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
649
Forks
59
PR merge metrics
No merged PRs in 30d

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing ScriptValue, ReflectBase, MapEntities, and EntityMapper in the scripting integration. Define how entity references are remapped recursively through lists and maps, and decide the behavior for resources, owned allocations, functions, and other non-component references. Done means ScriptValue supports the intended entity mapping behavior with the invalid-reference policy documented or covered by tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
lua, rust
Domain
game-dev
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.