Skip to content

RFC: Std I/O driven application (aka amethyst_commands) #13

@azriel91

Description

@azriel91

Issue 999, this is gonna be epic!

Summary

Ability to control an Amethyst application using commands issued through stdin, with human-friendly terminal interaction.

Motivation

Inspecting and manipulating the state1 of an application at run time is a crucial part of development, with at least the following use cases:

  • Determining that the application is behaving as expected.
  • Experimenting with new features.
  • Triggering certain cases.
  • Investigating / troubleshooting unexpected behaviour.
  • Automatically driving the application for integration tests.

A command terminal will greatly reduce the effort to carry out the aforementioned tasks.

1 state here means the runtime values, not amethyst::State

Prior Art

Expand -- copied from #995 (warning: code heavy)

okay, so this post is code heavy, but it's how I've done commands in my game (youtube). It shouldn't force people to use the state machine, since event types are "plug in if you need it".

Crate: stdio_view (probably analogous to amethyst_commands)

  • Reads stdin strings, uses shell_words to parse into separate tokens.
  • Parses the first token into an AppEventVariant to determine which AppEvent the tokens correspond to. On success, it sends a tuple: (AppEventVariant, Vec<String>) (the tokens) to an EventChannel<(AppEventVariant, Vec<String>)>.

Changes if put into Amethyst:

  • StdinSystem would be generic over top level types E and EVariant, which would take in AppEvent and AppEventVariant.

Crate: application_event

  • Contains AppEvent and AppEventVariant.

  • AppEvent is an enum over all custom event types, AppEventVariant is derived from AppEvent, without the fields.

    Example:

    use character_selection_model::CharacterSelectionEvent;
    use map_selection_model::MapSelectionEvent;
    
    #[derive(Clone, Debug, Display, EnumDiscriminants, From, PartialEq)]
    #[strum_discriminants(
        name(AppEventVariant),
        derive(Display, EnumIter, EnumString),
        strum(serialize_all = "snake_case")
    )]
    pub enum AppEvent {
        /// `character_selection` events.
        CharacterSelection(CharacterSelectionEvent),
        /// `map_selection` events.
        MapSelection(MapSelectionEvent),
    }

This would be an application specific crate, so it wouldn't go into Amethyst. If I want to have State event control, this will include an additional variant State(StateEvent) from use amethyst_state::StateEvent;, where StateEvent carries the information of what to do (e.g. Pop or Switch).

Crate: stdio_spi

  • StdinMapper is a trait with the following associated types:

    use structopt::StructOpt;
    
    use Result;
    
    /// Maps tokens from stdin to a state specific event.
    pub trait StdinMapper {
        /// Resource needed by the mapper to construct the state specific event.
        ///
        /// Ideally we can have this be the `SystemData` of an ECS system. However, we cannot add
        /// a `Resources: for<'res> SystemData<'res>` trait bound as generic associated types (GATs)
        /// are not yet implemented. See:
        ///
        /// * <https://users.rust-lang.org/t/17444>
        /// * <https://github.com/rust-lang/rust/issues/44265>
        type Resource;
        /// State specific event type that this maps tokens to.
        type Event: Send + Sync + 'static;
        /// Data structure representing the arguments.
        type Args: StructOpt;
        /// Returns the state specific event constructed from stdin tokens.
        ///
        /// # Parameters
        ///
        /// * `tokens`: Tokens received from stdin.
        fn map(resource: &Self::Resource, args: Self::Args) -> Result<Self::Event>;
    }

    Args is a T: StructOpt which we can convert the String tokens from before we pass it to the map function. Resource is there because the constructed AppEvent can contain fields that are constructed based on an ECS resource.

  • This crate also provides a generic MapperSystem that reads from EventChannel<(AppEventVariant, Vec<String>)> from the stdio_view crate. If the variant matches the AppEventVariant this system is responsible for, it passes all of the tokens to a T: StdinMapper that understands how to turn them into an AppEvent, given the Resource.

    /// Type to fetch the application event channel.
    type MapperSystemData<'s, SysData> = (
        Read<'s, EventChannel<VariantAndTokens>>,
        Write<'s, EventChannel<AppEvent>>,
        SysData,
    );
    
    impl<'s, M> System<'s> for MapperSystem<M>
    where
        M: StdinMapper + TypeName,
        M::Resource: Default + Send + Sync + 'static,
        AppEvent: From<M::Event>,
    {
        type SystemData = MapperSystemData<'s, Read<'s, M::Resource>>;
    
        fn run(&mut self, (variant_channel, mut app_event_channel, resources): Self::SystemData) {
        // ...
        let args = M::Args::from_iter_safe(tokens.iter())?;
        M::map(&resources, args)
        // ... collect each event
    
        app_event_channel.drain_vec_write(&mut events);
    }
    }

Crate: character_selection_stdio (or any other crate that supports stdin -> AppEvent)

  • Implements the stdio_spi.

  • The Args type:

    #[derive(Clone, Debug, PartialEq, StructOpt)]
    pub enum MapSelectionEventArgs {
        /// Select event.
        #[structopt(name = "select")]
        Select {
            /// Slug of the map or random, e.g. "default/eruption", "random".
            #[structopt(short = "s", long = "selection")]
            selection: String,
        },
    }
  • The StdinMapper type:

    impl StdinMapper for MapSelectionEventStdinMapper {
        type Resource = MapAssets;         // Read resource from the `World`, I take a `MapHandle` from it
        type Event = MapSelectionEvent;    // Event to map to
        type Args = MapSelectionEventArgs; // Strong typed arguments, rather than the String tokens
    
        fn map(map_assets: &MapAssets, args: Self::Args) -> Result<Self::Event> {
            match args {
                MapSelectionEventArgs::Select { selection } => {
                    Self::map_select_event(map_assets, &selection)
                }
            }
        }
    }
  • The bundle, which adds a MapperSystem<MapSelectionEventStdinMapper>:

    builder.add(
        MapperSystem::<MapSelectionEventStdinMapper>::new(AppEventVariant::MapSelection),
        &MapperSystem::<MapSelectionEventStdinMapper>::type_name(),
        &[],
    );

Can use it as inspiration to drive the design, or I'm happy to push my code up for the reusable parts (stdio_spi should be usable as is, stdio_view probably needs a re-write).

Detailed Design

TODO: discuss

Alternatives

The amethyst-editor will let you do some of the above tasks (inspecting, manipulating entities and components). It doesn't cater for:

  • Server side inspection (e.g. SSH to an application running on a headless server)
  • Automated tests
  • Easy repeatability (source controlled actions)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions