eira-fransham / eira-fransham/bevy_mod_scripting_qcvm
Notes about implementation
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 2
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Loading progs.dat
progs.dat includes the names of builtins and fields that the script expects to exist. We should allow the scripting engine to either automatically figure out the mapping of indices<->builtin/field names using the header of the progs.dat, or supply them explicitly. This means we can fall back to using well-known headers for misbehaving progs.dats.
The progs.dat handle is stored in a component on the same entity as the Bsp component. While for now we should only ever have one progs.dat globally, eventually this will mean that we can load maps from multiple games seamlessly, using whatever progs.dat is required for the map we're loading. The state of the progs.dat is cleared between maps anyway, with the player's loadout etc being maintained out-of-band. This is an even more powerful form of some other engines' ability to change games at runtime. bevy_mod_pakfile would have to be modified to allow this, as currently it's not possible to add new game directories at runtime. We could have a system where we scan all game directories on startup and have a scheme where the first element of an asset path is treated more like a glob, so we can have a path like {hipnotic,id1}/maps/e1m1.bsp. This can then be handled using glob::Pattern, which does not require direct filesystem access.
Type system
While technically QuakeC permits type punning, from testing it seems like (luckily) no progs.dats actually use it. So long as the type system is actually respected, and it seems like it is, we can quite easily map StringId to String etc.
Note: For efficiency we should really use
Arc<str>. This is a common type in many languages, and we can probably getSymbolorStaticStror some similar name upstreamed tobevy_mod_scripting_bindings::ScriptValue.
Builtins
Builtins can either be assigned to the global namespace or the QuakeCBuiltins namespace. In order to allow overriding QuakeC functions with Rust or Lua functions, we should store all QuakeC functions in a QuakeCProgs registry. Since the registry is global but the progs are stored on the Bsp, we can pass a ReflectReference to the Quake C script handle component with some kind of wrapper that returns InteropError if the entity doesn't match. Returning an InteropError causes WorldAccessGuard::try_call_overloads to ignore this function and try the next. This would also allow bevy_trenchbroom to directly call the QuakeC functions for creating new entities.
TODO: We probably need to add (and hopefully upstream) a way to configure
bevy_trenchbroomto generate entities differently - right now the only way it can generate entities/components is with the class map.
Some builtins access the level data - e.g. trace, move_to_goal, spawn, etc. We can access components on the Bsp by checking the signature of the function and pass components from the entity that contains the Bsp and script component, by looking for Ref and RefMut and creating a ScriptQueryBuilder and passing that to WorldAccessGuard::query.
TODO: This magic approach makes sense if we never want to pass
ReflectReferences as an explicit parameter to builtins. It might be best to only do this when a builtin is registered in theQuakeCBuiltinsnamespace, since then it can be considered to be something like "a method of the entity that holds theprogs.dat".
Note:
ScriptQueryBuilderis somewhat inflexible and inefficient, see "Extra Notes" for a better approach when builtins are defined in Rust.
NamespaceBuilder::<GlobalNamespace>::new(&mut world)
.register(
"centerprint",
|text: String| {
let world = ThreadWorldContainer
.try_get_world()
.unwrap();
let dest = Vec::<u8>::new();
ServerCmd::CenterPrint { text }.serialize(&mut dest)?;
world.with_resource_mut(|ev: &mut Events<ServerMessage>| {
// ...
});
},
);
Fields
Fields should be registered with an API something like:
NamespaceBuilder::<QuakeCEntity>::new(&mut world)
.register(
"foo",
|component: Ref<MyComponent>| component.x,
)
.register(
"foo",
|component: RefMut<MyComponent>, value: f32| component.x = value,
)
.register(
"bar",
|component: Ref<MyOtherComponent>| component.y,
)
.register(
"bar",
|component: RefMut<MyOtherComponent>, FromQuake(entity)| {
component.some_entity = entity;
},
);
(see bevy_mod_scripting_bindings::Ref).
FromQuake can be a thin wrapper around Entity which internally accesses a QuakeEntityIdRegistry component on the Bsp entity that the script is attached to.
We can use ScriptFunctionRegistry::iter_overloads + FunctionArgInfo to get the component ID to access. This provides a TypeId of the underlying component and info on whether it's through a Ref or RefMut. When loading the progs.dat, the ComponentId can be retrieved via the TypeId by using Components::get_id and stored in a Vec along with the relevant DynamicScriptFunction based on field offset. Then WorldGuard::try_call_overloads can be used to access the getter/setter and the stored ComponentId can be used to construct a ReflectReference which is then converted into Ref/RefMut by the FromScript infrastructure.
TODO: Should we use
ScriptFunctionRegistry.magic_functions.{get,set}if named fields aren't found? That would probably make it easier to port.
Globals
The first 28 globals are fully script-internal - used for args and returns. Other globals such as time, force retouch, deathmatch, coop etc should be modifiable by outside code. This should be done with ScriptGlobalsRegistry, and acts the same as entity fields except stored in the namespace QuakeCGlobal (+ maybe GlobalNamespace).
Note: This will probably mean that, to avoid reallocating a new
Arc<ScriptGlobalMakerFn>every time the global is updated from inside a builtin, game bindings will need to have their own global storage.
TODO: Should we use
ScriptFunctionRegistry.magic_functions.{get,set}for the first 28 "magic" globals? Probably not, messing with them would be bad.
Extra notes
In the builtins section it was mentioned that a good approach to giving access to the script entity's data (specifically, Bsp) would be to iterate over parameters and create a dynamic query. This is necessary if the builtins are implemented in another language - which is likely the best approach long-term - but for builtins implemented in Rust we can do better. We can have an extension trait that takes an IntoSystem and uses ThreadWorldContainer and WorldGuard::with_global_access to run it, and In(..) parameters to pass variables. This will almost certainly be significantly more efficient than using DynamicScriptFn for everything.
Note: If we want to use
In(..)and still useFromScript, we will need to bound<Input as FromScript>::This<'_>: 'staticso that we can get it from theWorldGuardbefore callingwith_global_access. This is a reasonable restriction.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
This issue is a broad design note rather than a scoped implementation task. Read the referenced bevy_trenchbroom src/class/mod.rs and the bevy_mod_scripting APIs for NamespaceBuilder, ScriptFunctionRegistry, ScriptGlobalsRegistry, and ThreadWorldContainer, then trace the progs.dat loading entry point. Done is not defined; the work needs to be split into smaller issues with concrete acceptance criteria.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers, game-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100