eira-fransham / eira-fransham/bevy_mod_scripting_qcvm

Notes about implementation

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

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 get Symbol or StaticStr or some similar name upstreamed to bevy_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_trenchbroom to 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 the QuakeCBuiltins namespace, since then it can be considered to be something like "a method of the entity that holds the progs.dat".

Note: ScriptQueryBuilder is 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 use FromScript, we will need to bound <Input as FromScript>::This<'_>: 'static so that we can get it from the WorldGuard before calling with_global_access. This is a reasonable restriction.

Contributor guide

No contributing guide indexed for this repository

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

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.