mirror of
https://github.com/mat-1/azalea.git
synced 2025-08-02 23:44:38 +00:00
organize az-client file structure by moving things into plugins directory
This commit is contained in:
parent
e532511ba5
commit
9d0fcc671b
25 changed files with 162 additions and 157 deletions
|
@ -62,28 +62,27 @@ use uuid::Uuid;
|
|||
|
||||
use crate::{
|
||||
attack::{self, AttackPlugin},
|
||||
brand::BrandPlugin,
|
||||
chat::ChatPlugin,
|
||||
chunks::{ChunkBatchInfo, ChunkPlugin},
|
||||
configuration::ConfigurationPlugin,
|
||||
chunks::{ChunkBatchInfo, ChunksPlugin},
|
||||
disconnect::{DisconnectEvent, DisconnectPlugin},
|
||||
events::{Event, EventPlugin, LocalPlayerEvents},
|
||||
events::{Event, EventsPlugin, LocalPlayerEvents},
|
||||
interact::{CurrentSequenceNumber, InteractPlugin},
|
||||
inventory::{Inventory, InventoryPlugin},
|
||||
local_player::{
|
||||
death_event, GameProfileComponent, Hunger, InstanceHolder, PermissionLevel,
|
||||
PlayerAbilities, TabList,
|
||||
GameProfileComponent, Hunger, InstanceHolder, PermissionLevel, PlayerAbilities, TabList,
|
||||
},
|
||||
mining::{self, MinePlugin},
|
||||
movement::{LastSentLookDirection, PhysicsState, PlayerMovePlugin},
|
||||
mining::{self, MiningPlugin},
|
||||
movement::{LastSentLookDirection, MovementPlugin, PhysicsState},
|
||||
packet::{
|
||||
login::{self, LoginSendPacketQueue},
|
||||
PacketHandlerPlugin,
|
||||
PacketPlugin,
|
||||
},
|
||||
player::retroactively_add_game_profile_component,
|
||||
raw_connection::RawConnection,
|
||||
respawn::RespawnPlugin,
|
||||
send_client_end::TickEndPlugin,
|
||||
task_pool::TaskPoolPlugin,
|
||||
tick_end::TickEndPlugin,
|
||||
Account, PlayerInfo,
|
||||
};
|
||||
|
||||
|
@ -818,8 +817,6 @@ impl Plugin for AzaleaPlugin {
|
|||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
// fire the Death event when the player dies.
|
||||
death_event,
|
||||
// add GameProfileComponent when we get an AddPlayerEvent
|
||||
retroactively_add_game_profile_component.after(EntityUpdateSet::Index),
|
||||
),
|
||||
|
@ -965,23 +962,23 @@ impl PluginGroup for DefaultPlugins {
|
|||
let mut group = PluginGroupBuilder::start::<Self>()
|
||||
.add(AmbiguityLoggerPlugin)
|
||||
.add(TimePlugin)
|
||||
.add(PacketHandlerPlugin)
|
||||
.add(PacketPlugin)
|
||||
.add(AzaleaPlugin)
|
||||
.add(EntityPlugin)
|
||||
.add(PhysicsPlugin)
|
||||
.add(EventPlugin)
|
||||
.add(EventsPlugin)
|
||||
.add(TaskPoolPlugin::default())
|
||||
.add(InventoryPlugin)
|
||||
.add(ChatPlugin)
|
||||
.add(DisconnectPlugin)
|
||||
.add(PlayerMovePlugin)
|
||||
.add(MovementPlugin)
|
||||
.add(InteractPlugin)
|
||||
.add(RespawnPlugin)
|
||||
.add(MinePlugin)
|
||||
.add(MiningPlugin)
|
||||
.add(AttackPlugin)
|
||||
.add(ChunkPlugin)
|
||||
.add(ChunksPlugin)
|
||||
.add(TickEndPlugin)
|
||||
.add(ConfigurationPlugin)
|
||||
.add(BrandPlugin)
|
||||
.add(TickBroadcastPlugin);
|
||||
#[cfg(feature = "log")]
|
||||
{
|
||||
|
|
|
@ -8,26 +8,13 @@
|
|||
#![feature(error_generic_member_access)]
|
||||
|
||||
mod account;
|
||||
pub mod attack;
|
||||
pub mod chat;
|
||||
pub mod chunks;
|
||||
mod client;
|
||||
pub mod configuration;
|
||||
pub mod disconnect;
|
||||
mod entity_query;
|
||||
pub mod events;
|
||||
pub mod interact;
|
||||
pub mod inventory;
|
||||
mod local_player;
|
||||
pub mod mining;
|
||||
pub mod movement;
|
||||
pub mod packet;
|
||||
pub mod ping;
|
||||
mod player;
|
||||
mod plugins;
|
||||
pub mod raw_connection;
|
||||
pub mod respawn;
|
||||
pub mod send_client_end;
|
||||
pub mod task_pool;
|
||||
|
||||
pub use account::{Account, AccountOpts};
|
||||
pub use azalea_protocol::common::client_information::ClientInformation;
|
||||
|
@ -41,3 +28,4 @@ pub use movement::{
|
|||
PhysicsState, SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection,
|
||||
};
|
||||
pub use player::PlayerInfo;
|
||||
pub use plugins::*;
|
||||
|
|
|
@ -2,7 +2,6 @@ use std::{collections::HashMap, io, sync::Arc};
|
|||
|
||||
use azalea_auth::game_profile::GameProfile;
|
||||
use azalea_core::game_type::GameMode;
|
||||
use azalea_entity::Dead;
|
||||
use azalea_protocol::packets::game::c_player_abilities::ClientboundPlayerAbilities;
|
||||
use azalea_world::{Instance, PartialInstance};
|
||||
use bevy_ecs::{component::Component, prelude::*};
|
||||
|
@ -13,10 +12,8 @@ use tokio::sync::mpsc;
|
|||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
events::{Event as AzaleaEvent, LocalPlayerEvents},
|
||||
ClientInformation, PlayerInfo,
|
||||
};
|
||||
use crate::Event as AzaleaEvent;
|
||||
use crate::{ClientInformation, PlayerInfo};
|
||||
|
||||
/// A component that keeps strong references to our [`PartialInstance`] and
|
||||
/// [`Instance`] for local players.
|
||||
|
@ -143,13 +140,6 @@ impl InstanceHolder {
|
|||
}
|
||||
}
|
||||
|
||||
/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
|
||||
pub fn death_event(query: Query<&LocalPlayerEvents, Added<Dead>>) {
|
||||
for local_player_events in &query {
|
||||
local_player_events.send(AzaleaEvent::Death(None)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum HandlePacketError {
|
||||
#[error("{0}")]
|
||||
|
|
|
@ -12,8 +12,8 @@ use bevy_ecs::prelude::*;
|
|||
|
||||
use crate::{client::InConfigState, packet::config::SendConfigPacketEvent};
|
||||
|
||||
pub struct ConfigurationPlugin;
|
||||
impl Plugin for ConfigurationPlugin {
|
||||
pub struct BrandPlugin;
|
||||
impl Plugin for BrandPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
||||
|
@ -22,13 +22,14 @@ impl Plugin for ConfigurationPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_in_configuration_state(
|
||||
pub fn handle_in_configuration_state(
|
||||
query: Query<(Entity, &ClientInformation), Added<InConfigState>>,
|
||||
mut send_packet_events: EventWriter<SendConfigPacketEvent>,
|
||||
) {
|
||||
for (entity, client_information) in query.iter() {
|
||||
let mut brand_data = Vec::new();
|
||||
// they don't have to know :)
|
||||
// azalea pretends to be vanilla everywhere else so it makes sense to lie here
|
||||
// too
|
||||
"vanilla".azalea_write(&mut brand_data).unwrap();
|
||||
send_packet_events.send(SendConfigPacketEvent::new(
|
||||
entity,
|
61
azalea-client/src/plugins/chat/handler.rs
Normal file
61
azalea-client/src/plugins/chat/handler.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use azalea_protocol::packets::{
|
||||
game::{s_chat::LastSeenMessagesUpdate, ServerboundChat, ServerboundChatCommand},
|
||||
Packet,
|
||||
};
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
use super::ChatKind;
|
||||
use crate::packet::game::SendPacketEvent;
|
||||
|
||||
/// Send a chat packet to the server of a specific kind (chat message or
|
||||
/// command). Usually you just want [`SendChatEvent`] instead.
|
||||
///
|
||||
/// Usually setting the kind to `Message` will make it send a chat message even
|
||||
/// if it starts with a slash, but some server implementations will always do a
|
||||
/// command if it starts with a slash.
|
||||
///
|
||||
/// If you're wondering why this isn't two separate events, it's so ordering is
|
||||
/// preserved if multiple chat messages and commands are sent at the same time.
|
||||
#[derive(Event)]
|
||||
pub struct SendChatKindEvent {
|
||||
pub entity: Entity,
|
||||
pub content: String,
|
||||
pub kind: ChatKind,
|
||||
}
|
||||
|
||||
pub fn handle_send_chat_kind_event(
|
||||
mut events: EventReader<SendChatKindEvent>,
|
||||
mut send_packet_events: EventWriter<SendPacketEvent>,
|
||||
) {
|
||||
for event in events.read() {
|
||||
let content = event
|
||||
.content
|
||||
.chars()
|
||||
.filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | '§'))
|
||||
.take(256)
|
||||
.collect::<String>();
|
||||
let packet = match event.kind {
|
||||
ChatKind::Message => ServerboundChat {
|
||||
message: content,
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time shouldn't be before epoch")
|
||||
.as_millis()
|
||||
.try_into()
|
||||
.expect("Instant should fit into a u64"),
|
||||
salt: azalea_crypto::make_salt(),
|
||||
signature: None,
|
||||
last_seen_messages: LastSeenMessagesUpdate::default(),
|
||||
}
|
||||
.into_variant(),
|
||||
ChatKind::Command => {
|
||||
// TODO: chat signing
|
||||
ServerboundChatCommand { command: content }.into_variant()
|
||||
}
|
||||
};
|
||||
|
||||
send_packet_events.send(SendPacketEvent::new(event.entity, packet));
|
||||
}
|
||||
}
|
110
azalea-client/src/chat.rs → azalea-client/src/plugins/chat/mod.rs
Executable file → Normal file
110
azalea-client/src/chat.rs → azalea-client/src/plugins/chat/mod.rs
Executable file → Normal file
|
@ -1,20 +1,13 @@
|
|||
//! Implementations of chat-related features.
|
||||
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
pub mod handler;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use azalea_chat::FormattedText;
|
||||
use azalea_protocol::packets::{
|
||||
game::{
|
||||
c_disguised_chat::ClientboundDisguisedChat,
|
||||
c_player_chat::ClientboundPlayerChat,
|
||||
c_system_chat::ClientboundSystemChat,
|
||||
s_chat::{LastSeenMessagesUpdate, ServerboundChat},
|
||||
s_chat_command::ServerboundChatCommand,
|
||||
},
|
||||
Packet,
|
||||
use azalea_protocol::packets::game::{
|
||||
c_disguised_chat::ClientboundDisguisedChat, c_player_chat::ClientboundPlayerChat,
|
||||
c_system_chat::ClientboundSystemChat,
|
||||
};
|
||||
use bevy_app::{App, Plugin, Update};
|
||||
use bevy_ecs::{
|
||||
|
@ -23,12 +16,27 @@ use bevy_ecs::{
|
|||
prelude::Event,
|
||||
schedule::IntoSystemConfigs,
|
||||
};
|
||||
use handler::{handle_send_chat_kind_event, SendChatKindEvent};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
client::Client,
|
||||
packet::game::{handle_sendpacketevent, SendPacketEvent},
|
||||
};
|
||||
use crate::{client::Client, packet::game::handle_sendpacketevent};
|
||||
|
||||
pub struct ChatPlugin;
|
||||
impl Plugin for ChatPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<SendChatEvent>()
|
||||
.add_event::<SendChatKindEvent>()
|
||||
.add_event::<ChatReceivedEvent>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
handle_send_chat_event,
|
||||
handle_send_chat_kind_event.after(handle_sendpacketevent),
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A chat packet, either a system message or a chat message.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@ -180,23 +188,6 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ChatPlugin;
|
||||
impl Plugin for ChatPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<SendChatEvent>()
|
||||
.add_event::<SendChatKindEvent>()
|
||||
.add_event::<ChatReceivedEvent>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
handle_send_chat_event,
|
||||
handle_send_chat_kind_event.after(handle_sendpacketevent),
|
||||
)
|
||||
.chain(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A client received a chat message packet.
|
||||
#[derive(Event, Debug, Clone)]
|
||||
pub struct ChatReceivedEvent {
|
||||
|
@ -232,63 +223,12 @@ pub fn handle_send_chat_event(
|
|||
}
|
||||
}
|
||||
|
||||
/// Send a chat packet to the server of a specific kind (chat message or
|
||||
/// command). Usually you just want [`SendChatEvent`] instead.
|
||||
///
|
||||
/// Usually setting the kind to `Message` will make it send a chat message even
|
||||
/// if it starts with a slash, but some server implementations will always do a
|
||||
/// command if it starts with a slash.
|
||||
///
|
||||
/// If you're wondering why this isn't two separate events, it's so ordering is
|
||||
/// preserved if multiple chat messages and commands are sent at the same time.
|
||||
#[derive(Event)]
|
||||
pub struct SendChatKindEvent {
|
||||
pub entity: Entity,
|
||||
pub content: String,
|
||||
pub kind: ChatKind,
|
||||
}
|
||||
|
||||
/// A kind of chat packet, either a chat message or a command.
|
||||
pub enum ChatKind {
|
||||
Message,
|
||||
Command,
|
||||
}
|
||||
|
||||
pub fn handle_send_chat_kind_event(
|
||||
mut events: EventReader<SendChatKindEvent>,
|
||||
mut send_packet_events: EventWriter<SendPacketEvent>,
|
||||
) {
|
||||
for event in events.read() {
|
||||
let content = event
|
||||
.content
|
||||
.chars()
|
||||
.filter(|c| !matches!(c, '\x00'..='\x1F' | '\x7F' | '§'))
|
||||
.take(256)
|
||||
.collect::<String>();
|
||||
let packet = match event.kind {
|
||||
ChatKind::Message => ServerboundChat {
|
||||
message: content,
|
||||
timestamp: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("Time shouldn't be before epoch")
|
||||
.as_millis()
|
||||
.try_into()
|
||||
.expect("Instant should fit into a u64"),
|
||||
salt: azalea_crypto::make_salt(),
|
||||
signature: None,
|
||||
last_seen_messages: LastSeenMessagesUpdate::default(),
|
||||
}
|
||||
.into_variant(),
|
||||
ChatKind::Command => {
|
||||
// TODO: chat signing
|
||||
ServerboundChatCommand { command: content }.into_variant()
|
||||
}
|
||||
};
|
||||
|
||||
send_packet_events.send(SendPacketEvent::new(event.entity, packet));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
// MessageSigner, ChatMessageContent, LastSeenMessages
|
||||
// fn sign_message() -> MessageSignature {
|
|
@ -26,8 +26,8 @@ use crate::{
|
|||
InstanceHolder,
|
||||
};
|
||||
|
||||
pub struct ChunkPlugin;
|
||||
impl Plugin for ChunkPlugin {
|
||||
pub struct ChunksPlugin;
|
||||
impl Plugin for ChunksPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
|
@ -5,6 +5,7 @@ use std::sync::Arc;
|
|||
|
||||
use azalea_chat::FormattedText;
|
||||
use azalea_core::tick::GameTick;
|
||||
use azalea_entity::Dead;
|
||||
use azalea_protocol::packets::game::{
|
||||
c_player_combat_kill::ClientboundPlayerCombatKill, ClientboundGamePacket,
|
||||
};
|
||||
|
@ -32,20 +33,25 @@ use crate::{
|
|||
|
||||
// (for contributors):
|
||||
// HOW TO ADD A NEW (packet based) EVENT:
|
||||
// - make a struct that contains an entity field and a data field (look in
|
||||
// packet_handling.rs for examples, also you should end the struct name with
|
||||
// "Event")
|
||||
// - the entity field is the local player entity that's receiving the event
|
||||
// - in packet_handling, you always have a variable called player_entity that
|
||||
// you can use
|
||||
// - add the event struct in the `impl Plugin for PacketHandlerPlugin`
|
||||
// - to get the event writer, you have to get an
|
||||
// EventWriter<SomethingHappenedEvent> from the SystemState (the convention is
|
||||
// to end your variable with the word "events", like "something_events")
|
||||
// - Add it as an ECS event first:
|
||||
// - Make a struct that contains an entity field and some data fields (look
|
||||
// in packet/game/events.rs for examples. These structs should always have
|
||||
// their names end with "Event".
|
||||
// - (the `entity` field is the local player entity that's receiving the
|
||||
// event)
|
||||
// - In the GamePacketHandler, you always have a `player` field that you can
|
||||
// use.
|
||||
// - Add the event struct in PacketPlugin::build
|
||||
// - (in the `impl Plugin for PacketPlugin`)
|
||||
// - To get the event writer, you have to get an EventWriter<ThingEvent>.
|
||||
// Look at other packets in packet/game/mod.rs for examples.
|
||||
//
|
||||
// - then here in this file, add it to the Event enum
|
||||
// - and make an event listener system/function like the other ones and put the
|
||||
// function in the `impl Plugin for EventPlugin`
|
||||
// At this point, you've created a new ECS event. That's annoying for bots to
|
||||
// use though, so you might wanna add it to the Event enum too:
|
||||
// - In this file, add a new variant to that Event enum with the same name
|
||||
// as your event (without the "Event" suffix).
|
||||
// - Create a new system function like the other ones here, and put that
|
||||
// system function in the `impl Plugin for EventsPlugin`
|
||||
|
||||
/// Something that happened in-game, such as a tick passing or chat message
|
||||
/// being sent.
|
||||
|
@ -111,8 +117,8 @@ pub enum Event {
|
|||
#[derive(Component, Deref, DerefMut)]
|
||||
pub struct LocalPlayerEvents(pub mpsc::UnboundedSender<Event>);
|
||||
|
||||
pub struct EventPlugin;
|
||||
impl Plugin for EventPlugin {
|
||||
pub struct EventsPlugin;
|
||||
impl Plugin for EventsPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
||||
|
@ -222,6 +228,13 @@ pub fn death_listener(query: Query<&LocalPlayerEvents>, mut events: EventReader<
|
|||
}
|
||||
}
|
||||
|
||||
/// Send the "Death" event for [`LocalEntity`]s that died with no reason.
|
||||
pub fn dead_component_listener(query: Query<&LocalPlayerEvents, Added<Dead>>) {
|
||||
for local_player_events in &query {
|
||||
local_player_events.send(Event::Death(None)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn keepalive_listener(
|
||||
query: Query<&LocalPlayerEvents>,
|
||||
mut events: EventReader<KeepAliveEvent>,
|
|
@ -22,8 +22,8 @@ use crate::{
|
|||
};
|
||||
|
||||
/// A plugin that allows clients to break blocks in the world.
|
||||
pub struct MinePlugin;
|
||||
impl Plugin for MinePlugin {
|
||||
pub struct MiningPlugin;
|
||||
impl Plugin for MiningPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<StartMiningBlockEvent>()
|
||||
.add_event::<StartMiningBlockWithDirectionEvent>()
|
||||
|
@ -59,6 +59,7 @@ impl Plugin for MinePlugin {
|
|||
}
|
||||
}
|
||||
|
||||
/// The Bevy system set for things related to mining.
|
||||
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
|
||||
pub struct MiningSet;
|
||||
|
14
azalea-client/src/plugins/mod.rs
Normal file
14
azalea-client/src/plugins/mod.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
pub mod attack;
|
||||
pub mod brand;
|
||||
pub mod chat;
|
||||
pub mod chunks;
|
||||
pub mod disconnect;
|
||||
pub mod events;
|
||||
pub mod interact;
|
||||
pub mod inventory;
|
||||
pub mod mining;
|
||||
pub mod movement;
|
||||
pub mod packet;
|
||||
pub mod respawn;
|
||||
pub mod task_pool;
|
||||
pub mod tick_end;
|
|
@ -47,9 +47,9 @@ impl From<MoveEntityError> for MovePlayerError {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct PlayerMovePlugin;
|
||||
pub struct MovementPlugin;
|
||||
|
||||
impl Plugin for PlayerMovePlugin {
|
||||
impl Plugin for MovementPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_event::<StartWalkEvent>()
|
||||
.add_event::<StartSprintEvent>()
|
|
@ -18,7 +18,7 @@ pub mod config;
|
|||
pub mod game;
|
||||
pub mod login;
|
||||
|
||||
pub struct PacketHandlerPlugin;
|
||||
pub struct PacketPlugin;
|
||||
|
||||
pub fn death_event_on_0_health(
|
||||
query: Query<(Entity, &Health), Changed<Health>>,
|
||||
|
@ -34,7 +34,7 @@ pub fn death_event_on_0_health(
|
|||
}
|
||||
}
|
||||
|
||||
impl Plugin for PacketHandlerPlugin {
|
||||
impl Plugin for PacketPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
First,
|
|
@ -60,13 +60,13 @@ fn create_simulation_instance(chunks: ChunkStorage) -> (App, Arc<RwLock<Instance
|
|||
app.add_plugins((
|
||||
azalea_physics::PhysicsPlugin,
|
||||
azalea_entity::EntityPlugin,
|
||||
azalea_client::movement::PlayerMovePlugin,
|
||||
azalea_client::movement::MovementPlugin,
|
||||
super::PathfinderPlugin,
|
||||
crate::BotPlugin,
|
||||
azalea_client::task_pool::TaskPoolPlugin::default(),
|
||||
// for mining
|
||||
azalea_client::inventory::InventoryPlugin,
|
||||
azalea_client::mining::MinePlugin,
|
||||
azalea_client::mining::MiningPlugin,
|
||||
azalea_client::interact::InteractPlugin,
|
||||
))
|
||||
.insert_resource(InstanceContainer {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue