├── base ├── .gitignore ├── Cargo.toml └── src │ └── main.rs ├── ffa ├── .gitignore ├── Cargo.toml └── src │ ├── systems.rs │ └── main.rs ├── ctf ├── tests │ ├── all_tests.rs │ ├── behaviour │ │ ├── mod.rs │ │ └── flags.rs │ └── game-score-reset.rs ├── .gitignore ├── src │ ├── tasks │ │ └── mod.rs │ ├── consts.rs │ ├── component.rs │ ├── event.rs │ ├── resource.rs │ ├── systems │ │ ├── on_player_respawn.rs │ │ ├── on_player_leave.rs │ │ ├── on_game_start.rs │ │ ├── mod.rs │ │ ├── command.rs │ │ └── on_player_join.rs │ ├── lib.rs │ ├── config.rs │ ├── main.rs │ └── shuffle │ │ └── mod.rs └── Cargo.toml ├── configs ├── default.lua ├── hyperspeed.lua ├── infinite-fire.lua └── lazormissiles.lua ├── .gitignore ├── CONTRIBUTORS.md ├── .cargo ├── config.toml └── nightly-config.toml ├── docker-compose.yml ├── rustfmt.toml ├── utils ├── anymap │ ├── Cargo.toml │ ├── src │ │ └── dbg.rs │ └── build.rs ├── kdtree │ ├── Cargo.toml │ ├── src │ │ ├── lib.rs │ │ ├── test.rs │ │ └── aabb.rs │ └── tests │ │ └── random-points.rs └── serde-rlua │ ├── Cargo.toml │ └── src │ ├── lib.rs │ └── error.rs ├── server ├── tests │ ├── all_tests.rs │ ├── behaviour │ │ ├── mod.rs │ │ ├── shoot.rs │ │ ├── admin.rs │ │ ├── despawn.rs │ │ ├── prowler.rs │ │ ├── respawn.rs │ │ ├── upgrades.rs │ │ └── powerups.rs │ ├── utils │ │ └── mod.rs │ └── task.rs ├── src │ ├── resource │ │ ├── stats.rs │ │ ├── mod.rs │ │ └── game_config.rs │ ├── event │ │ ├── missile.rs │ │ ├── collision.rs │ │ ├── mob.rs │ │ ├── packet.rs │ │ └── player.rs │ ├── util │ │ ├── vector.rs │ │ ├── escapes.rs │ │ ├── mod.rs │ │ ├── powerup_spawner.rs │ │ └── spectate.rs │ ├── system │ │ ├── handler │ │ │ ├── on_player_hit.rs │ │ │ ├── on_player_change_plane.rs │ │ │ ├── on_key_packet.rs │ │ │ ├── on_mob_spawn.rs │ │ │ ├── on_mob_despawn.rs │ │ │ ├── on_missile_terrain_collision.rs │ │ │ ├── on_player_score_update.rs │ │ │ ├── mod.rs │ │ │ ├── on_player_spawn.rs │ │ │ ├── on_event_bounce.rs │ │ │ ├── on_missile_despawn.rs │ │ │ ├── on_player_mob_collision.rs │ │ │ ├── on_event_stealth.rs │ │ │ ├── on_event_boost.rs │ │ │ ├── on_player_fire.rs │ │ │ ├── on_player_leave.rs │ │ │ ├── on_player_spectate.rs │ │ │ ├── chat.rs │ │ │ ├── on_event_horizon.rs │ │ │ ├── on_player_respawn.rs │ │ │ ├── on_player_powerup.rs │ │ │ └── on_player_missile_collision.rs │ │ ├── powerups.rs │ │ ├── upgrades.rs │ │ ├── ffa.rs │ │ ├── regen.rs │ │ ├── ctf.rs │ │ ├── scoreboard.rs │ │ ├── despawn.rs │ │ ├── ping.rs │ │ ├── keys.rs │ │ ├── mod.rs │ │ └── admin.rs │ ├── component │ │ ├── keystate.rs │ │ └── effect.rs │ ├── consts │ │ ├── hitcircles.rs │ │ └── mod.rs │ ├── defaults.rs │ ├── macros.rs │ └── lib.rs └── Cargo.toml ├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── auto-dependabot.yml │ ├── build-images.yml │ └── build-project.yml ├── server-macros ├── Cargo.toml └── src │ ├── args.rs │ ├── lib.rs │ └── handler.rs ├── notes ├── votemutes.md ├── goliath-repel.md ├── missile-distance.md ├── missile-damage.md └── movement-prediction.md ├── Cargo.toml ├── dependabot.yml ├── Dockerfile ├── server-config ├── Cargo.toml ├── examples │ ├── export.rs │ └── validate.rs ├── src │ ├── effect.rs │ ├── lib.rs │ ├── common.rs │ ├── powerup.rs │ ├── game.rs │ ├── mob.rs │ ├── error.rs │ ├── script.rs │ ├── missile.rs │ └── util.rs └── tests │ └── validate.rs ├── LICENSE-MIT ├── README.md └── improvements.md /base/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target/ 3 | -------------------------------------------------------------------------------- /ffa/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target/ 3 | -------------------------------------------------------------------------------- /ctf/tests/all_tests.rs: -------------------------------------------------------------------------------- 1 | mod behaviour; 2 | -------------------------------------------------------------------------------- /ctf/tests/behaviour/mod.rs: -------------------------------------------------------------------------------- 1 | mod flags; 2 | -------------------------------------------------------------------------------- /ctf/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target/ 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /configs/default.lua: -------------------------------------------------------------------------------- 1 | -- Empty LUA file means no edits to the config 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | build.bat 3 | .vscode/ 4 | notes/logbot1.log 5 | target/ 6 | -------------------------------------------------------------------------------- /ctf/src/tasks/mod.rs: -------------------------------------------------------------------------------- 1 | mod new_game; 2 | 3 | pub use self::new_game::new_game; 4 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | - STEAMROLLER 2 | - xyz 3 | - congratulatio / jam 4 | - BlackCrawler / lucy 5 | -------------------------------------------------------------------------------- /configs/hyperspeed.lua: -------------------------------------------------------------------------------- 1 | 2 | for idx, plane in pairs(data.planes) do 3 | plane.max_speed = 30.0 4 | end 5 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | 2 | [profile.prod] 3 | inherits = "release" 4 | debug = true 5 | lto = "thin" 6 | codegen-units = 1 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | version: '3' 3 | services: 4 | server: 5 | build: . 6 | ports: 7 | - '0.0.0.0:3501:3501' 8 | 9 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | 2 | tab_spaces = 2 3 | wrap_comments = true 4 | imports_granularity = "module" 5 | group_imports = "stdexternalcrate" 6 | -------------------------------------------------------------------------------- /utils/anymap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "anymap" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | cfg-if = "1.0" 8 | -------------------------------------------------------------------------------- /server/tests/all_tests.rs: -------------------------------------------------------------------------------- 1 | // extern crate airmash as server; 2 | 3 | #[macro_use] 4 | extern crate approx; 5 | 6 | mod behaviour; 7 | mod utils; 8 | -------------------------------------------------------------------------------- /server/tests/behaviour/mod.rs: -------------------------------------------------------------------------------- 1 | mod admin; 2 | mod despawn; 3 | mod powerups; 4 | mod prowler; 5 | mod respawn; 6 | mod shoot; 7 | mod upgrades; 8 | mod visibility; 9 | -------------------------------------------------------------------------------- /server/src/resource/stats.rs: -------------------------------------------------------------------------------- 1 | /// Various server statistics and counters. 2 | #[derive(Debug, Default)] 3 | pub struct ServerStats { 4 | pub num_players: u32, 5 | } 6 | -------------------------------------------------------------------------------- /.cargo/nightly-config.toml: -------------------------------------------------------------------------------- 1 | cargo-features = ["named-profiles"] 2 | 3 | 4 | [profile.publish] 5 | inherits = "release" 6 | debug = true 7 | lto = "thin" 8 | codegen-units = 1 9 | -------------------------------------------------------------------------------- /utils/kdtree/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kdtree" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | pdqselect = "0.1" 8 | 9 | [dev-dependencies] 10 | rand = "0.8" 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | .git/ 3 | 4 | server/target/ 5 | server/.vscode/ 6 | 7 | specgen/target/ 8 | specgen/.vscode/ 9 | 10 | ctf/target/ 11 | ctf/.vscode/ 12 | 13 | base/target/ 14 | base/.vscode/ 15 | 16 | target/ 17 | .vscode/ 18 | -------------------------------------------------------------------------------- /utils/serde-rlua/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serde-rlua" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = "1.0" 8 | rlua = "0.19" 9 | 10 | [dev-dependencies] 11 | serde = { version = "1.0", features = [ "derive" ] } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "docker" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /server-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server-macros" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | syn = { version="1.0", features=["full"] } 11 | quote = "1.0" 12 | proc-macro2 = "1.0" 13 | proc-macro-crate = "1.0" 14 | -------------------------------------------------------------------------------- /notes/votemutes.md: -------------------------------------------------------------------------------- 1 | 2 | # Required Votemutes for Game Size 3 | 4 | The number of votes required to mute a player is given 5 | by the formula 6 | ```js 7 | Math.floor(Math.sqrt(player_count)) + 1 8 | ``` 9 | 10 | where `player_count` is given by 11 | ```js 12 | Players.count()[1] 13 | ``` 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [profile.release] 3 | # Turn on nice backtraces even when in release mode 4 | debug = true 5 | 6 | [workspace] 7 | members = [ 8 | "base", 9 | "ctf", 10 | "ffa", 11 | "server", 12 | "server-config", 13 | "server-macros", 14 | "utils/anymap", 15 | "utils/kdtree", 16 | "utils/serde-rlua" 17 | ] 18 | -------------------------------------------------------------------------------- /notes/goliath-repel.md: -------------------------------------------------------------------------------- 1 | 2 | All planes have repel radius of 180 3 | 4 | missile| biggest repel | smallest non-repel 5 | -------|---------------|------------------ 6 | mohawk | 222 | 222 7 | pred | 189 | 189 8 | goli | 187 | 9 | tornado| 196 or 187 | 10 | tornado triple| 226 | 11 | prow | 216 | -------------------------------------------------------------------------------- /configs/infinite-fire.lua: -------------------------------------------------------------------------------- 1 | 2 | -- Set the firing delay and energy for all planes to 0. 3 | for key, plane in pairs(data.planes) do 4 | plane.fire_delay = 0 5 | plane.fire_energy = 0 6 | end 7 | 8 | -- Also enable infinite fire for tornado multishot 9 | for key, special in pairs(data.specials) do 10 | if special.name == "multishot" then 11 | special.cost = 0 12 | special.delay = 0 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /ctf/src/consts.rs: -------------------------------------------------------------------------------- 1 | use crate::server::component::event::TimerEventType; 2 | 3 | lazy_static! { 4 | pub static ref RESTORE_CONFIG: TimerEventType = TimerEventType::register(); 5 | pub static ref GAME_START_TIMER: TimerEventType = TimerEventType::register(); 6 | pub static ref RETEAM_TIMER: TimerEventType = TimerEventType::register(); 7 | pub static ref SET_GAME_ACTIVE: TimerEventType = TimerEventType::register(); 8 | } 9 | -------------------------------------------------------------------------------- /server/src/event/missile.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | 3 | /// The reason that the missile despawned. 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 5 | pub enum MissileDespawnType { 6 | HitPlayer, 7 | HitTerrain, 8 | LifetimeEnded, 9 | } 10 | 11 | /// A missile despawned. 12 | #[derive(Copy, Clone, Debug)] 13 | pub struct MissileDespawn { 14 | pub missile: Entity, 15 | pub ty: MissileDespawnType, 16 | } 17 | -------------------------------------------------------------------------------- /server/src/util/vector.rs: -------------------------------------------------------------------------------- 1 | use ultraviolet::Vec2; 2 | 3 | pub trait NalgebraExt { 4 | fn zeros() -> Self; 5 | 6 | fn norm(&self) -> f32; 7 | fn norm_squared(&self) -> f32; 8 | } 9 | 10 | impl NalgebraExt for Vec2 { 11 | fn zeros() -> Self { 12 | Self::zero() 13 | } 14 | 15 | fn norm(&self) -> f32 { 16 | self.mag() 17 | } 18 | fn norm_squared(&self) -> f32 { 19 | self.mag_sq() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /utils/kdtree/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | mod aabb; 4 | mod kdtree; 5 | 6 | #[cfg(test)] 7 | mod test; 8 | 9 | pub use self::kdtree::KdTree; 10 | 11 | pub trait Node { 12 | fn position(&self) -> [f32; 2]; 13 | fn radius(&self) -> f32; 14 | } 15 | 16 | impl Node for ([f32; 2], f32) { 17 | fn position(&self) -> [f32; 2] { 18 | self.0 19 | } 20 | 21 | fn radius(&self) -> f32 { 22 | self.1 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /base/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airmash-server-base" 3 | version = "0.0.1" 4 | authors = ["STEAMROLLER"] 5 | license = "Apache-2.0 OR MIT" 6 | description = "Minimal airmash server game mode" 7 | publish = false 8 | repository = 'https://github.com/steamroller-airmash/airmash-server' 9 | edition = "2018" 10 | 11 | [dependencies] 12 | log = "0.4" 13 | env_logger = "0.10" 14 | clap = "3.2.22" 15 | airmash = { path="../server", features = ["mt-network"] } 16 | -------------------------------------------------------------------------------- /ffa/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airmash-server-ffa" 3 | version = "0.0.1" 4 | authors = ["STEAMROLLER"] 5 | license = "Apache-2.0 OR MIT" 6 | description = "Airmash CTF game mode" 7 | publish = false 8 | repository = 'https://github.com/steamroller-airmash/airmash-server' 9 | edition = "2018" 10 | 11 | [dependencies] 12 | log = "0.4" 13 | env_logger = "0.10" 14 | rand = "0.8" 15 | clap = "3.2.22" 16 | serde = "1.0" 17 | color-backtrace = "0.5" 18 | airmash = { path="../server", features = ["mt-network"] } 19 | 20 | -------------------------------------------------------------------------------- /configs/lazormissiles.lua: -------------------------------------------------------------------------------- 1 | 2 | for idx, missile in pairs(data.missiles) do 3 | missile.max_speed = 30.0 4 | missile.base_speed = 30.0 5 | missile.distance = 10000.0 6 | end 7 | 8 | -- Set the firing delay and energy for all planes to 0. 9 | for key, plane in pairs(data.planes) do 10 | plane.fire_delay = 0.1 11 | plane.fire_energy = 0 12 | end 13 | 14 | for key, special in pairs(data.specials) do 15 | if special.name == "multishot" then 16 | special.cost = 0 17 | special.delay = 0.1 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ctf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airmash-server-ctf" 3 | version = "0.0.1" 4 | authors = ["STEAMROLLER"] 5 | license = "Apache-2.0 OR MIT" 6 | publish = false 7 | repository = 'https://github.com/steamroller-airmash/airmash-server' 8 | edition = "2018" 9 | 10 | 11 | [dependencies] 12 | log = "0.4" 13 | env_logger = "0.10" 14 | clap = "3.2.22" 15 | htmlescape = "0.3" 16 | rand = "0.8" 17 | bstr = "0.2" 18 | 19 | lazy_static = "1.4" 20 | smallvec = "1.11" 21 | airmash = { path="../server", features = ["mt-network"] } 22 | 23 | -------------------------------------------------------------------------------- /notes/missile-distance.md: -------------------------------------------------------------------------------- 1 | 2 | This table has the time that it takes missiles to 3 | despawn before being fired. This is without 4 | missile speed upgrades. 5 | 6 | All values were reverse engineered by running 7 | a bot from a digitalocean droplet against 8 | US FFA1. 9 | 10 | | | lifetime (ms) 11 | |-----------------|------------------------ 12 | |predator | 2300 13 | |prowler | 2270 14 | |tornado | 2536 15 | |tornado (triple) | 1572 16 | |mohawk | 2223 17 | |goliath | 3545 18 | -------------------------------------------------------------------------------- /dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | reviewers: 9 | - "steamroller-airmash" 10 | 11 | - package-ecosystem: "cargo" 12 | directory: "/" 13 | schedule: 14 | interval: "daily" 15 | reviewers: 16 | - "steamroller-airmash" 17 | 18 | - package-ecosystem: "docker" 19 | directory: "/" 20 | schedule: 21 | interval: "weekly" 22 | reviewers: 23 | - "steamroller-airmash" 24 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_hit.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::event::PlayerHit; 3 | use crate::AirmashGame; 4 | 5 | #[handler] 6 | fn update_damage(event: &PlayerHit, game: &mut AirmashGame) { 7 | let attacker = match event.attacker { 8 | Some(attacker) => attacker, 9 | None => return, 10 | }; 11 | 12 | let (damage, _) = match game 13 | .world 14 | .query_one_mut::<(&mut TotalDamage, &IsPlayer)>(attacker) 15 | { 16 | Ok(query) => query, 17 | Err(_) => return, 18 | }; 19 | 20 | damage.0 += event.damage; 21 | } 22 | -------------------------------------------------------------------------------- /ctf/src/component.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use airmash::Entity; 4 | 5 | #[derive(Copy, Clone, Debug, Default)] 6 | pub struct IsFlag; 7 | 8 | #[derive(Copy, Clone, Debug, Default)] 9 | pub struct FlagCarrier(pub Option); 10 | 11 | #[derive(Copy, Clone, Debug)] 12 | pub struct LastDrop { 13 | pub player: Option, 14 | pub time: Instant, 15 | } 16 | 17 | #[derive(Copy, Clone, Debug)] 18 | pub struct Flags { 19 | pub red: Entity, 20 | pub blue: Entity, 21 | } 22 | 23 | #[derive(Clone, Copy, Debug)] 24 | pub struct LastReturnTime(pub Instant); 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.61.0-slim-bullseye as build-env 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y dwz \ 5 | && apt-get clean 6 | 7 | WORKDIR /build 8 | COPY . /build 9 | 10 | ARG TARGET 11 | 12 | RUN cargo build --profile prod --bin airmash-server-${TARGET} 13 | RUN mv target/prod/airmash-server-${TARGET} target/airmash-server 14 | RUN dwz -L none -l none --odr target/airmash-server 15 | 16 | FROM debian:bullseye-slim 17 | 18 | COPY --from=build-env /build/target/airmash-server / 19 | 20 | EXPOSE 3501/tcp 21 | ENV RUST_LOG=info 22 | 23 | ENTRYPOINT [ "/airmash-server" ] 24 | -------------------------------------------------------------------------------- /ctf/src/event.rs: -------------------------------------------------------------------------------- 1 | use airmash::Entity; 2 | 3 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 4 | pub enum FlagEventType { 5 | PickUp, 6 | Return, 7 | Capture, 8 | Drop, 9 | } 10 | 11 | #[derive(Copy, Clone, Debug)] 12 | pub struct FlagEvent { 13 | pub ty: FlagEventType, 14 | /// Player that carried out the action (capturer, player that returned) 15 | pub player: Option, 16 | pub flag: Entity, 17 | } 18 | 19 | #[derive(Copy, Clone, Debug)] 20 | pub struct GameStartEvent; 21 | 22 | #[derive(Copy, Clone, Debug)] 23 | pub struct GameEndEvent { 24 | pub winning_team: u16, 25 | } 26 | -------------------------------------------------------------------------------- /ctf/src/resource.rs: -------------------------------------------------------------------------------- 1 | use airmash::AirmashGame; 2 | 3 | #[derive(Copy, Clone, Debug, Default)] 4 | pub struct GameScores { 5 | pub redteam: u8, 6 | pub blueteam: u8, 7 | } 8 | 9 | #[derive(Copy, Clone, Debug, Default)] 10 | pub struct CTFGameStats { 11 | pub red_players: usize, 12 | pub blue_players: usize, 13 | } 14 | 15 | #[derive(Copy, Clone, Debug)] 16 | pub struct GameActive(pub bool); 17 | 18 | pub fn register_all(game: &mut AirmashGame) { 19 | game.resources.insert(GameScores::default()); 20 | game.resources.insert(GameActive(true)); 21 | game.resources.insert(CTFGameStats::default()); 22 | } 23 | -------------------------------------------------------------------------------- /notes/missile-damage.md: -------------------------------------------------------------------------------- 1 | 2 | ### Missile Damage Table 3 | 4 | 5 | | | pred | goli | heli | torn | prow | 6 | |-----|------|------|--------|------|------| 7 | |pred |`0.2` |`0.6` |`0` |`0.33`|`0.33`| 8 | |goli |`0` |`0` |`0` |`0` |`0` | 9 | |heli |`0.6` |`0.8` |`0.4275`|`0.67`|`0.67`| 10 | |torn |`0.2` |`0.6` |`0` |`0.33`|`0.33`| 11 | |prow |`~0.1`|`0.55`|`0` |`0.25`|`0.25`| 12 | ^shooter 13 | 14 | # Special cases for 5 upgrades 15 | 16 | | | heli | goli 17 | |-----|----------|------ 18 | |pred |`0.086275`| `-` 19 | |torn |`0.086275`| `-` 20 | |goli |`-` | 21 | |torn multi| `-` | `0.7` 22 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_change_plane.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::PlanePrototypeRef; 3 | use crate::event::PlayerChangePlane; 4 | use crate::AirmashGame; 5 | 6 | #[handler] 7 | fn send_packet(event: &PlayerChangePlane, game: &mut AirmashGame) { 8 | use crate::protocol::server as s; 9 | 10 | let (&plane, _) = match game 11 | .world 12 | .query_one_mut::<(&PlanePrototypeRef, &IsPlayer)>(event.player) 13 | { 14 | Ok(query) => query, 15 | Err(_) => return, 16 | }; 17 | 18 | game.send_to_all(s::PlayerType { 19 | id: event.player.id() as _, 20 | ty: plane.server_type, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /server/src/event/collision.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | use smallvec::SmallVec; 3 | 4 | /// A collision occurred between a missile and any number of players. 5 | #[derive(Clone, Debug)] 6 | pub struct PlayerMissileCollision { 7 | pub missile: Entity, 8 | pub players: SmallVec<[Entity; 1]>, 9 | } 10 | 11 | /// A collision occurred between a mob and a player. 12 | #[derive(Copy, Clone, Debug)] 13 | pub struct PlayerMobCollision { 14 | pub mob: Entity, 15 | pub player: Entity, 16 | } 17 | 18 | /// A collision occurred between a missile and the terrain. 19 | #[derive(Copy, Clone, Debug)] 20 | pub struct MissileTerrainCollision { 21 | pub missile: Entity, 22 | } 23 | -------------------------------------------------------------------------------- /server/src/event/mob.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | 3 | /// Emitted when a new mob is spawned 4 | #[derive(Clone, Copy, Debug)] 5 | pub struct MobSpawn { 6 | pub mob: Entity, 7 | } 8 | 9 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] 10 | pub enum MobDespawnType { 11 | Expired, 12 | PickUp, 13 | } 14 | 15 | /// Emitted when a mob is despawned. 16 | #[derive(Copy, Clone, Debug)] 17 | pub struct MobDespawn { 18 | pub mob: Entity, 19 | pub ty: MobDespawnType, 20 | } 21 | 22 | /// Emitted when a player picks up a mob. 23 | #[derive(Clone, Copy, Debug)] 24 | pub struct MobPickUp { 25 | pub mob: Entity, 26 | pub player: Entity, 27 | } 28 | -------------------------------------------------------------------------------- /server/src/system/handler/on_key_packet.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::event::{KeyEvent, PacketEvent}; 3 | use crate::protocol::client::Key; 4 | use crate::AirmashGame; 5 | 6 | #[handler] 7 | fn transform_key_event(event: &PacketEvent, game: &mut AirmashGame) { 8 | let (alive, _) = match game 9 | .world 10 | .query_one_mut::<(&IsAlive, &IsPlayer)>(event.entity) 11 | { 12 | Ok(query) => query, 13 | Err(_) => return, 14 | }; 15 | 16 | if !alive.0 { 17 | return; 18 | } 19 | 20 | game.dispatch(KeyEvent { 21 | player: event.entity, 22 | key: event.packet.key, 23 | state: event.packet.state, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/auto-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge compatible dependabot PRs 2 | on: pull_request 3 | permissions: write-all 4 | 5 | jobs: 6 | auto-merge: 7 | if: ${{ github.actor == 'dependabot[bot]' }} 8 | runs-on: ubuntu-latest 9 | steps: 10 | # Validate that the PR is from dependabot 11 | - id: metadata 12 | uses: dependabot/fetch-metadata@v1.1.1 13 | with: 14 | github-token: "${{ secrets.GITHUB_TOKEN }}" 15 | - uses: ahmadnassri/action-dependabot-auto-merge@v2 16 | with: 17 | target: minor 18 | command: squash and merge 19 | github-token: ${{ secrets.USER_TOKEN }} 20 | -------------------------------------------------------------------------------- /ctf/src/systems/on_player_respawn.rs: -------------------------------------------------------------------------------- 1 | use airmash::component::*; 2 | use airmash::event::PlayerRespawn; 3 | use airmash::{AirmashGame, Vector2}; 4 | 5 | use crate::config; 6 | 7 | #[handler(priority = airmash::priority::MEDIUM)] 8 | fn setup_team_and_pos(event: &PlayerRespawn, game: &mut AirmashGame) { 9 | let team = match game.world.get::(event.player) { 10 | Ok(team) => team.0, 11 | Err(_) => return, 12 | }; 13 | 14 | let offset = Vector2::new(rand::random::() - 0.5, rand::random::() - 0.5); 15 | let respawn = config::team_respawn_pos(team) + 400.0 * offset; 16 | 17 | let _ = game.world.insert_one(event.player, Position(respawn)); 18 | } 19 | -------------------------------------------------------------------------------- /server/src/system/handler/on_mob_spawn.rs: -------------------------------------------------------------------------------- 1 | use airmash_protocol::server::MobUpdateStationary; 2 | 3 | use crate::component::*; 4 | use crate::config::MobPrototypeRef; 5 | use crate::event::MobSpawn; 6 | use crate::AirmashGame; 7 | 8 | #[handler] 9 | fn send_packet(event: &MobSpawn, game: &mut AirmashGame) { 10 | let (&mob, &pos, _) = match game 11 | .world 12 | .query_one_mut::<(&MobPrototypeRef, &Position, &IsMob)>(event.mob) 13 | { 14 | Ok(query) => query, 15 | Err(_) => return, 16 | }; 17 | 18 | game.send_to_visible( 19 | pos.0, 20 | MobUpdateStationary { 21 | id: event.mob.id() as _, 22 | ty: mob.server_type, 23 | pos: pos.into(), 24 | }, 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /server-macros/src/args.rs: -------------------------------------------------------------------------------- 1 | use quote::ToTokens; 2 | use syn::parse::Parse; 3 | use syn::{Ident, Token}; 4 | 5 | pub struct AttrArg

{ 6 | pub ident: Ident, 7 | pub equals_token: Token![=], 8 | pub value: P, 9 | } 10 | 11 | impl Parse for AttrArg

{ 12 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 13 | Ok(Self { 14 | ident: input.parse()?, 15 | equals_token: input.parse()?, 16 | value: input.parse()?, 17 | }) 18 | } 19 | } 20 | 21 | impl ToTokens for AttrArg

{ 22 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 23 | self.ident.to_tokens(tokens); 24 | self.equals_token.to_tokens(tokens); 25 | self.value.to_tokens(tokens); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/src/system/handler/on_mob_despawn.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::event::{MobDespawn, MobDespawnType}; 3 | use crate::AirmashGame; 4 | 5 | #[handler] 6 | fn send_packet(event: &MobDespawn, game: &mut AirmashGame) { 7 | use crate::protocol::server::MobDespawn; 8 | use crate::protocol::DespawnType; 9 | 10 | let (&pos, _) = match game.world.query_one_mut::<(&Position, &IsMob)>(event.mob) { 11 | Ok(query) => query, 12 | Err(_) => return, 13 | }; 14 | 15 | let ty = match event.ty { 16 | MobDespawnType::Expired => DespawnType::LifetimeEnded, 17 | MobDespawnType::PickUp => DespawnType::Collided, 18 | }; 19 | 20 | game.send_to_visible( 21 | pos.0, 22 | MobDespawn { 23 | id: event.mob.id() as _, 24 | ty, 25 | }, 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /server/src/system/handler/on_missile_terrain_collision.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::MissilePrototypeRef; 3 | use crate::event::MissileTerrainCollision; 4 | use crate::AirmashGame; 5 | 6 | #[handler] 7 | fn send_despawn_packet(event: &MissileTerrainCollision, game: &mut AirmashGame) { 8 | use crate::protocol::server::MobDespawnCoords; 9 | 10 | let query = game 11 | .world 12 | .query_one_mut::<(&MissilePrototypeRef, &Position, &IsMissile)>(event.missile); 13 | let (&mob, pos, ..) = match query { 14 | Ok(query) => query, 15 | Err(_) => return, 16 | }; 17 | 18 | let packet = MobDespawnCoords { 19 | id: event.missile.id() as _, 20 | ty: mob.server_type, 21 | pos: pos.into(), 22 | }; 23 | game.send_to_visible(packet.pos.into(), packet); 24 | } 25 | -------------------------------------------------------------------------------- /server/tests/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use airmash::protocol::client as c; 2 | use airmash::test::{MockConnection, MockConnectionEndpoint, TestGame}; 3 | use airmash_protocol::ServerPacket; 4 | 5 | pub fn create_login_packet(name: &str) -> c::Login { 6 | c::Login { 7 | protocol: 5, 8 | session: Default::default(), 9 | name: name.into(), 10 | horizon_x: 4000, 11 | horizon_y: 4000, 12 | flag: "UN".into(), 13 | } 14 | } 15 | 16 | pub fn get_login_id(mock: &mut MockConnection) -> u16 { 17 | let packet = mock.next_packet().expect("No packets available"); 18 | 19 | match packet { 20 | ServerPacket::Login(login) => login.id, 21 | _ => panic!("Expected Login packet, got: {:#?}", packet), 22 | } 23 | } 24 | 25 | pub fn create_mock_server() -> (TestGame, MockConnectionEndpoint) { 26 | TestGame::new() 27 | } 28 | -------------------------------------------------------------------------------- /utils/anymap/src/dbg.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use cfg_if::cfg_if; 4 | 5 | #[cfg(anydebug)] 6 | mod with_spec { 7 | use std::fmt; 8 | 9 | pub(super) struct DebugAny(pub T); 10 | 11 | impl fmt::Debug for DebugAny { 12 | default fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 13 | f.write_str("..") 14 | } 15 | } 16 | 17 | impl fmt::Debug for DebugAny 18 | where 19 | T: fmt::Debug, 20 | { 21 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 22 | self.0.fmt(f) 23 | } 24 | } 25 | } 26 | 27 | #[allow(unused_variables)] 28 | pub(crate) fn debug_any(value: &T) -> impl Debug + '_ { 29 | cfg_if! { 30 | if #[cfg(anydebug)] { 31 | self::with_spec::DebugAny(value) 32 | } else { 33 | std::any::type_name::() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server-config" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [features] 7 | script = [ "rlua", "serde-rlua" ] 8 | default = [ "script" ] 9 | 10 | [dependencies] 11 | serde = { version = "1.0", features = [ "derive" ] } 12 | ultraviolet = { version = "0.9", features = ["serde", "mint"] } 13 | 14 | rlua = { version = "0.19", optional = true } 15 | serde-rlua = { path = "../utils/serde-rlua", optional = true } 16 | 17 | [dependencies.protocol] 18 | version = "0.6.2" 19 | package = "airmash-protocol" 20 | features = [ "serde" ] 21 | 22 | [dev-dependencies] 23 | serde_json = "1.0" 24 | serde_path_to_error = "0.1.13" 25 | anyhow = "1.0" 26 | 27 | [[example]] 28 | name = "export" 29 | required-features = ["script"] 30 | 31 | [[example]] 32 | name = "validate" 33 | required-features = ["script"] 34 | -------------------------------------------------------------------------------- /server/tests/behaviour/shoot.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::protocol::{MobType, ServerPacket}; 4 | use airmash::test::TestGame; 5 | 6 | #[test] 7 | fn predator_fires_predator_missile() { 8 | let (mut game, mut mock) = TestGame::new(); 9 | 10 | let mut client = mock.open(); 11 | client.login("test", &mut game); 12 | 13 | game.run_for(Duration::from_secs(1)); 14 | 15 | client.send_key(airmash_protocol::KeyCode::Fire, true); 16 | game.run_once(); 17 | 18 | loop { 19 | match client.next_packet() { 20 | Some(ServerPacket::PlayerFire(evt)) => { 21 | assert_eq!(evt.projectiles.len(), 1); 22 | assert_eq!(evt.projectiles[0].ty, MobType::PredatorMissile); 23 | break; 24 | } 25 | Some(_) => (), 26 | None => panic!("Never received PlayerFire packet"), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/tests/behaviour/admin.rs: -------------------------------------------------------------------------------- 1 | use airmash::component::Position; 2 | use airmash::protocol::client as c; 3 | use airmash::resource::GameConfig; 4 | 5 | #[test] 6 | fn admin_teleport() { 7 | let (mut game, mut mock) = crate::utils::create_mock_server(); 8 | 9 | let mut client = mock.open(); 10 | client.send_login("test"); 11 | 12 | game.run_once(); 13 | 14 | let id = crate::utils::get_login_id(&mut client); 15 | let ent = game.find_entity_by_id(id).unwrap(); 16 | 17 | game.resources.write::().admin_enabled = true; 18 | 19 | client.send(c::Command { 20 | com: "teleport".into(), 21 | data: "0 -700 2200".into(), 22 | }); 23 | 24 | game.run_once(); 25 | 26 | let pos = game.world.get::(ent).unwrap(); 27 | 28 | assert_abs_diff_eq!(pos.x, -700.0, epsilon = 0.1); 29 | assert_abs_diff_eq!(pos.y, 2200.0, epsilon = 0.1); 30 | } 31 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_score_update.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::event::PlayerScoreUpdate; 3 | use crate::AirmashGame; 4 | 5 | #[handler] 6 | pub fn send_packet(event: &PlayerScoreUpdate, game: &mut AirmashGame) { 7 | use crate::protocol::server::ScoreUpdate; 8 | 9 | let (score, earnings, upgrades, kills, deaths, _) = match game.world.query_one_mut::<( 10 | &Score, 11 | &Earnings, 12 | &Upgrades, 13 | &KillCount, 14 | &DeathCount, 15 | &IsPlayer, 16 | )>(event.player) 17 | { 18 | Ok(query) => query, 19 | Err(_) => return, 20 | }; 21 | 22 | let packet = ScoreUpdate { 23 | id: event.player.id() as _, 24 | score: score.0, 25 | earnings: earnings.0, 26 | upgrades: upgrades.unused, 27 | total_deaths: deaths.0, 28 | total_kills: kills.0, 29 | }; 30 | 31 | game.send_to(event.player, packet); 32 | } 33 | -------------------------------------------------------------------------------- /server/src/system/powerups.rs: -------------------------------------------------------------------------------- 1 | use smallvec::SmallVec; 2 | 3 | use crate::component::*; 4 | use crate::event::PowerupExpire; 5 | use crate::AirmashGame; 6 | 7 | pub fn update(game: &mut AirmashGame) { 8 | expire_effects(game); 9 | } 10 | 11 | fn expire_effects(game: &mut AirmashGame) { 12 | let this_frame = game.this_frame(); 13 | let query = game.world.query_mut::<&Effects>().with::(); 14 | 15 | let mut events = SmallVec::<[_; 16]>::new(); 16 | for (ent, effects) in query { 17 | match effects.expiry() { 18 | Some(expiry) if expiry <= this_frame => (), 19 | _ => continue, 20 | }; 21 | 22 | events.push(PowerupExpire { player: ent }); 23 | } 24 | 25 | for event in events { 26 | game.dispatch(event); 27 | game 28 | .world 29 | .get_mut::(event.player) 30 | .unwrap() 31 | .clear_powerup(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build-images.yml: -------------------------------------------------------------------------------- 1 | 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches-ignore: 6 | - 'dependabot/**' 7 | 8 | name: docker 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | target: [ctf, ffa, base] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: docker/setup-buildx-action@v1 19 | 20 | - uses: docker/login-action@v1 21 | with: 22 | registry: ghcr.io 23 | username: ${{ github.actor }} 24 | password: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - uses: docker/build-push-action@v2 27 | with: 28 | context: . 29 | tags: ghcr.io/${{ github.repository_owner }}/server-${{ matrix.target }}:latest 30 | build-args: | 31 | TARGET=${{ matrix.target }} 32 | push: ${{ github.ref == 'refs/heads/master' }} 33 | 34 | -------------------------------------------------------------------------------- /server/src/system/handler/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module is a dedicated space for event handlers. 2 | //! 3 | //! These will all be implicitly registered via the #[handler] proc macro. 4 | //! so the actual source of this module is just a list of other modules. 5 | 6 | mod chat; 7 | mod on_command; 8 | mod on_event_boost; 9 | mod on_event_bounce; 10 | mod on_event_horizon; 11 | mod on_event_stealth; 12 | mod on_key_packet; 13 | mod on_missile_despawn; 14 | mod on_missile_terrain_collision; 15 | mod on_mob_despawn; 16 | mod on_mob_spawn; 17 | mod on_player_change_plane; 18 | mod on_player_fire; 19 | mod on_player_hit; 20 | mod on_player_join; 21 | mod on_player_killed; 22 | mod on_player_leave; 23 | mod on_player_missile_collision; 24 | mod on_player_mob_collision; 25 | mod on_player_powerup; 26 | mod on_player_repel; 27 | mod on_player_respawn; 28 | mod on_player_score_update; 29 | mod on_player_spawn; 30 | mod on_player_spectate; 31 | -------------------------------------------------------------------------------- /server/src/system/upgrades.rs: -------------------------------------------------------------------------------- 1 | use crate::component::{IsPlayer, PrevUpgrades, Upgrades}; 2 | use crate::protocol::server::PlayerUpgrade; 3 | use crate::protocol::UpgradeType; 4 | use crate::AirmashGame; 5 | 6 | pub fn update(game: &mut AirmashGame) { 7 | send_updates_for_outdated(game); 8 | } 9 | 10 | fn send_updates_for_outdated(game: &mut AirmashGame) { 11 | let mut query = game 12 | .world 13 | .query::<(&Upgrades, &mut PrevUpgrades)>() 14 | .with::(); 15 | for (player, (upgrades, prev)) in &mut query { 16 | if *upgrades == prev.0 { 17 | continue; 18 | } 19 | 20 | let packet = PlayerUpgrade { 21 | upgrades: upgrades.unused, 22 | ty: UpgradeType::None, 23 | speed: upgrades.speed, 24 | defense: upgrades.defense, 25 | energy: upgrades.energy, 26 | missile: upgrades.missile, 27 | }; 28 | 29 | if upgrades.speed != prev.0.speed { 30 | game.force_update(player); 31 | } 32 | 33 | game.send_to(player, packet); 34 | prev.0 = *upgrades; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server/tests/task.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::protocol::server::ServerMessage; 4 | use airmash::resource::TaskScheduler; 5 | use airmash::test::*; 6 | use airmash_protocol::ServerPacket; 7 | 8 | #[test] 9 | fn tasks_obey_test_time() { 10 | let (mut game, mut mock) = TestGame::new(); 11 | 12 | let mut conn = mock.open(); 13 | conn.login("test", &mut game); 14 | 15 | unsafe { 16 | let sched = game.resources.read::().clone(); 17 | sched.spawn(move |mut game| async move { 18 | game.sleep_for(Duration::from_secs(5)).await; 19 | 20 | game.send_to_all(ServerMessage { 21 | ty: airmash_protocol::ServerMessageType::Banner, 22 | text: "test-message".into(), 23 | duration: 1000, 24 | }); 25 | }); 26 | } 27 | 28 | game.run_for(Duration::from_secs(7)); 29 | 30 | let found = conn 31 | .packets() 32 | .find(|x| match x { 33 | ServerPacket::ServerMessage(msg) => msg.text == "test-message", 34 | _ => false, 35 | }) 36 | .is_some(); 37 | assert!(found, "Server message not found"); 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 STEAMROLLER 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/src/event/packet.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use hecs::Entity; 4 | 5 | use crate::network::ConnectionId; 6 | use crate::protocol::client as c; 7 | 8 | /// A packet has been recieved from a connection that has been associated with 9 | /// an entity. 10 | /// 11 | /// This will happen for any packet except [`Login`] and [`Backup`] as those are 12 | /// supposed to occur on newly-opened connections. 13 | /// 14 | /// [`Login`]: crate::protocol::client::Login 15 | /// [`Backup`]: crate::protocol::client::Backup 16 | #[derive(Copy, Clone, Debug)] 17 | pub struct PacketEvent

{ 18 | pub conn: ConnectionId, 19 | pub entity: Entity, 20 | // The time at which the packet was received 21 | pub time: Instant, 22 | pub packet: P, 23 | } 24 | 25 | /// A login packet was received from a connection. 26 | /// 27 | /// Note that this doesn't guarantee that the player will log in successfully. 28 | /// If you want to listen for that use the [`PlayerJoin`] event. 29 | /// 30 | /// [`PlayerJoin`]: crate::event::PlayerJoin 31 | #[derive(Clone, Debug)] 32 | pub struct LoginEvent { 33 | pub conn: ConnectionId, 34 | pub packet: c::Login, 35 | } 36 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_spawn.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::event::{PlayerPowerup, PlayerSpawn}; 3 | use crate::resource::{Config, GameConfig}; 4 | use crate::AirmashGame; 5 | 6 | // If GameConfig::always_upgraded is true then we need to stamp over the set of 7 | // upgrades. 8 | #[handler(priority = crate::priority::MEDIUM)] 9 | fn override_player_upgrades(evt: &PlayerSpawn, game: &mut AirmashGame) { 10 | if !game.resources.read::().always_upgraded { 11 | return; 12 | } 13 | 14 | let upgrades = match game.world.query_one_mut::<&mut Upgrades>(evt.player) { 15 | Ok(upgrades) => upgrades, 16 | Err(_) => return, 17 | }; 18 | 19 | upgrades.speed = 5; 20 | upgrades.defense = 5; 21 | upgrades.energy = 5; 22 | upgrades.missile = 5; 23 | } 24 | 25 | #[handler] 26 | fn give_spawn_shield(event: &PlayerSpawn, game: &mut AirmashGame) { 27 | let proto = game 28 | .resources 29 | .read::() 30 | .powerups 31 | .get("spawn-shield") 32 | .copied(); 33 | 34 | if let Some(proto) = proto { 35 | game.dispatch(PlayerPowerup { 36 | player: event.player, 37 | powerup: proto, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/tests/behaviour/despawn.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::component::*; 4 | use airmash::config::GamePrototype; 5 | use airmash::protocol::{DespawnType, ServerPacket}; 6 | use airmash::test::TestGame; 7 | use airmash::util::NalgebraExt; 8 | use airmash::Vector2; 9 | 10 | #[test] 11 | fn upgrade_despawns_on_time() { 12 | let mut config = GamePrototype::default(); 13 | config.view_radius = 1000.0; 14 | 15 | let (mut game, mut mock) = TestGame::with_config(config); 16 | 17 | let mut client = mock.open(); 18 | let ent = client.login("test", &mut game); 19 | 20 | game.world.get_mut::(ent).unwrap().0 = Vector2::zeros(); 21 | let mob = game.spawn_mob( 22 | MobType::Upgrade, 23 | Vector2::new(100.0, 100.0), 24 | Duration::from_secs(5), 25 | ); 26 | 27 | game.run_for(Duration::from_secs(6)); 28 | 29 | loop { 30 | match client.next_packet() { 31 | Some(ServerPacket::MobDespawn(evt)) => { 32 | assert_eq!(evt.ty, DespawnType::LifetimeEnded); 33 | assert_eq!(evt.id as u32, mob.id()); 34 | break; 35 | } 36 | Some(_) => (), 37 | None => panic!("Never recieved MobDespawn packet"), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/src/system/handler/on_event_bounce.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::PlanePrototypeRef; 3 | use crate::event::EventBounce; 4 | use crate::world::AirmashGame; 5 | 6 | #[handler] 7 | fn send_bounce_packet(event: &EventBounce, game: &mut AirmashGame) { 8 | let clock = crate::util::get_current_clock(game); 9 | 10 | let query = game.world.query_one_mut::<( 11 | &Position, 12 | &Velocity, 13 | &Rotation, 14 | &KeyState, 15 | &PlanePrototypeRef, 16 | &Team, 17 | &SpecialActive, 18 | &Effects, 19 | )>(event.player); 20 | let (pos, vel, rot, keystate, plane, team, active, effects) = match query { 21 | Ok(query) => query, 22 | Err(_) => return, 23 | }; 24 | 25 | let packet = crate::protocol::server::EventBounce { 26 | clock, 27 | id: event.player.id() as _, 28 | pos: pos.into(), 29 | rot: rot.0, 30 | speed: vel.into(), 31 | keystate: keystate.to_server(plane, active, effects), 32 | }; 33 | 34 | let team = team.0; 35 | drop(query); 36 | 37 | if keystate.stealthed { 38 | game.send_to_team_visible(team, packet.pos.into(), packet); 39 | } else { 40 | game.send_to_visible(packet.pos.into(), packet); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "airmash" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | [features] 7 | mt-network = [] 8 | 9 | [dependencies] 10 | hecs = "0.7.7" 11 | linkme = "0.3.12" 12 | log = "0.4" 13 | crossbeam-channel = "0.5" 14 | bstr = "0.2" 15 | rand = "0.8" 16 | uuid = { version = "1.4", features = ["v4"] } 17 | smallvec = "1.11" 18 | itertools = "0.11" 19 | slab = "0.4" 20 | httparse = "1.8.0" 21 | humantime = "2.1.0" 22 | mint = "0.5" 23 | ultraviolet = { version = "0.9", features = ["serde", "mint"] } 24 | 25 | tokio = { version="1.29", features=["rt", "sync", "io-util", "macros", "time", "rt-multi-thread"] } 26 | tokio-tungstenite = "0.19.0" 27 | 28 | serde = { version = "1.0", features = ["derive"] } 29 | 30 | airmash-protocol = { version = "0.6.2", features = ["serde"] } 31 | server-macros = { path="../server-macros" } 32 | server-config = { path="../server-config" } 33 | kdtree = { path="../utils/kdtree" } 34 | anymap = { path="../utils/anymap" } 35 | 36 | [dev-dependencies] 37 | approx = "0.5" 38 | 39 | [dependencies.futures-util] 40 | version = "0.3" 41 | default-features = false 42 | features = [ "sink" ] 43 | 44 | [dependencies.futures-task] 45 | version = "0.3" 46 | default-features = false 47 | -------------------------------------------------------------------------------- /ctf/src/systems/on_player_leave.rs: -------------------------------------------------------------------------------- 1 | use airmash::component::*; 2 | use airmash::event::PlayerLeave; 3 | use airmash::AirmashGame; 4 | use smallvec::SmallVec; 5 | 6 | use crate::component::{FlagCarrier, IsFlag}; 7 | use crate::config; 8 | use crate::event::FlagEvent; 9 | use crate::resource::CTFGameStats; 10 | 11 | #[handler] 12 | fn drop_flag(event: &PlayerLeave, game: &mut AirmashGame) { 13 | let query = game.world.query_mut::<&FlagCarrier>().with::(); 14 | 15 | let mut events = SmallVec::<[_; 2]>::new(); 16 | for (flag, carrier) in query { 17 | if carrier.0 != Some(event.player) { 18 | continue; 19 | } 20 | 21 | events.push(FlagEvent { 22 | ty: crate::event::FlagEventType::Drop, 23 | player: Some(event.player), 24 | flag, 25 | }) 26 | } 27 | } 28 | 29 | #[handler] 30 | fn update_player_count(event: &PlayerLeave, game: &mut AirmashGame) { 31 | let mut counts = game.resources.write::(); 32 | 33 | let team = match game.world.get::(event.player) { 34 | Ok(team) => team, 35 | Err(_) => return, 36 | }; 37 | 38 | match team.0 { 39 | config::BLUE_TEAM => counts.blue_players -= 1, 40 | config::RED_TEAM => counts.red_players -= 1, 41 | _ => (), 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/src/system/handler/on_missile_despawn.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::MissilePrototypeRef; 3 | use crate::event::{MissileDespawn, MissileDespawnType}; 4 | use crate::AirmashGame; 5 | 6 | #[handler] 7 | fn send_despawn_packet(event: &MissileDespawn, game: &mut AirmashGame) { 8 | use crate::protocol::{server as s, DespawnType}; 9 | 10 | let (&pos, &mob, ..) = match game 11 | .world 12 | .query_one_mut::<(&Position, &MissilePrototypeRef, &IsMissile)>(event.missile) 13 | { 14 | Ok(query) => query, 15 | Err(_) => return, 16 | }; 17 | 18 | let ty = match event.ty { 19 | MissileDespawnType::HitPlayer => DespawnType::Collided, 20 | MissileDespawnType::HitTerrain => DespawnType::Collided, 21 | MissileDespawnType::LifetimeEnded => DespawnType::LifetimeEnded, 22 | }; 23 | 24 | if event.ty != MissileDespawnType::LifetimeEnded { 25 | game.send_to_visible( 26 | pos.0, 27 | s::MobDespawnCoords { 28 | id: event.missile.id() as _, 29 | pos: pos.into(), 30 | ty: mob.server_type, 31 | }, 32 | ); 33 | } 34 | 35 | game.send_to_visible( 36 | pos.0, 37 | s::MobDespawn { 38 | id: event.missile.id() as _, 39 | ty, 40 | }, 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /utils/kdtree/src/test.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | type DefaultNode = ([f32; 2], f32); 4 | 5 | #[test] 6 | fn empty_tree_has_no_collisions() { 7 | let tree: KdTree = KdTree::default(); 8 | assert_eq!(tree.within([0.0, 0.0], 1000.0).count(), 0); 9 | } 10 | 11 | #[test] 12 | fn points_in_same_location() { 13 | let mut arr = vec![([0.0, 0.0], 10.0), ([0.0, 0.0], 10.0)]; 14 | let tree = KdTree::with_values(&mut arr); 15 | 16 | let query = tree.within([1.0, 1.0], 0.5); 17 | 18 | assert_eq!(query.count(), 2); 19 | } 20 | 21 | #[test] 22 | fn degenerate() { 23 | let mut values = vec![]; 24 | for _ in 0..100 { 25 | values.push(([0.0, 0.0], 0.0)); 26 | } 27 | values.push(([1.0, 1.0], 0.99)); 28 | 29 | let tree = KdTree::with_values(&mut values); 30 | let query = tree.within([0.0, 0.0], 0.0); 31 | 32 | assert_eq!(query.count(), 100); 33 | } 34 | 35 | #[test] 36 | fn regression_bad_dir() { 37 | let mut values = vec![ 38 | ([0.0, 0.0], 0.0), 39 | ([-1.0, -1.0], 0.0), 40 | ([4.0, 1.0], 0.0), 41 | ([2.0, 2.0], 0.0), 42 | ]; 43 | 44 | let tree = KdTree::with_values(&mut values); 45 | let query = tree.within_aabb(-2.0, 0.5, -2.0, 8.0).collect::>(); 46 | 47 | assert_eq!(query.len(), 2, "{:?}", query); 48 | } 49 | -------------------------------------------------------------------------------- /server/src/system/ffa.rs: -------------------------------------------------------------------------------- 1 | //! Event handlers for the FFA scoreboard. 2 | 3 | use std::convert::TryInto; 4 | 5 | use crate::component::*; 6 | use crate::event::PacketEvent; 7 | use crate::protocol::client::ScoreDetailed; 8 | use crate::AirmashGame; 9 | 10 | pub fn register_all(game: &mut AirmashGame) { 11 | game.register(respond_to_packet); 12 | } 13 | 14 | fn respond_to_packet(event: &PacketEvent, game: &mut AirmashGame) { 15 | use crate::protocol::server::{ScoreDetailedFFA, ScoreDetailedFFAEntry}; 16 | 17 | let mut scores = Vec::new(); 18 | let query = game 19 | .world 20 | .query_mut::<( 21 | &Level, 22 | &Score, 23 | &KillCount, 24 | &DeathCount, 25 | &TotalDamage, 26 | &PlayerPing, 27 | )>() 28 | .with::(); 29 | for (ent, (level, score, kills, deaths, damage, ping)) in query { 30 | scores.push(ScoreDetailedFFAEntry { 31 | id: ent.id() as _, 32 | level: level.0, 33 | score: score.0, 34 | kills: kills.0.try_into().unwrap_or(u16::MAX), 35 | deaths: deaths.0.try_into().unwrap_or(u16::MAX), 36 | damage: damage.0, 37 | ping: ping.as_millis().try_into().unwrap_or(u16::MAX), 38 | }); 39 | } 40 | 41 | game.send_to(event.entity, ScoreDetailedFFA { scores }); 42 | } 43 | -------------------------------------------------------------------------------- /server/src/system/regen.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::resource::{LastFrame, ThisFrame}; 3 | use crate::AirmashGame; 4 | 5 | pub fn update(game: &mut AirmashGame) { 6 | run_energy_regen(game); 7 | run_health_regen(game); 8 | } 9 | 10 | fn run_energy_regen(game: &mut AirmashGame) { 11 | let last_frame = game.resources.read::().0; 12 | let this_frame = game.resources.read::().0; 13 | 14 | let query = game 15 | .world 16 | .query_mut::<(&mut Energy, &mut EnergyRegen)>() 17 | .with::(); 18 | 19 | let delta = crate::util::convert_time(this_frame - last_frame); 20 | 21 | for (_, (energy, regen)) in query { 22 | energy.0 += regen.0 * delta; 23 | energy.0 = energy.0.clamp(0.0, 1.0); 24 | } 25 | } 26 | 27 | fn run_health_regen(game: &mut AirmashGame) { 28 | let last_frame = game.resources.read::().0; 29 | let this_frame = game.resources.read::().0; 30 | 31 | let query = game 32 | .world 33 | .query_mut::<(&mut Health, &mut HealthRegen)>() 34 | .with::(); 35 | 36 | let delta = crate::util::convert_time(this_frame - last_frame); 37 | 38 | for (_, (health, regen)) in query { 39 | health.0 += regen.0 * delta; 40 | health.0 = health.0.clamp(0.0, 1.0); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /utils/anymap/build.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::process::{Command, ExitStatus, Stdio}; 3 | use std::{env, fs}; 4 | 5 | const PROBE: &str = r#" 6 | #![feature(specialization)] 7 | 8 | use std::fmt; 9 | 10 | struct DebugAny(T); 11 | 12 | impl fmt::Debug for DebugAny { 13 | default fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 14 | f.write_str("..") 15 | } 16 | } 17 | 18 | impl fmt::Debug for DebugAny 19 | where 20 | T: fmt::Debug 21 | { 22 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 23 | self.0.fmt(f) 24 | } 25 | } 26 | "#; 27 | 28 | fn compile_probe() -> Option { 29 | let rustc = env::var_os("RUSTC")?; 30 | let outdir = env::var_os("OUT_DIR")?; 31 | let probe = Path::new(&outdir).join("probe.rs"); 32 | fs::write(&probe, PROBE).ok()?; 33 | 34 | Command::new(rustc) 35 | .stderr(Stdio::null()) 36 | .arg("--crate-name=anymap_probe") 37 | .arg("--crate-type=lib") 38 | .arg("--emit=metadata") 39 | .arg("--out-dir") 40 | .arg(outdir) 41 | .arg(probe) 42 | .status() 43 | .ok() 44 | } 45 | 46 | fn main() { 47 | match compile_probe() { 48 | Some(status) if status.success() => println!("cargo:rustc-cfg=anydebug"), 49 | _ => (), 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_mob_collision.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::MobPrototypeRef; 3 | use crate::event::{MobDespawn, MobDespawnType, PlayerMobCollision, PlayerPowerup, PowerupExpire}; 4 | use crate::AirmashGame; 5 | 6 | #[handler] 7 | fn dispatch_despawn_event(event: &PlayerMobCollision, game: &mut AirmashGame) { 8 | if !game.world.contains(event.mob) { 9 | return; 10 | } 11 | 12 | game.dispatch(MobDespawn { 13 | ty: MobDespawnType::PickUp, 14 | mob: event.mob, 15 | }); 16 | } 17 | 18 | #[handler(priority = crate::priority::HIGH)] 19 | fn update_player_powerup(event: &PlayerMobCollision, game: &mut AirmashGame) { 20 | let (&mob, _) = match game 21 | .world 22 | .query_one_mut::<(&MobPrototypeRef, &IsMob)>(event.mob) 23 | { 24 | Ok(query) => query, 25 | Err(_) => return, 26 | }; 27 | 28 | let (effects, _) = match game 29 | .world 30 | .query_one_mut::<(&Effects, &IsPlayer)>(event.player) 31 | { 32 | Ok(query) => query, 33 | Err(_) => return, 34 | }; 35 | 36 | if effects.powerup().is_some() { 37 | game.dispatch(PowerupExpire { 38 | player: event.player, 39 | }); 40 | } 41 | 42 | game.dispatch(PlayerPowerup { 43 | player: event.player, 44 | powerup: mob.powerup, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /server-config/examples/export.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use serde::Deserialize; 4 | use server_config::GamePrototype; 5 | 6 | fn main() -> Result<(), Box> { 7 | let args = std::env::args().skip(1).collect::>(); 8 | // let args = vec!["server-prototypes/configs/infinite-fire.lua"]; 9 | 10 | let mut prototype = GamePrototype::default(); 11 | 12 | let lua = rlua::Lua::new(); 13 | lua.context(|lua| -> Result<(), Box> { 14 | for file in args { 15 | let contents = std::fs::read_to_string(file)?; 16 | let value = prototype.patch_direct(lua, &contents)?; 17 | 18 | let mut track = serde_path_to_error::Track::new(); 19 | let de = serde_rlua::Deserializer::new(value); 20 | let de = serde_path_to_error::Deserializer::new(de, &mut track); 21 | 22 | match GamePrototype::deserialize(de) { 23 | Ok(proto) => prototype = proto, 24 | Err(e) => Err(format!( 25 | "Error while deserializing field {}: {}", 26 | track.path(), 27 | e 28 | ))?, 29 | } 30 | } 31 | 32 | Ok(()) 33 | })?; 34 | 35 | let mut stdout = std::io::stdout().lock(); 36 | serde_json::to_writer_pretty(&mut stdout, &prototype)?; 37 | stdout.write_all(b"\n")?; 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /server/src/system/ctf.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | use crate::component::*; 4 | use crate::event::PacketEvent; 5 | use crate::protocol::client::ScoreDetailed; 6 | use crate::AirmashGame; 7 | 8 | pub fn register_all(game: &mut AirmashGame) { 9 | game.register(respond_to_packet); 10 | } 11 | 12 | fn respond_to_packet(event: &PacketEvent, game: &mut AirmashGame) { 13 | use crate::protocol::server::{ScoreDetailedCTF, ScoreDetailedCTFEntry}; 14 | 15 | let mut scores = Vec::new(); 16 | let query = game 17 | .world 18 | .query_mut::<( 19 | &Level, 20 | &Captures, 21 | &Score, 22 | &KillCount, 23 | &DeathCount, 24 | &TotalDamage, 25 | &PlayerPing, 26 | )>() 27 | .with::(); 28 | for (player, (level, captures, score, kills, deaths, damage, ping)) in query { 29 | scores.push(ScoreDetailedCTFEntry { 30 | id: player.id() as _, 31 | level: level.0, 32 | captures: captures.0.try_into().unwrap_or(u16::MAX), 33 | score: score.0, 34 | kills: kills.0.try_into().unwrap_or(u16::MAX), 35 | deaths: deaths.0.try_into().unwrap_or(u16::MAX), 36 | damage: damage.0, 37 | ping: ping.as_millis().try_into().unwrap_or(u16::MAX), 38 | }); 39 | } 40 | 41 | let packet = ScoreDetailedCTF { scores }; 42 | game.send_to(event.entity, packet); 43 | } 44 | -------------------------------------------------------------------------------- /ctf/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Airmash CTF server. 2 | 3 | use airmash::AirmashGame; 4 | 5 | #[macro_use] 6 | extern crate log; 7 | #[macro_use] 8 | extern crate airmash; 9 | 10 | pub mod component; 11 | pub mod config; 12 | pub mod event; 13 | pub mod resource; 14 | pub mod shuffle; 15 | mod systems; 16 | 17 | fn setup_flag_entities(game: &mut AirmashGame) { 18 | use std::time::Instant; 19 | 20 | use airmash::component::*; 21 | 22 | use crate::component::*; 23 | use crate::config::{BLUE_TEAM, RED_TEAM}; 24 | 25 | game.world.spawn(( 26 | Position(config::flag_home_pos(RED_TEAM)), 27 | Team(RED_TEAM), 28 | FlagCarrier(None), 29 | LastDrop { 30 | player: None, 31 | time: Instant::now(), 32 | }, 33 | LastReturnTime(Instant::now()), 34 | IsFlag, 35 | )); 36 | 37 | game.world.spawn(( 38 | Position(config::flag_home_pos(BLUE_TEAM)), 39 | Team(BLUE_TEAM), 40 | FlagCarrier(None), 41 | LastDrop { 42 | player: None, 43 | time: Instant::now(), 44 | }, 45 | LastReturnTime(Instant::now()), 46 | IsFlag, 47 | )); 48 | } 49 | 50 | pub fn setup_ctf_server(game: &mut AirmashGame) { 51 | use airmash::resource::GameType; 52 | 53 | game.resources.insert(GameType::CTF); 54 | 55 | setup_flag_entities(game); 56 | crate::resource::register_all(game); 57 | airmash::system::ctf::register_all(game); 58 | } 59 | -------------------------------------------------------------------------------- /server/src/util/escapes.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use itertools::Itertools; 4 | use smallvec::SmallVec; 5 | 6 | pub(crate) trait StringEscape { 7 | fn escaped(&self) -> AsciiEscapedString; 8 | } 9 | 10 | impl StringEscape for [u8] { 11 | fn escaped(&self) -> AsciiEscapedString { 12 | AsciiEscapedString(self) 13 | } 14 | } 15 | 16 | pub(crate) struct AsciiEscapedString<'a>(&'a [u8]); 17 | 18 | impl<'a> Display for AsciiEscapedString<'a> { 19 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 20 | let mut bytes = self.0; 21 | 22 | loop { 23 | if let Some(len) = bytes 24 | .iter() 25 | .find_position(|&c| match c { 26 | b'"' | b'\'' | b'\\' => false, 27 | 0x20..=0x7E => true, 28 | _ => false, 29 | }) 30 | .map(|x| x.0) 31 | { 32 | f.write_str(unsafe { std::str::from_utf8_unchecked(&bytes[..len]) })?; 33 | bytes = &bytes[len..]; 34 | } 35 | 36 | let next = match bytes.split_first() { 37 | Some((next, rest)) => { 38 | bytes = rest; 39 | *next 40 | } 41 | None => break, 42 | }; 43 | 44 | let temp: SmallVec<[u8; 4]> = std::ascii::escape_default(next).collect(); 45 | f.write_str(unsafe { std::str::from_utf8_unchecked(&temp) })?; 46 | } 47 | 48 | Ok(()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /utils/kdtree/src/aabb.rs: -------------------------------------------------------------------------------- 1 | use std::ops::RangeInclusive; 2 | 3 | #[derive(Clone, Debug)] 4 | pub(crate) struct Aabb { 5 | pub x: RangeInclusive, 6 | pub y: RangeInclusive, 7 | } 8 | 9 | impl Aabb { 10 | pub fn contains(&self, point: [f32; 2]) -> bool { 11 | return point[0] >= *self.x.start() 12 | && point[0] <= *self.x.end() 13 | && point[1] >= *self.y.start() 14 | && point[1] <= *self.y.end(); 15 | } 16 | 17 | pub fn clip(&mut self, range: &Self) { 18 | let min_x = self.x.start().max(*range.x.start()); 19 | let max_x = self.x.end().min(*range.x.end()); 20 | let min_y = self.y.start().max(*range.y.start()); 21 | let max_y = self.y.end().min(*range.y.end()); 22 | 23 | *self = Self { 24 | x: min_x..=max_x, 25 | y: min_y..=max_y, 26 | } 27 | } 28 | 29 | // Grow the range to include the new point 30 | pub fn expand(&mut self, point: [f32; 2]) { 31 | self.x = self.x.start().min(point[0])..=self.x.end().max(point[0]); 32 | self.y = self.y.start().min(point[1])..=self.y.end().max(point[1]); 33 | } 34 | 35 | pub fn empty() -> Self { 36 | Self { 37 | x: 0.0..=0.0, 38 | y: 0.0..=0.0, 39 | } 40 | } 41 | 42 | pub fn is_empty(&self) -> bool { 43 | self.x.is_empty() || self.y.is_empty() 44 | } 45 | } 46 | 47 | impl Default for Aabb { 48 | fn default() -> Self { 49 | Self::empty() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ffa/src/systems.rs: -------------------------------------------------------------------------------- 1 | use airmash::component::*; 2 | use airmash::event::{PlayerJoin, PlayerRespawn}; 3 | use airmash::resource::collision::{LayerSpec, Terrain}; 4 | use airmash::{AirmashGame, Vector2}; 5 | 6 | const SPAWN_TOP_RIGHT: Vector2 = Vector2::new(-1325.0, -4330.0); 7 | const SPAWN_SIZE: Vector2 = Vector2::new(3500.0, 2500.0); 8 | const SPAWN_RADIUS: f32 = 100.0; 9 | 10 | pub fn select_spawn_position(game: &AirmashGame) -> Vector2 { 11 | let terrain = game.resources.read::(); 12 | 13 | loop { 14 | let pos = SPAWN_TOP_RIGHT 15 | + Vector2::new( 16 | rand::random::() * SPAWN_SIZE.x, 17 | rand::random::() * SPAWN_SIZE.y, 18 | ); 19 | 20 | if !terrain.contains(pos, SPAWN_RADIUS, LayerSpec::None) { 21 | break pos; 22 | } 23 | } 24 | } 25 | 26 | #[airmash::handler(priority = airmash::priority::PRE_LOGIN)] 27 | fn choose_join_position(event: &PlayerJoin, game: &mut AirmashGame) { 28 | let spawn_pos = select_spawn_position(game); 29 | 30 | if let Ok(mut pos) = game.world.get_mut::(event.player) { 31 | pos.0 = spawn_pos; 32 | } 33 | } 34 | 35 | #[airmash::handler(priority = airmash::priority::HIGH)] 36 | fn choose_respawn_position(event: &PlayerRespawn, game: &mut AirmashGame) { 37 | let spawn_pos = select_spawn_position(game); 38 | 39 | if let Ok(mut pos) = game.world.get_mut::(event.player) { 40 | pos.0 = spawn_pos; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ctf/tests/behaviour/flags.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::component::Team; 4 | use airmash::protocol::{FlagUpdateType, ServerPacket}; 5 | use airmash::resource::GameConfig; 6 | use airmash::test::TestGame; 7 | use airmash_server_ctf::config::{FLAG_NO_REGRAB_TIME, RED_TEAM}; 8 | 9 | #[test] 10 | fn player_with_flag_has_flagspeed_set() { 11 | let (mut game, mut mock) = TestGame::new(); 12 | airmash_server_ctf::setup_ctf_server(&mut game); 13 | 14 | let mut client = mock.open(); 15 | let entity = client.login("test", &mut game); 16 | 17 | game.resources.write::().admin_enabled = true; 18 | game.world.insert_one(entity, Team(RED_TEAM)).unwrap(); 19 | game.run_for(FLAG_NO_REGRAB_TIME + Duration::from_secs(1)); 20 | 21 | client.send_command("teleport", "0 blue-flag"); 22 | // Drain all packets 23 | let _ = client.packets().count(); 24 | game.run_for(Duration::from_secs(3)); 25 | 26 | let has_picked_up_flag = client 27 | .packets() 28 | .filter_map(|p| match p { 29 | ServerPacket::GameFlag(p) => Some(p), 30 | _ => None, 31 | }) 32 | .any(|p| p.flag == 1 && p.ty == FlagUpdateType::Carrier); 33 | let has_flagspeed_update = client 34 | .packets() 35 | .filter_map(|p| match p { 36 | ServerPacket::PlayerUpdate(p) => Some(p), 37 | _ => None, 38 | }) 39 | .any(|p| p.keystate.flagspeed); 40 | 41 | assert!(has_picked_up_flag); 42 | assert!(has_flagspeed_update); 43 | } 44 | -------------------------------------------------------------------------------- /server/src/component/keystate.rs: -------------------------------------------------------------------------------- 1 | use super::Effects; 2 | use crate::component::SpecialActive; 3 | use crate::config::PlanePrototypeRef; 4 | use crate::protocol::ServerKeyState; 5 | 6 | /// Known key state of a player. 7 | /// 8 | /// This is kept updated based on packets from the client. However, if the 9 | /// client is dead or recently respawned it may be innaccurate. This ends up 10 | /// being corrected the first time the player presses a key. 11 | #[derive(Default, Clone, Debug)] 12 | pub struct KeyState { 13 | pub up: bool, 14 | pub down: bool, 15 | pub left: bool, 16 | pub right: bool, 17 | pub fire: bool, 18 | pub special: bool, 19 | // This might not be the best place to 20 | // keep these, can be moved later if 21 | // necessary 22 | pub stealthed: bool, 23 | } 24 | 25 | impl KeyState { 26 | pub fn strafe(&self, plane: &PlanePrototypeRef) -> bool { 27 | plane.special.is_strafe() && self.special 28 | } 29 | 30 | pub fn to_server( 31 | &self, 32 | plane: &PlanePrototypeRef, 33 | active: &SpecialActive, 34 | effects: &Effects, 35 | ) -> ServerKeyState { 36 | ServerKeyState { 37 | up: self.up, 38 | down: self.down, 39 | left: self.left, 40 | right: self.right, 41 | boost: plane.special.is_boost() && active.0, 42 | strafe: plane.special.is_strafe() && self.special, 43 | stealth: plane.special.is_stealth() && active.0, 44 | flagspeed: effects.fixed_speed().is_some(), 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/tests/behaviour/prowler.rs: -------------------------------------------------------------------------------- 1 | use airmash::component::*; 2 | use airmash::protocol::KeyCode; 3 | use airmash::resource::Config; 4 | use airmash::Vector2; 5 | 6 | #[test] 7 | fn prowler_decloak_on_hit() { 8 | let (mut game, mut mock) = crate::utils::create_mock_server(); 9 | 10 | let &prowler = game 11 | .resources 12 | .read::() 13 | .planes 14 | .get("prowler") 15 | .unwrap(); 16 | 17 | let mut client1 = mock.open(); 18 | let mut client2 = mock.open(); 19 | 20 | client1.send(crate::utils::create_login_packet("test-1")); 21 | client2.send(crate::utils::create_login_packet("test-2")); 22 | 23 | game.run_once(); 24 | 25 | let id1 = crate::utils::get_login_id(&mut client1); 26 | let id2 = crate::utils::get_login_id(&mut client2); 27 | 28 | let ent1 = game.find_entity_by_id(id1).unwrap(); 29 | let ent2 = game.find_entity_by_id(id2).unwrap(); 30 | 31 | game 32 | .world 33 | .insert(ent1, (Position(Vector2::new(0.0, -250.0)), prowler)) 34 | .unwrap(); 35 | game 36 | .world 37 | .insert_one(ent2, Position(Vector2::new(0.0, 0.0))) 38 | .unwrap(); 39 | 40 | client1.send_key(KeyCode::Special, true); 41 | game.run_count(5); 42 | client1.send_key(KeyCode::Special, false); 43 | client2.send_key(KeyCode::Fire, true); 44 | game.run_count(5); 45 | client2.send_key(KeyCode::Fire, false); 46 | 47 | game.run_count(60); 48 | 49 | let active = game.world.get::(ent2).unwrap(); 50 | assert!(!active.0); 51 | } 52 | -------------------------------------------------------------------------------- /server/src/system/handler/on_event_stealth.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::event::EventStealth; 3 | use crate::resource::StartTime; 4 | use crate::AirmashGame; 5 | 6 | #[handler(priority = crate::priority::MEDIUM)] 7 | fn update_player_state(event: &EventStealth, game: &mut AirmashGame) { 8 | let this_frame = game.this_frame(); 9 | let start_time = game.resources.read::().0; 10 | 11 | let (active, last_special, last_update, keystate, _) = match game.world.query_one_mut::<( 12 | &mut SpecialActive, 13 | &mut LastSpecialTime, 14 | &mut LastUpdateTime, 15 | &mut KeyState, 16 | &IsPlayer, 17 | )>(event.player) 18 | { 19 | Ok(query) => query, 20 | Err(_) => return, 21 | }; 22 | 23 | active.0 = event.stealthed; 24 | last_special.0 = this_frame; 25 | last_update.0 = start_time; 26 | keystate.stealthed = event.stealthed; 27 | } 28 | 29 | #[handler] 30 | fn send_packet(event: &EventStealth, game: &mut AirmashGame) { 31 | use crate::protocol::server as s; 32 | 33 | let (&pos, energy, regen, _) = match game 34 | .world 35 | .query_one_mut::<(&Position, &Energy, &EnergyRegen, &IsPlayer)>(event.player) 36 | { 37 | Ok(query) => query, 38 | Err(_) => return, 39 | }; 40 | 41 | let packet = s::EventStealth { 42 | id: event.player.id() as _, 43 | state: event.stealthed, 44 | energy: energy.0, 45 | energy_regen: regen.0, 46 | }; 47 | 48 | if event.stealthed { 49 | game.send_to_visible(pos.0, packet); 50 | } else { 51 | game.send_to(event.player, packet); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server-config/src/effect.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Clone, Debug, Serialize, Deserialize)] 4 | #[non_exhaustive] 5 | #[serde(tag = "type", rename = "kebab-case")] 6 | pub enum EffectPrototype { 7 | Shield { 8 | damage_mult: f32, 9 | }, 10 | Inferno, 11 | FixedSpeed { 12 | speed: f32, 13 | }, 14 | Upgrade { 15 | count: u16, 16 | }, 17 | /// Despawn the mob that just collided. 18 | Despawn, 19 | } 20 | 21 | impl EffectPrototype { 22 | pub const fn shield() -> Self { 23 | Self::Shield { damage_mult: 0.0 } 24 | } 25 | 26 | pub const fn inferno() -> Self { 27 | Self::Inferno 28 | } 29 | 30 | pub const fn flag_speed() -> Self { 31 | Self::FixedSpeed { speed: 5.0 } 32 | } 33 | 34 | pub const fn upgrade() -> Self { 35 | Self::Upgrade { count: 1 } 36 | } 37 | 38 | pub const fn despawn() -> Self { 39 | Self::Despawn 40 | } 41 | } 42 | 43 | impl EffectPrototype { 44 | pub const fn is_shield(&self) -> bool { 45 | matches!(self, Self::Shield { .. }) 46 | } 47 | 48 | pub const fn is_inferno(&self) -> bool { 49 | matches!(self, Self::Inferno) 50 | } 51 | 52 | pub const fn is_fixed_speed(&self) -> bool { 53 | matches!(self, Self::FixedSpeed { .. }) 54 | } 55 | 56 | pub const fn is_upgrade(&self) -> bool { 57 | matches!(self, Self::Upgrade { .. }) 58 | } 59 | 60 | pub const fn is_despawn(&self) -> bool { 61 | matches!(self, Self::Despawn) 62 | } 63 | 64 | pub const fn is_instant(&self) -> bool { 65 | matches!(self, Self::Upgrade { .. } | Self::Despawn) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ctf/src/config.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::util::NalgebraExt; 4 | use airmash::Vector2; 5 | 6 | pub const BLUE_TEAM: u16 = 1; 7 | pub const RED_TEAM: u16 = 2; 8 | 9 | pub const FLAG_RADIUS: f32 = 100.0; 10 | 11 | pub const FLAG_HOME_POS: [Vector2; 2] = [ 12 | // Blue team 13 | Vector2::new(-9670.0, -1470.0), 14 | // Red team 15 | Vector2::new(8600.0, -940.0), 16 | ]; 17 | 18 | pub const TEAM_RESPAWN_POS: [Vector2; 2] = [ 19 | Vector2::new(-8878.0, -2971.0), 20 | Vector2::new(7818.0, -2930.0), 21 | ]; 22 | 23 | pub const FLAG_NO_REGRAB_TIME: Duration = Duration::from_secs(5); 24 | 25 | /// The base score that a player would get if they were 26 | /// the only ones on the server and they capped. This 27 | /// value will be multiplied by the number of players 28 | /// in the server (up to a max of 10 times). 29 | pub const FLAG_CAP_BOUNTY_BASE: u32 = 100; 30 | /// The base score that a winning player would get 31 | /// if they were the only ones on the server. 32 | pub const GAME_WIN_BOUNTY_BASE: u32 = 100; 33 | 34 | pub fn flag_return_pos(team: u16) -> Vector2 { 35 | FLAG_HOME_POS[(2 - team) as usize] 36 | } 37 | pub fn flag_home_pos(team: u16) -> Vector2 { 38 | FLAG_HOME_POS[(team - 1) as usize] 39 | } 40 | pub fn team_respawn_pos(team: u16) -> Vector2 { 41 | TEAM_RESPAWN_POS 42 | .get((team - 1) as usize) 43 | .copied() 44 | .unwrap_or_else(Vector2::zeros) 45 | } 46 | 47 | pub fn flag_message_team(team: u16) -> &'static str { 48 | match team { 49 | BLUE_TEAM => "blueflag", 50 | RED_TEAM => "redflag", 51 | _ => unreachable!(), 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | //! Various utility functions that perform common functions. 2 | 3 | use std::time::{Duration, Instant}; 4 | 5 | use crate::component::{Effects, Upgrades}; 6 | use crate::protocol::Time; 7 | use crate::resource::*; 8 | use crate::{AirmashGame, Vector2}; 9 | 10 | pub(crate) mod escapes; 11 | mod powerup_spawner; 12 | pub mod spectate; 13 | mod vector; 14 | 15 | pub use self::powerup_spawner::PeriodicPowerupSpawner; 16 | pub use self::vector::NalgebraExt; 17 | 18 | pub fn convert_time(dur: Duration) -> Time { 19 | dur.as_secs_f32() * 60.0 20 | } 21 | 22 | pub fn get_time_clock(game: &AirmashGame, time: Instant) -> u32 { 23 | let start_time = game 24 | .resources 25 | .get::() 26 | .expect("StartTime not registered in resources"); 27 | 28 | let duration = time.saturating_duration_since(start_time.0); 29 | (((duration.as_secs() * 1_000_000) + duration.subsec_micros() as u64) / 10) as u32 30 | } 31 | 32 | pub fn get_current_clock(game: &AirmashGame) -> u32 { 33 | let this_frame = game 34 | .resources 35 | .get::() 36 | .expect("ThisFrame not registered in resources"); 37 | 38 | get_time_clock(game, this_frame.0) 39 | } 40 | 41 | pub fn get_server_upgrades(upgrades: &Upgrades, effects: &Effects) -> crate::protocol::Upgrades { 42 | crate::protocol::Upgrades { 43 | speed: upgrades.speed, 44 | shield: effects.has_shield(), 45 | inferno: effects.has_inferno(), 46 | } 47 | } 48 | 49 | pub fn rotate(v: Vector2, angle: f32) -> Vector2 { 50 | let (sin, cos) = angle.sin_cos(); 51 | Vector2::new(v.x * cos - v.y * sin, v.x * sin + v.y * cos) 52 | } 53 | -------------------------------------------------------------------------------- /server/src/util/powerup_spawner.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use crate::event::Frame; 4 | use crate::protocol::MobType; 5 | use crate::{Entity, EventHandler, Vector2}; 6 | 7 | #[derive(Copy, Clone)] 8 | enum SpawnerState { 9 | Spawned(Entity), 10 | Unspawned(Instant), 11 | } 12 | 13 | pub struct PeriodicPowerupSpawner { 14 | state: SpawnerState, 15 | interval: Duration, 16 | mob: MobType, 17 | pos: Vector2, 18 | } 19 | 20 | impl PeriodicPowerupSpawner { 21 | pub fn new(mob: MobType, pos: Vector2, interval: Duration) -> Self { 22 | Self { 23 | mob, 24 | pos, 25 | interval, 26 | state: SpawnerState::Unspawned(Instant::now()), 27 | } 28 | } 29 | 30 | pub fn inferno(pos: Vector2, interval: Duration) -> Self { 31 | Self::new(MobType::Inferno, pos, interval) 32 | } 33 | 34 | pub fn shield(pos: Vector2, interval: Duration) -> Self { 35 | Self::new(MobType::Shield, pos, interval) 36 | } 37 | } 38 | 39 | impl EventHandler for PeriodicPowerupSpawner { 40 | fn on_event(&mut self, _: &Frame, game: &mut crate::AirmashGame) { 41 | let frame = game.this_frame(); 42 | 43 | match self.state { 44 | SpawnerState::Spawned(entity) => { 45 | if !game.world.contains(entity) { 46 | self.state = SpawnerState::Unspawned(frame + self.interval); 47 | } 48 | } 49 | SpawnerState::Unspawned(next) => { 50 | if frame > next { 51 | let entity = game.spawn_mob(self.mob, self.pos, Duration::from_secs(60)); 52 | self.state = SpawnerState::Spawned(entity); 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ctf/src/systems/on_game_start.rs: -------------------------------------------------------------------------------- 1 | use airmash::component::*; 2 | use airmash::event::*; 3 | use airmash::resource::GameConfig; 4 | use airmash::AirmashGame; 5 | use smallvec::SmallVec; 6 | 7 | use crate::component::IsFlag; 8 | use crate::event::{FlagEvent, GameStartEvent}; 9 | use crate::resource::*; 10 | 11 | #[handler] 12 | fn respawn_all_players(_: &GameStartEvent, game: &mut AirmashGame) { 13 | let mut events = Vec::new(); 14 | let query = game 15 | .world 16 | .query_mut::<(&IsSpectating, &IsAlive)>() 17 | .with::(); 18 | for (player, (spec, alive)) in query { 19 | if spec.0 { 20 | continue; 21 | } 22 | 23 | events.push(PlayerRespawn { 24 | player, 25 | alive: alive.0, 26 | }); 27 | } 28 | 29 | game.dispatch_many(events); 30 | } 31 | 32 | #[handler(priority = airmash::priority::MEDIUM)] 33 | fn reset_damage(_: &GameStartEvent, game: &mut AirmashGame) { 34 | // Reset the scores 35 | *game.resources.write::() = GameScores::default(); 36 | 37 | // Allow players to deal damage to each other. 38 | game.resources.write::().allow_damage = true; 39 | 40 | // Allow players to pick up flags again 41 | game.resources.write::().0 = true; 42 | } 43 | 44 | #[handler] 45 | fn reset_flags(_: &GameStartEvent, game: &mut AirmashGame) { 46 | let mut events = SmallVec::<[_; 2]>::new(); 47 | for (flag, ()) in game.world.query_mut::<()>().with::() { 48 | events.push(FlagEvent { 49 | flag, 50 | player: None, 51 | ty: crate::event::FlagEventType::Return, 52 | }); 53 | } 54 | 55 | game.dispatch_many(events); 56 | } 57 | -------------------------------------------------------------------------------- /utils/serde-rlua/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Serde serializers and deserializers for deserializing to and from [`rlua`] 2 | //! [`Value`]s. 3 | //! 4 | //! It is more or less a port of [`rlua_serde`] that has been made to work with 5 | //! the most recent version of [`rlua`] and made to provide better error 6 | //! messages when things fail. 7 | //! 8 | //! [`Value`]: rlua::Value 9 | //! [`rlua_serde`]: https://crates.io/crates/rlua_serde 10 | 11 | #[cfg(test)] 12 | #[macro_use] 13 | extern crate serde; 14 | 15 | mod de; 16 | mod error; 17 | mod ser; 18 | 19 | pub use self::de::LuaDeserializer as Deserializer; 20 | pub use self::error::Error; 21 | pub use self::ser::LuaSerializer as Serializer; 22 | 23 | /// Convert a rust value to a [`Value`]. 24 | /// 25 | /// This is a simple wrapper around `serialize` and [`Serializer`] that returns 26 | /// a [`rlua::Error`] so that it can be more easily used in [`rlua::ToLua`] 27 | /// implementations. 28 | /// 29 | /// [`Value`]: rlua::Value 30 | pub fn to_value<'lua, T>( 31 | lua: rlua::Context<'lua>, 32 | value: &T, 33 | ) -> Result, rlua::Error> 34 | where 35 | T: serde::Serialize, 36 | { 37 | value.serialize(Serializer::new(lua)).map_err(From::from) 38 | } 39 | 40 | /// Convert a [`Value`] to a rust value. 41 | /// 42 | /// This is a simple wrapper around `serialize` and [`Serializer`] that returns 43 | /// a [`rlua::Error`] so that it can be more easily used in [`rlua::FromLua`] 44 | /// implementations. 45 | /// 46 | /// [`Value`]: rlua::Value 47 | pub fn from_value<'lua, 'de, T>(value: rlua::Value<'lua>) -> Result 48 | where 49 | T: serde::Deserialize<'de>, 50 | { 51 | T::deserialize(Deserializer::new(value)).map_err(From::from) 52 | } 53 | -------------------------------------------------------------------------------- /server/tests/behaviour/respawn.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::protocol::{PlaneType, ServerPacket}; 4 | use airmash::test::TestGame; 5 | 6 | #[test] 7 | fn respawn_as_mohawk() { 8 | let (mut game, mut mock) = TestGame::new(); 9 | 10 | let mut client = mock.open(); 11 | client.login("test", &mut game); 12 | 13 | game.run_for(Duration::from_secs(2)); 14 | client.send_command("respawn", "3"); 15 | game.run_once(); 16 | 17 | loop { 18 | match client.next_packet() { 19 | Some(ServerPacket::PlayerType(evt)) => { 20 | assert_eq!(evt.ty, PlaneType::Mohawk); 21 | break; 22 | } 23 | Some(_) => (), 24 | None => panic!("Never received PlayerType packet"), 25 | } 26 | } 27 | } 28 | 29 | /// This test validates that issue #201 is actually fixed. If we send 30 | /// PLAYER_TYPE packets after PLAYER_RESPAWN packets then we'll find that the 31 | /// plane type used by the client for determining whether Q and E are enabled - 32 | /// and only for that - is one behind what it should be. 33 | #[test] 34 | fn player_type_before_player_respawn() { 35 | let (mut game, mut mock) = TestGame::new(); 36 | 37 | let mut client = mock.open(); 38 | client.login("test", &mut game); 39 | 40 | game.run_for(Duration::from_secs(2)); 41 | 42 | // Drain all previously sent packets 43 | let _ = client.packets().count(); 44 | client.send_command("respawn", "3"); 45 | game.run_once(); 46 | 47 | // Verify that PLAYER_TYPE occurs before PLAYER_RESPAWN 48 | assert!(client 49 | .packets() 50 | .any(|p| matches!(p, ServerPacket::PlayerType(_)))); 51 | assert!(client 52 | .packets() 53 | .any(|p| matches!(p, ServerPacket::PlayerRespawn(_)))); 54 | } 55 | -------------------------------------------------------------------------------- /server/src/system/scoreboard.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Reverse; 2 | use std::time::{Duration, Instant}; 3 | 4 | use crate::component::*; 5 | use crate::resource::ThisFrame; 6 | use crate::AirmashGame; 7 | 8 | def_wrappers! { 9 | type LastScoreBoardTime = Instant; 10 | } 11 | 12 | pub fn update(game: &mut AirmashGame) { 13 | send_packets(game); 14 | } 15 | 16 | fn send_packets(game: &mut AirmashGame) { 17 | use crate::protocol::server as s; 18 | 19 | let this_frame = game.resources.read::().0; 20 | let last_sb = game 21 | .resources 22 | .entry::() 23 | .or_insert(LastScoreBoardTime(this_frame)); 24 | 25 | if this_frame < last_sb.0 + Duration::from_secs(2) { 26 | return; 27 | } 28 | 29 | last_sb.0 = this_frame; 30 | 31 | let mut data = Vec::new(); 32 | let query = game 33 | .world 34 | .query_mut::<(&Position, &IsAlive, &Score, &Level, &JoinTime)>() 35 | .with::(); 36 | for (player, (pos, alive, score, level, join_time)) in query { 37 | let low_res_pos = match alive.0 { 38 | true => Some(pos.0), 39 | false => None, 40 | }; 41 | 42 | data.push(( 43 | join_time.0, 44 | s::ScoreBoardData { 45 | id: player.id() as _, 46 | score: score.0, 47 | level: level.0, 48 | }, 49 | s::ScoreBoardRanking { 50 | id: player.id() as _, 51 | pos: low_res_pos.map(|x| x.into()), 52 | }, 53 | )); 54 | } 55 | 56 | data.sort_unstable_by_key(|x| (Reverse(x.1.score), x.0)); 57 | 58 | let packet = s::ScoreBoard { 59 | rankings: data.iter().map(|x| x.2).collect(), 60 | data: data.into_iter().take(10).map(|x| x.1).collect(), 61 | }; 62 | 63 | game.send_to_all(packet); 64 | } 65 | -------------------------------------------------------------------------------- /server/src/consts/hitcircles.rs: -------------------------------------------------------------------------------- 1 | use crate::protocol::PlaneType; 2 | use crate::Vector2; 3 | 4 | type HitCircle = (Vector2, f32); 5 | 6 | macro_rules! hit_circle { 7 | ($x:expr, $y:expr, $r:expr) => { 8 | (Vector2::new($x as f32, $y as f32), $r as f32) 9 | }; 10 | } 11 | 12 | const PRED_HC: &[HitCircle] = &[ 13 | hit_circle!(0, 5, 23), 14 | hit_circle!(0, -15, 15), 15 | hit_circle!(0, -25, 12), 16 | ]; 17 | 18 | const GOLI_HC: &[HitCircle] = &[ 19 | hit_circle!(0, 0, 35), 20 | hit_circle!(50, 14, 16), 21 | hit_circle!(74, 26, 14), 22 | hit_circle!(30, 8, 23), 23 | hit_circle!(63, 22, 15), 24 | hit_circle!(-50, 14, 16), 25 | hit_circle!(-74, 26, 14), 26 | hit_circle!(-30, 8, 23), 27 | hit_circle!(-63, 22, 15), 28 | ]; 29 | 30 | const MOHAWK_HC: &[HitCircle] = &[ 31 | hit_circle!(0, -12, 15), 32 | hit_circle!(0, 0, 17), 33 | hit_circle!(0, 13, 15), 34 | hit_circle!(0, 26, 15), 35 | ]; 36 | 37 | const TORNADO_HC: &[HitCircle] = &[ 38 | hit_circle!(0, 8, 18), 39 | hit_circle!(14, 12, 13), 40 | hit_circle!(-14, 12, 13), 41 | hit_circle!(0, -12, 16), 42 | hit_circle!(0, -26, 14), 43 | hit_circle!(0, -35, 12), 44 | ]; 45 | 46 | const PROWLER_HC: &[HitCircle] = &[ 47 | hit_circle!(0, 11, 25), 48 | hit_circle!(0, -8, 18), 49 | hit_circle!(19, 20, 10), 50 | hit_circle!(-19, 20, 10), 51 | hit_circle!(0, -20, 14), 52 | ]; 53 | 54 | pub fn hitcircles_for_plane(plane: PlaneType) -> &'static [HitCircle] { 55 | match plane { 56 | PlaneType::Predator => PRED_HC, 57 | PlaneType::Goliath => GOLI_HC, 58 | PlaneType::Mohawk => MOHAWK_HC, 59 | PlaneType::Prowler => PROWLER_HC, 60 | PlaneType::Tornado => TORNADO_HC, 61 | _ => panic!("got unexpected plane type {:?}", plane), 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /server/src/system/handler/on_event_boost.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::PlanePrototypeRef; 3 | use crate::event::EventBoost; 4 | use crate::AirmashGame; 5 | 6 | /// When an internal EventBoost occurs we also need to forward it on so that 7 | /// clients know that it has happened as well. 8 | #[handler] 9 | fn send_boost_packet(event: &EventBoost, game: &mut AirmashGame) { 10 | use crate::protocol::server as s; 11 | 12 | let clock = crate::util::get_current_clock(game); 13 | 14 | let mut query = match game 15 | .world 16 | .query_one::<(&Position, &Rotation, &Velocity, &Energy, &EnergyRegen)>(event.player) 17 | { 18 | Ok(query) => query.with::(), 19 | Err(_) => return, 20 | }; 21 | 22 | if let Some((pos, rot, vel, energy, regen)) = query.get() { 23 | let packet = s::EventBoost { 24 | clock, 25 | id: event.player.id() as _, 26 | boost: event.boosting, 27 | pos: pos.into(), 28 | rot: rot.0, 29 | speed: vel.into(), 30 | energy: energy.0, 31 | energy_regen: regen.0, 32 | }; 33 | 34 | game.send_to_visible(pos.0, packet); 35 | } 36 | } 37 | 38 | /// When boosting the player has negative regen. We need to set that. 39 | #[handler] 40 | fn set_player_energy_regen(event: &EventBoost, game: &mut AirmashGame) { 41 | let (regen, plane) = match game 42 | .world 43 | .query_one_mut::<(&mut EnergyRegen, &PlanePrototypeRef)>(event.player) 44 | { 45 | Ok(query) => query, 46 | Err(_) => return, 47 | }; 48 | 49 | let boost = match plane.special.as_boost() { 50 | Some(boost) => boost, 51 | None => return, 52 | }; 53 | 54 | if event.boosting { 55 | regen.0 = -boost.cost; 56 | } else { 57 | regen.0 = plane.energy_regen; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/src/system/despawn.rs: -------------------------------------------------------------------------------- 1 | use smallvec::SmallVec; 2 | 3 | use crate::component::{IsMissile, *}; 4 | use crate::event::{MissileDespawn, MissileDespawnType, MobDespawn, MobDespawnType}; 5 | use crate::util::NalgebraExt; 6 | use crate::AirmashGame; 7 | 8 | pub fn update(game: &mut AirmashGame) { 9 | despawn_missiles(game); 10 | despawn_mobs(game); 11 | } 12 | 13 | /// Missiles despawn after having travelled a configurable distance. Every frame 14 | /// we need to check all missiles for those that should be despawned. 15 | fn despawn_missiles(game: &mut AirmashGame) { 16 | let mut query = game 17 | .world 18 | .query::<(&Position, &MissileTrajectory)>() 19 | .with::(); 20 | 21 | let mut events = SmallVec::<[MissileDespawn; 16]>::new(); 22 | for (ent, (pos, traj)) in query.iter() { 23 | let dist = (pos.0 - traj.start).norm_squared(); 24 | if dist > traj.maxdist * traj.maxdist { 25 | events.push(MissileDespawn { 26 | missile: ent, 27 | ty: MissileDespawnType::LifetimeEnded, 28 | }); 29 | } 30 | } 31 | 32 | drop(query); 33 | 34 | for event in events { 35 | game.dispatch(event); 36 | game.despawn(event.missile); 37 | } 38 | } 39 | 40 | /// Mobs despawn after a configurable amount of time. 41 | fn despawn_mobs(game: &mut AirmashGame) { 42 | let this_frame = game.this_frame(); 43 | 44 | let query = game.world.query_mut::<&Expiry>().with::(); 45 | let events = query 46 | .into_iter() 47 | .filter(|(_, expiry)| expiry.0 < this_frame) 48 | .map(|(mob, _)| MobDespawn { 49 | mob, 50 | ty: MobDespawnType::Expired, 51 | }) 52 | .collect::>(); 53 | 54 | for event in events { 55 | game.dispatch(event); 56 | game.despawn(event.mob); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/src/util/spectate.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use hecs::Entity; 4 | use itertools::Itertools; 5 | 6 | use crate::component::*; 7 | use crate::AirmashGame; 8 | 9 | pub enum SpectateTarget { 10 | Next, 11 | Prev, 12 | Force, 13 | Target(u16), 14 | } 15 | 16 | fn wrapped_compare(ent: Entity) -> impl Fn(&Entity, &Entity) -> Ordering { 17 | move |a, b| { 18 | let aid = a.id().wrapping_sub(ent.id()); 19 | let bid = b.id().wrapping_sub(ent.id()); 20 | 21 | aid.cmp(&bid) 22 | } 23 | } 24 | 25 | pub fn spectate_target( 26 | player: Entity, 27 | target: Option, 28 | spec: SpectateTarget, 29 | game: &AirmashGame, 30 | ) -> Option { 31 | let minmax = game 32 | .world 33 | .query::<&IsAlive>() 34 | .with::() 35 | .iter() 36 | .filter(|&(_, alive)| alive.0) 37 | .filter(|&(e, _)| e != player) 38 | .filter(|&(e, _)| Some(e) != target) 39 | .map(|(e, _)| e) 40 | .minmax_by(wrapped_compare(target.unwrap_or(player))); 41 | 42 | match minmax.into_option() { 43 | Some((min, max)) => { 44 | let default = target.unwrap_or(min); 45 | 46 | Some(match spec { 47 | SpectateTarget::Force => min, 48 | SpectateTarget::Next => min, 49 | SpectateTarget::Prev => max, 50 | SpectateTarget::Target(id) => match game.find_entity_by_id(id) { 51 | Some(entity) => match game.world.query_one::<&IsAlive>(entity) { 52 | Ok(query) => match query.with::().get() { 53 | Some(alive) if alive.0 => entity, 54 | _ => default, 55 | }, 56 | _ => default, 57 | }, 58 | None => match target { 59 | Some(tgt) => tgt, 60 | None => default, 61 | }, 62 | }, 63 | }) 64 | } 65 | None => target, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ctf/src/systems/mod.rs: -------------------------------------------------------------------------------- 1 | use airmash::event::{EventStealth, PacketEvent, PlayerKilled, PlayerLeave, PlayerRespawn}; 2 | use airmash::protocol::client::Command; 3 | use airmash::{AirmashGame, Entity}; 4 | 5 | mod command; 6 | mod on_flag_event; 7 | mod on_frame; 8 | mod on_game_end; 9 | mod on_game_start; 10 | mod on_player_join; 11 | mod on_player_leave; 12 | mod on_player_respawn; 13 | 14 | pub fn drop_carried_flags(player: Entity, game: &mut AirmashGame) { 15 | use airmash::component::IsPlayer; 16 | use smallvec::SmallVec; 17 | 18 | use crate::component::{FlagCarrier, IsFlag}; 19 | use crate::event::FlagEvent; 20 | 21 | if game.world.get::(player).is_err() { 22 | return; 23 | } 24 | 25 | let query = game.world.query_mut::<&FlagCarrier>().with::(); 26 | 27 | let mut events = SmallVec::<[_; 2]>::new(); 28 | for (flag, carrier) in query { 29 | if carrier.0 != Some(player) { 30 | continue; 31 | } 32 | 33 | events.push(FlagEvent { 34 | ty: crate::event::FlagEventType::Drop, 35 | player: Some(player), 36 | flag, 37 | }) 38 | } 39 | 40 | game.dispatch_many(events); 41 | } 42 | 43 | #[handler] 44 | fn drop_on_leave(event: &PlayerLeave, game: &mut AirmashGame) { 45 | drop_carried_flags(event.player, game); 46 | } 47 | 48 | #[handler] 49 | fn drop_on_death(event: &PlayerKilled, game: &mut AirmashGame) { 50 | drop_carried_flags(event.player, game); 51 | } 52 | 53 | #[handler] 54 | fn drop_on_stealth(event: &EventStealth, game: &mut AirmashGame) { 55 | drop_carried_flags(event.player, game); 56 | } 57 | 58 | #[handler] 59 | fn drop_on_respawn(event: &PlayerRespawn, game: &mut AirmashGame) { 60 | drop_carried_flags(event.player, game); 61 | } 62 | 63 | #[handler] 64 | fn drop_on_command(event: &PacketEvent, game: &mut AirmashGame) { 65 | if event.packet.com == "drop" { 66 | drop_carried_flags(event.entity, game); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /server/src/resource/mod.rs: -------------------------------------------------------------------------------- 1 | //! All resource types used within the server. 2 | 3 | use std::collections::{HashMap, HashSet}; 4 | use std::time::Instant; 5 | 6 | use bstr::BString; 7 | use hecs::Entity; 8 | 9 | pub mod collision; 10 | 11 | mod game_config; 12 | mod stats; 13 | 14 | pub use self::game_config::GameConfig; 15 | pub use self::stats::ServerStats; 16 | pub use crate::protocol::GameType; 17 | pub use crate::TaskScheduler; 18 | 19 | pub type Config = crate::config::GameConfig; 20 | 21 | def_wrappers! { 22 | /// Time at which the last frame occurred. 23 | /// 24 | /// This can also be accessed via [`AirmashGame::last_frame`]. 25 | /// 26 | /// [`AirmashGame::last_frame`]: crate::AirmashGame::last_frame 27 | pub type LastFrame = Instant; 28 | 29 | /// Time at which the current frame is occurring. 30 | /// 31 | /// This can also be accessed via [`AirmashGame::this_frame`]. 32 | /// 33 | /// [`AirmashGame::this_frame`]: crate::AirmashGame::this_frame 34 | pub type ThisFrame = Instant; 35 | 36 | /// Time at which the server is started. 37 | /// 38 | /// This also useful as a time that is before any possible value of 39 | /// [`ThisFrame`]. 40 | pub type StartTime = Instant; 41 | } 42 | 43 | def_wrapper_resources! { 44 | /// The name of the current region. 45 | /// 46 | /// This is what is shown to the player in the menu on the top left where 47 | /// they have the option to change servers. 48 | ##[nocopy] 49 | pub type RegionName = String; 50 | 51 | /// Record of the names of players currently within the server. 52 | /// 53 | /// This is used to avoid assigning the same name to multiple players when 54 | /// a new player attempts to log in with an existing name. 55 | ##[nocopy] 56 | pub type TakenNames = HashSet; 57 | 58 | /// Mapping of user-facing ID to existing entities. 59 | ##[nocopy] 60 | pub type EntityMapping = HashMap; 61 | } 62 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_fire.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::MissilePrototypeRef; 3 | use crate::event::PlayerFire; 4 | use crate::AirmashGame; 5 | 6 | #[handler] 7 | pub fn send_player_fire(event: &PlayerFire, game: &mut AirmashGame) { 8 | use crate::protocol::server as s; 9 | 10 | let clock = crate::util::get_current_clock(game); 11 | 12 | let mut projectiles = Vec::with_capacity(event.missiles.len()); 13 | for &missile in event.missiles.iter() { 14 | let mut query = match game 15 | .world 16 | .query_one::<(&Position, &Velocity, &MissilePrototypeRef)>(missile) 17 | { 18 | Ok(query) => query.with::(), 19 | Err(_) => { 20 | warn!("Missile event contained bad missile entity {:?}", missile); 21 | continue; 22 | } 23 | }; 24 | 25 | if let Some((pos, vel, &mob)) = query.get() { 26 | projectiles.push(s::PlayerFireProjectile { 27 | id: missile.id() as _, 28 | pos: pos.into(), 29 | speed: vel.into(), 30 | ty: mob.server_type, 31 | accel: (vel.normalized() * mob.accel).into(), 32 | max_speed: mob.max_speed, 33 | }); 34 | } else { 35 | warn!("Missile {:?} missing required components", missile); 36 | } 37 | } 38 | 39 | let mut query = match game 40 | .world 41 | .query_one::<(&Position, &Energy, &EnergyRegen)>(event.player) 42 | { 43 | Ok(query) => query.with::(), 44 | Err(_) => return, 45 | }; 46 | 47 | if let Some((&pos, energy, regen)) = query.get() { 48 | let packet = s::PlayerFire { 49 | clock, 50 | id: event.player.id() as _, 51 | energy: energy.0, 52 | energy_regen: regen.0, 53 | projectiles, 54 | }; 55 | 56 | drop(query); 57 | 58 | game.send_to_visible(pos.0, packet); 59 | } else { 60 | warn!("Player {:?} missing required components", event.player); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_leave.rs: -------------------------------------------------------------------------------- 1 | use smallvec::SmallVec; 2 | 3 | use crate::component::*; 4 | use crate::event::{PlayerLeave, PlayerSpectate}; 5 | use crate::resource::ServerStats; 6 | use crate::AirmashGame; 7 | 8 | #[handler] 9 | fn send_packet(event: &PlayerLeave, game: &mut AirmashGame) { 10 | use crate::protocol::server as s; 11 | 12 | if game.world.get::(event.player).is_err() { 13 | return; 14 | } 15 | 16 | game.send_to_all(s::PlayerLeave { 17 | id: event.player.id() as _, 18 | }); 19 | } 20 | 21 | #[handler] 22 | fn remove_name(event: &PlayerLeave, game: &mut AirmashGame) { 23 | use crate::resource::TakenNames; 24 | 25 | let mut taken_names = game.resources.write::(); 26 | let name = match game.world.get::(event.player) { 27 | Ok(name) => name, 28 | Err(_) => return, 29 | }; 30 | 31 | taken_names.remove(&name.0); 32 | } 33 | 34 | #[handler] 35 | fn retarget_spectators(event: &PlayerLeave, game: &mut AirmashGame) { 36 | use crate::util::spectate::*; 37 | 38 | let mut query = game.world.query::<&mut Spectating>().with::(); 39 | let mut events = SmallVec::<[_; 8]>::new(); 40 | for (ent, spec) in query.iter() { 41 | if spec.0 == Some(event.player) { 42 | spec.0 = match spectate_target(ent, spec.0, SpectateTarget::Next, game) { 43 | Some(ent) if ent == event.player => Some(ent), 44 | x => x, 45 | }; 46 | 47 | events.push(PlayerSpectate { 48 | player: ent, 49 | was_alive: false, 50 | }); 51 | } 52 | } 53 | 54 | drop(query); 55 | 56 | game.dispatch_many(events); 57 | } 58 | 59 | #[handler] 60 | fn update_server_stats(_: &PlayerLeave, game: &mut AirmashGame) { 61 | use crate::network::NUM_PLAYERS; 62 | 63 | let mut stats = game.resources.write::(); 64 | 65 | stats.num_players -= 1; 66 | NUM_PLAYERS.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); 67 | } 68 | -------------------------------------------------------------------------------- /utils/serde-rlua/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt; 3 | 4 | use rlua::Error as LuaError; 5 | use serde::{de, ser}; 6 | 7 | /// Error for when a conversion between rust values and [`rlua`] [`Value`]s 8 | /// fails. 9 | /// 10 | /// This type is just a wrapper around [`rlua::Error`] that provides some useful 11 | /// trait implementations as needed by the [`Serializer`] and [`Deserializer`] 12 | /// types. Usually you won't want to use this type but instead just convert it 13 | /// directly to [`rlua::Error`]. The [`to_value`] and [`from_value`] helper 14 | /// methods will do this for you. 15 | /// 16 | /// [`Value`]: rlua::Value 17 | /// [`Serializer`]: crate::Serializer 18 | /// [`Deserializer`]: crate::Deserializer 19 | /// [`to_value`]: crate::to_value 20 | /// [`from_value`]: crate::from_value 21 | #[derive(Clone, Debug)] 22 | pub struct Error(LuaError); 23 | 24 | impl From for Error { 25 | fn from(e: LuaError) -> Self { 26 | Self(e) 27 | } 28 | } 29 | 30 | impl From for LuaError { 31 | fn from(e: Error) -> Self { 32 | e.0 33 | } 34 | } 35 | 36 | impl fmt::Display for Error { 37 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 38 | self.0.fmt(f) 39 | } 40 | } 41 | 42 | impl StdError for Error { 43 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 44 | Some(&self.0) 45 | } 46 | } 47 | 48 | impl ser::Error for Error { 49 | fn custom(msg: T) -> Self 50 | where 51 | T: fmt::Display, 52 | { 53 | Self(LuaError::ToLuaConversionError { 54 | from: "serialize", 55 | to: "value", 56 | message: Some(msg.to_string()), 57 | }) 58 | } 59 | } 60 | 61 | impl de::Error for Error { 62 | fn custom(msg: T) -> Self 63 | where 64 | T: fmt::Display, 65 | { 66 | Self(LuaError::FromLuaConversionError { 67 | from: "value", 68 | to: "deserialize", 69 | message: Some(msg.to_string()), 70 | }) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server-config/tests/validate.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "script")] 2 | 3 | use serde::Deserialize; 4 | use server_config::{GameConfig, GamePrototype}; 5 | 6 | #[test] 7 | fn default_config_validates() { 8 | let prototype = GamePrototype::default(); 9 | let lua = rlua::Lua::new(); 10 | 11 | lua.context(|lua| { 12 | let value = prototype 13 | .patch_direct(lua, "") 14 | .expect("Failed to run empty patch script"); 15 | 16 | let mut track = serde_path_to_error::Track::new(); 17 | let de = serde_rlua::Deserializer::new(value); 18 | let de = serde_path_to_error::Deserializer::new(de, &mut track); 19 | 20 | let prototype = GamePrototype::deserialize(de) 21 | .map_err(|e| { 22 | anyhow::Error::new(e).context(format!("error while deserializing field {}", track.path())) 23 | }) 24 | .expect("Failed to deserialize config from lua script"); 25 | 26 | GameConfig::new(prototype).expect("error while validating the config"); 27 | }); 28 | } 29 | 30 | #[test] 31 | fn powerup_with_no_server_type_validates() { 32 | let prototype = GamePrototype::default(); 33 | let lua = rlua::Lua::new(); 34 | 35 | lua.context(|lua| { 36 | let value = prototype 37 | .patch_direct( 38 | lua, 39 | r#" 40 | data.powerups[1].server_type = nil; 41 | "#, 42 | ) 43 | .map_err(anyhow::Error::new) 44 | .expect("Failed to run empty patch script"); 45 | 46 | let mut track = serde_path_to_error::Track::new(); 47 | let de = serde_rlua::Deserializer::new(value); 48 | let de = serde_path_to_error::Deserializer::new(de, &mut track); 49 | 50 | let prototype = GamePrototype::deserialize(de) 51 | .map_err(|e| { 52 | anyhow::Error::new(e).context(format!("error while deserializing field {}", track.path())) 53 | }) 54 | .expect("Failed to deserialize config from lua script"); 55 | 56 | GameConfig::new(prototype).expect("error while validating the config"); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /base/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use airmash::protocol::GameType; 4 | use airmash::resource::RegionName; 5 | use airmash::*; 6 | use clap::arg; 7 | 8 | fn set_default_var(name: &str, value: &str) { 9 | if None == env::var_os(name) { 10 | env::set_var(name, value); 11 | } 12 | } 13 | 14 | fn main() { 15 | let matches = clap::Command::new("airmash-server-base") 16 | .version(env!("CARGO_PKG_VERSION")) 17 | .author("STEAMROLLER") 18 | .about("Airmash Test Server") 19 | .arg(arg!(-c --config [FILE] "Provides an alternate config file")) 20 | .arg(arg!(--port [PORT] "Port that the server will listen on")) 21 | .arg(arg!(--region [REGION] "The region that this server belongs to")) 22 | .get_matches(); 23 | 24 | set_default_var("RUST_BACKTRACE", "full"); 25 | set_default_var("RUST_LOG", "info"); 26 | 27 | env_logger::init(); 28 | 29 | let bind_addr = format!("0.0.0.0:{}", matches.value_of("port").unwrap_or("3501")); 30 | 31 | let mut game = AirmashGame::with_network( 32 | bind_addr 33 | .parse() 34 | .expect("Unable to parse provided network port address"), 35 | ); 36 | game.resources.insert(RegionName( 37 | matches.value_of("region").unwrap_or("default").to_string(), 38 | )); 39 | game.resources.insert(GameType::FFA); 40 | 41 | // Use the FFA scoreboard. 42 | airmash::system::ffa::register_all(&mut game); 43 | 44 | let mut config = airmash::config::GamePrototype::default(); 45 | if let Some(path) = matches.value_of("config") { 46 | let script = match std::fs::read_to_string(path) { 47 | Ok(script) => script, 48 | Err(e) => { 49 | eprintln!("Unable to open config file. Error was {}", e); 50 | std::process::exit(1); 51 | } 52 | }; 53 | 54 | config 55 | .patch(&script) 56 | .expect("Error while running config file"); 57 | } 58 | 59 | game 60 | .resources 61 | .insert(airmash::resource::Config::new(config).unwrap()); 62 | 63 | game.run_until_shutdown(); 64 | } 65 | -------------------------------------------------------------------------------- /ctf/src/systems/command.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::time::Duration; 3 | 4 | use airmash::component::*; 5 | use airmash::event::{PacketEvent, PlayerRespawn}; 6 | use airmash::protocol::client::Command; 7 | use airmash::AirmashGame; 8 | 9 | #[handler] 10 | fn switch_teams(event: &PacketEvent, game: &mut AirmashGame) { 11 | use airmash::protocol::server::{CommandReply, Error, PlayerReteam, PlayerReteamPlayer}; 12 | use airmash::protocol::ErrorType; 13 | 14 | use crate::config::{BLUE_TEAM, RED_TEAM}; 15 | 16 | if event.packet.com != "switch" { 17 | return; 18 | } 19 | 20 | let this_frame = game.this_frame(); 21 | let (team, name, &alive, &last_action, _) = 22 | match game 23 | .world 24 | .query_one_mut::<(&mut Team, &Name, &IsAlive, &LastActionTime, &IsPlayer)>(event.entity) 25 | { 26 | Ok(query) => query, 27 | Err(_) => return, 28 | }; 29 | 30 | if this_frame - last_action.0 < Duration::from_secs(2) { 31 | game.send_to( 32 | event.entity, 33 | Error { 34 | error: ErrorType::IdleRequiredBeforeRespawn, 35 | }, 36 | ); 37 | return; 38 | } 39 | 40 | team.0 = match team.0 { 41 | RED_TEAM => BLUE_TEAM, 42 | BLUE_TEAM => RED_TEAM, 43 | x => x, 44 | }; 45 | 46 | let reteam = PlayerReteam { 47 | players: vec![PlayerReteamPlayer { 48 | id: event.entity.id() as _, 49 | team: team.0, 50 | }], 51 | }; 52 | 53 | let reply = CommandReply { 54 | ty: airmash::protocol::CommandReplyType::ShowInConsole, 55 | text: format!( 56 | "{} has switched to {}", 57 | name.0, 58 | match team.0 { 59 | RED_TEAM => "red team".into(), 60 | BLUE_TEAM => "blue team".into(), 61 | _ => Cow::Owned(format!("team {}", team.0)), 62 | } 63 | ) 64 | .into(), 65 | }; 66 | 67 | game.send_to_all(reteam); 68 | game.send_to_all(reply); 69 | 70 | game.dispatch(PlayerRespawn { 71 | player: event.entity, 72 | alive: alive.0, 73 | }); 74 | } 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # AIRMASH Server 3 | 4 | [![Gitter chat](https://badges.gitter.im/org.png)](https://gitter.im/airmash-server/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link) 5 | 6 | This is an implementation of a server for the game 7 | [AIRMASH](https://airma.sh). As of this moment it 8 | aims to be fully compatible with the official 9 | servers. 10 | 11 | ## Building the server 12 | 13 | The quickest way to start a test server is using 14 | Docker. To do this run 15 | ``` 16 | docker-compose up 17 | ``` 18 | in the root directory of this repository. 19 | 20 | For more in-depth dev work, it will be easier to use a local install 21 | of rust nightly. To install rust see [here](https://www.rust-lang.org/en-US/install.html). 22 | 23 | The central server code is located in `server`. Code for the CTF 24 | game mode is contained within `ctf`, `base` contains a game mode 25 | that has no addition features and should be used for testing. 26 | 27 | To run a basic server locally, do 28 | ``` 29 | cargo run 30 | ``` 31 | within the `base` folder. 32 | 33 | 34 | ### Compiler Version 35 | 36 | CI verifies that each commit compiles with the latest stable version of rust. 37 | 38 | ## Logging in to the server 39 | 40 | To access the server locally, run a server hosting the files 41 | [here](https://github.com/steamroller-airmash/airmash-mirror) locally, 42 | then open that server in a web browser (e.g. `localhost:8000`) and 43 | use as a normal airmash client. 44 | 45 | ## License 46 | 47 | Licensed under either of 48 | 49 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 50 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 51 | 52 | at your option. 53 | 54 | 55 | ### Contribution 56 | 57 | Unless you explicitly state otherwise, any contribution intentionally 58 | submitted for inclusion in the work by you, as defined in the Apache-2.0 59 | license, shall be dual licensed as above, without any additional terms or 60 | conditions. 61 | -------------------------------------------------------------------------------- /server-config/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | 3 | mod common; 4 | mod config; 5 | mod effect; 6 | mod error; 7 | mod game; 8 | mod missile; 9 | mod mob; 10 | mod plane; 11 | mod powerup; 12 | mod special; 13 | mod util; 14 | 15 | #[cfg(feature = "script")] 16 | mod script; 17 | 18 | use std::borrow::Cow; 19 | use std::fmt::Debug; 20 | 21 | pub use self::common::GameConfigCommon; 22 | pub use self::config::GameConfig; 23 | pub use self::effect::EffectPrototype; 24 | pub use self::error::{Path, Segment, ValidationError}; 25 | pub use self::game::GamePrototype; 26 | pub use self::missile::MissilePrototype; 27 | pub use self::mob::MobPrototype; 28 | pub use self::plane::PlanePrototype; 29 | pub use self::powerup::PowerupPrototype; 30 | pub use self::special::*; 31 | 32 | pub type Vector2 = ultraviolet::Vec2; 33 | 34 | mod sealed { 35 | pub trait Sealed {} 36 | } 37 | 38 | use self::sealed::Sealed; 39 | 40 | pub trait PrototypeRef<'a>: Sealed { 41 | // Any traits we want to have automatically derived on the prototypes must be 42 | // mirrored here and the concrete instantiations must also derive them. 43 | type MissileRef: Clone + Debug + 'a; 44 | type SpecialRef: Clone + Debug + 'a; 45 | type PlaneRef: Clone + Debug + 'a; 46 | type MobRef: Clone + Debug + 'a; 47 | type PowerupRef: Clone + Debug + 'a; 48 | } 49 | 50 | #[derive(Copy, Clone, Debug)] 51 | pub enum StringRef {} 52 | 53 | #[derive(Copy, Clone, Debug)] 54 | pub enum PtrRef {} 55 | 56 | impl Sealed for StringRef {} 57 | impl Sealed for PtrRef {} 58 | 59 | impl<'a> PrototypeRef<'a> for StringRef { 60 | type MissileRef = Cow<'a, str>; 61 | type SpecialRef = Cow<'a, str>; 62 | type PowerupRef = Cow<'a, str>; 63 | type PlaneRef = Cow<'a, str>; 64 | type MobRef = Cow<'a, str>; 65 | } 66 | 67 | impl<'a> PrototypeRef<'a> for PtrRef { 68 | type MissileRef = &'a MissilePrototype; 69 | type SpecialRef = &'a SpecialPrototype<'a, Self>; 70 | type PowerupRef = &'a PowerupPrototype; 71 | type PlaneRef = &'a PlanePrototype<'a, Self>; 72 | type MobRef = &'a MobPrototype<'a, Self>; 73 | } 74 | -------------------------------------------------------------------------------- /server-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod args; 2 | mod handler; 3 | 4 | /// Automatically register a static function as an event handler. 5 | /// 6 | /// This macro is meant to be placed on a function definition. Optionally, it 7 | /// can also take a priority with which to register the event handler. This 8 | /// priority can be any const expression evaluating to an `i32`. The attribute 9 | /// macro expects that the first parameter is a reference to the type of the 10 | /// event that the function is supposed to handle. Any struct should 11 | /// work here as long as it meets the requirements for the `Event` trait. 12 | /// 13 | /// # Caveats 14 | /// Internally this macro uses the [`linkme`] crate. `linkme` has an 15 | /// [issue](https://github.com/dtolnay/linkme/issues/31) where if a module 16 | /// doesn't have anything else that is used by the program then it will be 17 | /// dropped incorrectly even if it contains elements that would be part of the 18 | /// linked slice. In practical terms, this means that modules that contain only 19 | /// handlers registered via `#[handler]` will be incorrectly dropped until we 20 | /// get a rust version where the compiler bug is fixed. 21 | /// 22 | /// # Example 23 | /// ```ignore 24 | /// # struct MyEvent; 25 | /// # struct AirmashGame; // We can't import airmash here 26 | /// const MY_CUSTOM_PRIORITY: i32 = 335; 27 | /// 28 | /// // Here we create an event handler with the default priority. 29 | /// #[handler] 30 | /// fn my_first_handler(event: &MyEvent, game: &mut AirmashGame) { 31 | /// // ... do stuff here 32 | /// } 33 | /// 34 | /// // Create an event handler with a custom priority. Note that we can use 35 | /// // any expression that is const-evaluatable. 36 | /// #[handler(priority = MY_CUSTOM_PRIORITY)] 37 | /// fn my_custom_handler(event: &MyEvent, game: &mut AirmashGame) { 38 | /// // .. do stuff here 39 | /// } 40 | /// ``` 41 | /// 42 | /// [`linkme`]: https://docs.rs/linkme 43 | #[proc_macro_attribute] 44 | pub fn handler( 45 | attr: proc_macro::TokenStream, 46 | item: proc_macro::TokenStream, 47 | ) -> proc_macro::TokenStream { 48 | handler::handler(attr, item) 49 | } 50 | -------------------------------------------------------------------------------- /utils/kdtree/tests/random-points.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use kdtree::KdTree; 4 | 5 | fn rand_coord(range: f32) -> f32 { 6 | (rand::random::() - 0.5) * 2.0 * range 7 | } 8 | 9 | fn rand_radius(range: f32) -> f32 { 10 | rand::random::() * range 11 | } 12 | 13 | struct Node { 14 | point: [f32; 2], 15 | radius: f32, 16 | id: usize, 17 | } 18 | 19 | impl kdtree::Node for Node { 20 | fn position(&self) -> [f32; 2] { 21 | self.point 22 | } 23 | 24 | fn radius(&self) -> f32 { 25 | self.radius 26 | } 27 | } 28 | 29 | #[test] 30 | fn test_random_points() { 31 | let mut points = vec![]; 32 | let mut expected = 0; 33 | let coord_range = 1000.0; 34 | let radius_range = 50.0; 35 | 36 | let query_radius = 500.0; 37 | 38 | for _ in 0..10000 { 39 | let point = [rand_coord(coord_range), rand_coord(coord_range)]; 40 | let radius = rand_radius(radius_range); 41 | 42 | if (point[0] * point[0] + point[1] * point[1]).sqrt() < query_radius + radius { 43 | expected += 1; 44 | } 45 | 46 | points.push((point, radius)); 47 | } 48 | 49 | let tree = KdTree::with_values(&mut points); 50 | let iter = tree.within([0.0, 0.0], query_radius); 51 | assert_eq!(iter.count(), expected); 52 | 53 | for &([x, y], r) in tree.within([0.0, 0.0], query_radius) { 54 | assert!((x * x + y * y).sqrt() < query_radius + r); 55 | } 56 | } 57 | 58 | #[test] 59 | fn test_random_points_2() { 60 | let mut points = vec![]; 61 | let mut expected = HashSet::new(); 62 | let coord_range = 1000.0; 63 | let radius_range = 50.0; 64 | 65 | let query_radius = 500.0; 66 | 67 | for id in 0..10000 { 68 | let point = [rand_coord(coord_range), rand_coord(coord_range)]; 69 | let radius = rand_radius(radius_range); 70 | 71 | if (point[0] * point[0] + point[1] * point[1]).sqrt() < query_radius + radius { 72 | expected.insert(id); 73 | } 74 | 75 | points.push(Node { point, radius, id }); 76 | } 77 | 78 | let tree = KdTree::with_values(&mut points); 79 | 80 | for node in tree.within([0.0, 0.0], query_radius) { 81 | assert!(expected.contains(&node.id)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /server-config/src/common.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::time::Duration; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::util::duration; 7 | use crate::{PlanePrototype, PrototypeRef, PtrRef, StringRef, ValidationError}; 8 | 9 | /// Common fields that are just copied directly between [`GamePrototype`] and 10 | /// [`GameConfig`]. 11 | /// 12 | /// [`GamePrototype`]: crate::GamePrototype 13 | /// [`GameConfig`]: crate::GameConfig 14 | #[derive(Clone, Debug, Serialize, Deserialize)] 15 | #[serde(deny_unknown_fields)] 16 | #[serde(bound( 17 | serialize = "Ref::PlaneRef: Serialize", 18 | deserialize = "Ref::PlaneRef: Deserialize<'de>" 19 | ))] 20 | pub struct GameConfigCommon<'a, Ref: PrototypeRef<'a>> { 21 | /// The default plane that a player joining the game will get unless the 22 | /// server overrides it. 23 | pub default_plane: Ref::PlaneRef, 24 | 25 | /// The radius in which the player can observe events happening. 26 | pub view_radius: f32, 27 | 28 | #[serde(with = "duration")] 29 | pub respawn_delay: Duration, 30 | } 31 | 32 | impl GameConfigCommon<'_, StringRef> { 33 | pub const fn new() -> Self { 34 | Self { 35 | default_plane: Cow::Borrowed("predator"), 36 | view_radius: 2250.0, 37 | respawn_delay: Duration::from_secs(2), 38 | } 39 | } 40 | 41 | pub(crate) fn resolve<'a>( 42 | self, 43 | planes: &'a [PlanePrototype<'a, PtrRef>], 44 | ) -> Result, ValidationError> { 45 | let default_plane = 46 | planes 47 | .iter() 48 | .find(|p| p.name == self.default_plane) 49 | .ok_or(ValidationError::custom( 50 | "default_plane", 51 | format_args!( 52 | "default_plane refers to a plane prototype `{}` which does not exist", 53 | self.default_plane 54 | ), 55 | ))?; 56 | 57 | Ok(GameConfigCommon { 58 | default_plane, 59 | view_radius: self.view_radius, 60 | respawn_delay: self.respawn_delay, 61 | }) 62 | } 63 | } 64 | 65 | impl Default for GameConfigCommon<'_, StringRef> { 66 | fn default() -> Self { 67 | Self::new() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_spectate.rs: -------------------------------------------------------------------------------- 1 | use smallvec::SmallVec; 2 | 3 | use crate::component::*; 4 | use crate::event::PlayerSpectate; 5 | use crate::util::NalgebraExt; 6 | use crate::{AirmashGame, Vector2}; 7 | 8 | #[handler(priority = crate::priority::MEDIUM)] 9 | fn update_player(event: &PlayerSpectate, game: &mut AirmashGame) { 10 | let (spec, alive, _) = match game 11 | .world 12 | .query_one_mut::<(&mut IsSpectating, &mut IsAlive, &IsPlayer)>(event.player) 13 | { 14 | Ok(query) => query, 15 | Err(_) => return, 16 | }; 17 | 18 | spec.0 = true; 19 | alive.0 = false; 20 | } 21 | 22 | #[handler] 23 | fn send_packets(event: &PlayerSpectate, game: &mut AirmashGame) { 24 | use crate::protocol::server::{GameSpectate, PlayerKill}; 25 | 26 | let (&pos, &target, _) = match game 27 | .world 28 | .query_one_mut::<(&Position, &Spectating, &IsPlayer)>(event.player) 29 | { 30 | Ok(query) => query, 31 | Err(_) => return, 32 | }; 33 | 34 | // If the player was already dead then we don't need to despawn their plane. 35 | if event.was_alive { 36 | game.send_to_visible( 37 | pos.0, 38 | PlayerKill { 39 | id: event.player.id() as _, 40 | killer: None, 41 | pos: Vector2::zeros().into(), 42 | }, 43 | ); 44 | } 45 | 46 | game.send_to( 47 | event.player, 48 | GameSpectate { 49 | id: target.0.unwrap_or(event.player).id() as _, 50 | }, 51 | ); 52 | } 53 | 54 | #[handler] 55 | fn retarget_spectators(event: &PlayerSpectate, game: &mut AirmashGame) { 56 | use crate::util::spectate::*; 57 | 58 | let mut query = game.world.query::<&mut Spectating>().with::(); 59 | let mut events = SmallVec::<[_; 8]>::new(); 60 | for (ent, spec) in query.iter() { 61 | if spec.0 == Some(event.player) { 62 | spec.0 = match spectate_target(ent, spec.0, SpectateTarget::Next, game) { 63 | Some(ent) if ent == event.player => Some(ent), 64 | x => x, 65 | }; 66 | 67 | events.push(PlayerSpectate { 68 | player: ent, 69 | was_alive: false, 70 | }); 71 | } 72 | } 73 | 74 | drop(query); 75 | 76 | game.dispatch_many(events); 77 | } 78 | -------------------------------------------------------------------------------- /server/src/resource/game_config.rs: -------------------------------------------------------------------------------- 1 | /// Flags to enable and/or disable engine features. 2 | /// 3 | /// By default these configs are set as would be needed for an FFA gamemode. 4 | #[derive(Clone, Debug)] 5 | pub struct GameConfig { 6 | /// Whether or not to enable the default respawn logic. 7 | /// 8 | /// If this is set to true then, by default, once a player dies then they will 9 | /// be automatically respawned in 2 seconds unless they decide to spectate. 10 | /// If this is set to false then dead players will be unable to respawn until 11 | /// some external server logic allows them to. 12 | /// 13 | /// This is enabled by default. 14 | pub default_respawn: bool, 15 | 16 | /// Whether or not players are allowed to respawn explicitly. 17 | /// 18 | /// If this is set to true then a player can request a respawn whenever they 19 | /// are inactive. This will also allow them to respawn with a new plane. If 20 | /// false then respawn requests are ignored. 21 | /// 22 | /// This is enabled by default. 23 | pub allow_respawn: bool, 24 | 25 | /// Whether or not players can damage each other. 26 | /// 27 | /// This is set to true by default. 28 | pub allow_damage: bool, 29 | 30 | /// Whether players will occasionally drop upgrades on death. If this is set 31 | /// to false then no upgrades will ever drop. 32 | /// 33 | /// This is set to true by default. 34 | pub spawn_upgrades: bool, 35 | 36 | /// Whether to default players to always being fully upgraded. If set to true 37 | /// then players will always have 5555 upgrades. 38 | /// 39 | /// This is set to false by default. 40 | pub always_upgraded: bool, 41 | 42 | /// Whether admin commands are enabled. 43 | /// 44 | /// This is set to false by default. 45 | /// 46 | /// TODO: This should be replaced with authenticating for admin commands. 47 | pub admin_enabled: bool, 48 | } 49 | 50 | impl Default for GameConfig { 51 | fn default() -> Self { 52 | Self { 53 | default_respawn: true, 54 | allow_respawn: true, 55 | allow_damage: true, 56 | spawn_upgrades: true, 57 | always_upgraded: false, 58 | admin_enabled: false, 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server-config/src/powerup.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::time::Duration; 3 | 4 | use protocol::PowerupType; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::{EffectPrototype, ValidationError}; 8 | 9 | #[derive(Clone, Debug, Serialize, Deserialize)] 10 | pub struct PowerupPrototype { 11 | pub name: Cow<'static, str>, 12 | pub server_type: Option, 13 | #[serde(with = "crate::util::option_duration")] 14 | pub duration: Option, 15 | pub effects: Vec, 16 | } 17 | 18 | impl PowerupPrototype { 19 | pub fn shield() -> Self { 20 | Self { 21 | name: Cow::Borrowed("shield"), 22 | server_type: Some(PowerupType::Shield), 23 | duration: Some(Duration::from_secs(10)), 24 | effects: vec![EffectPrototype::shield(), EffectPrototype::despawn()], 25 | } 26 | } 27 | 28 | pub fn spawn_shield() -> Self { 29 | Self { 30 | name: Cow::Borrowed("spawn-shield"), 31 | duration: Some(Duration::from_secs(2)), 32 | ..Self::shield() 33 | } 34 | } 35 | 36 | pub fn inferno() -> Self { 37 | Self { 38 | name: Cow::Borrowed("inferno"), 39 | server_type: Some(PowerupType::Inferno), 40 | duration: Some(Duration::from_secs(10)), 41 | effects: vec![EffectPrototype::inferno(), EffectPrototype::despawn()], 42 | } 43 | } 44 | 45 | pub fn upgrade() -> Self { 46 | Self { 47 | name: Cow::Borrowed("upgrade"), 48 | server_type: None, 49 | duration: None, 50 | effects: vec![EffectPrototype::upgrade(), EffectPrototype::despawn()], 51 | } 52 | } 53 | } 54 | 55 | impl PowerupPrototype { 56 | pub fn upgrade_count(&self) -> u16 { 57 | self 58 | .effects 59 | .iter() 60 | .filter_map(|x| match x { 61 | EffectPrototype::Upgrade { count } => Some(*count), 62 | _ => None, 63 | }) 64 | .sum() 65 | } 66 | } 67 | 68 | impl PowerupPrototype { 69 | pub(crate) fn resolve(self) -> Result { 70 | if self.name.is_empty() { 71 | return Err(ValidationError::custom( 72 | "name", 73 | "powerup protoype had an empty name", 74 | )); 75 | } 76 | 77 | Ok(self) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /server/tests/behaviour/upgrades.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::component::*; 4 | use airmash::event::PlayerKilled; 5 | use airmash::resource::{Config, GameConfig}; 6 | use airmash::test::TestGame; 7 | use airmash::util::NalgebraExt; 8 | use airmash::{FireMissileInfo, Vector2}; 9 | use airmash_protocol::ServerPacket; 10 | 11 | #[test] 12 | fn player_does_not_drop_upgrade_when_not_configured() { 13 | let (mut game, mut mock) = TestGame::new(); 14 | 15 | let mut client = mock.open(); 16 | let ent = client.login("test", &mut game); 17 | 18 | game.resources.write::().spawn_upgrades = false; 19 | let pred_missile = game 20 | .resources 21 | .read::() 22 | .missiles 23 | .get("predator") 24 | .copied() 25 | .unwrap(); 26 | 27 | game.world.get_mut::(ent).unwrap().0 = Vector2::zeros(); 28 | game.run_once(); 29 | 30 | let missiles = game 31 | .fire_missiles( 32 | ent, 33 | &[FireMissileInfo { 34 | pos_offset: Vector2::new(0.0, 100.0), 35 | rot_offset: 0.0, 36 | proto: pred_missile, 37 | }], 38 | ) 39 | .unwrap(); 40 | game.run_once(); 41 | 42 | game.dispatch(PlayerKilled { 43 | player: ent, 44 | missile: missiles[0], 45 | killer: None, 46 | }); 47 | 48 | game.run_once(); 49 | 50 | while let Some(packet) = client.next_packet() { 51 | if let ServerPacket::MobUpdateStationary(_) = packet { 52 | panic!("Upgrade was spawned despite upgrades being disabled"); 53 | } 54 | } 55 | } 56 | 57 | #[test] 58 | fn picking_up_an_upgrade_gives_an_upgrade() { 59 | let (mut game, mut mock) = TestGame::new(); 60 | 61 | let mut client = mock.open(); 62 | let player = client.login("test", &mut game); 63 | 64 | game.world.get_mut::(player).unwrap().0 = Vector2::zeros(); 65 | game.run_once(); 66 | game.spawn_mob(MobType::Upgrade, Vector2::zeros(), Duration::from_secs(60)); 67 | game.run_once(); 68 | 69 | let num_upgrades = client 70 | .packets() 71 | .filter_map(|p| match p { 72 | ServerPacket::ScoreUpdate(p) => Some(p), 73 | _ => None, 74 | }) 75 | .last() 76 | .expect("Client received no ScoreUpdate packets") 77 | .upgrades; 78 | 79 | assert_eq!(num_upgrades, 1); 80 | } 81 | -------------------------------------------------------------------------------- /server/src/defaults.rs: -------------------------------------------------------------------------------- 1 | //! This module has the default component sets for all entity types. This is 2 | //! meant to make it easier add new ones for use within the main server. 3 | //! (External code can add them in EntitySpawn events if it needs to.) 4 | 5 | use std::str::FromStr; 6 | use std::time::{Duration, Instant}; 7 | 8 | use hecs::EntityBuilder; 9 | use uuid::Uuid; 10 | 11 | use crate::component::*; 12 | use crate::config::PlanePrototypeRef; 13 | use crate::protocol::client::Login; 14 | use crate::protocol::FlagCode; 15 | use crate::util::NalgebraExt; 16 | use crate::Vector2; 17 | 18 | /// Build a player 19 | pub(crate) fn build_default_player( 20 | login: &Login, 21 | proto: PlanePrototypeRef, 22 | start_time: Instant, 23 | this_frame: Instant, 24 | ) -> EntityBuilder { 25 | let mut builder = EntityBuilder::new(); 26 | builder 27 | .add(IsPlayer) 28 | .add(Position(Vector2::zeros())) 29 | .add(Velocity(Vector2::zeros())) 30 | .add(Rotation(0.0)) 31 | .add(Energy(1.0)) 32 | .add(Health(1.0)) 33 | .add(EnergyRegen(proto.energy_regen)) 34 | .add(HealthRegen(proto.health_regen)) 35 | .add(proto) 36 | .add(FlagCode::from_str(&login.flag.to_string()).unwrap_or(FlagCode::UnitedNations)) 37 | .add(Level(0)) 38 | .add(Score(0)) 39 | .add(Earnings(0)) 40 | .add(KillCount(0)) 41 | .add(DeathCount(0)) 42 | .add(Upgrades::default()) 43 | .add(PrevUpgrades::default()) 44 | .add(Name(login.name.clone())) 45 | .add(Team(0)) 46 | .add(IsAlive(true)) 47 | .add(IsSpectating(false)) 48 | .add(Session(Uuid::new_v4())) 49 | .add(KeyState::default()) 50 | .add(LastFireTime(start_time)) 51 | .add(LastSpecialTime(start_time)) 52 | .add(LastActionTime(start_time)) 53 | .add(SpecialActive(false)) 54 | .add(RespawnAllowed(true)) 55 | .add(JoinTime(this_frame)) 56 | .add(Spectating::default()) 57 | .add(PlayerPing(Duration::ZERO)) 58 | .add(TotalDamage(0.0)) 59 | .add(Captures(0)) 60 | .add(MissileFiringSide::Left) 61 | .add(Effects::default()); 62 | 63 | builder 64 | } 65 | 66 | /// Build a missile. 67 | /// 68 | /// This one is smaller since most missile components end up needing the info 69 | /// from the player. 70 | pub(crate) fn build_default_missile() -> EntityBuilder { 71 | let mut builder = EntityBuilder::new(); 72 | builder.add(IsMissile); 73 | 74 | builder 75 | } 76 | -------------------------------------------------------------------------------- /server/src/system/handler/chat.rs: -------------------------------------------------------------------------------- 1 | use crate::component::{IsPlayer, Position, SpecialActive, Team}; 2 | use crate::config::PlanePrototypeRef; 3 | use crate::event::PacketEvent; 4 | use crate::protocol::client::{Chat, Say, TeamChat, Whisper}; 5 | use crate::protocol::server as s; 6 | use crate::AirmashGame; 7 | 8 | #[handler] 9 | fn on_chat(event: &PacketEvent, game: &mut AirmashGame) { 10 | if game.world.get::(event.entity).is_err() { 11 | return; 12 | } 13 | 14 | game.send_to_all(s::ChatPublic { 15 | id: event.entity.id() as _, 16 | text: event.packet.text.clone(), 17 | }); 18 | } 19 | 20 | #[handler] 21 | fn on_team_chat(event: &PacketEvent, game: &mut AirmashGame) { 22 | if game.world.get::(event.entity).is_err() { 23 | return; 24 | } 25 | 26 | let team = game.world.get::(event.entity).unwrap(); 27 | 28 | game.send_to_team( 29 | team.0, 30 | s::ChatTeam { 31 | id: event.entity.id() as _, 32 | text: event.packet.text.clone(), 33 | }, 34 | ); 35 | } 36 | 37 | #[handler] 38 | fn on_whisper(event: &PacketEvent, game: &mut AirmashGame) { 39 | if game.world.get::(event.entity).is_err() { 40 | return; 41 | } 42 | 43 | let target = match game.find_entity_by_id(event.packet.id) { 44 | Some(entity) => entity, 45 | None => return, 46 | }; 47 | 48 | if game.world.get::(target).is_err() { 49 | return; 50 | } 51 | 52 | let packet = s::ChatWhisper { 53 | to: target.id() as _, 54 | from: event.entity.id() as _, 55 | text: event.packet.text.clone(), 56 | }; 57 | 58 | if event.entity != target { 59 | game.send_to(target, packet.clone()); 60 | } 61 | game.send_to(event.entity, packet); 62 | } 63 | 64 | #[handler] 65 | fn on_say(event: &PacketEvent, game: &mut AirmashGame) { 66 | let (&pos, &plane, &special, &team, _) = match game.world.query_one_mut::<( 67 | &Position, 68 | &PlanePrototypeRef, 69 | &SpecialActive, 70 | &Team, 71 | &IsPlayer, 72 | )>(event.entity) 73 | { 74 | Ok(query) => query, 75 | Err(_) => return, 76 | }; 77 | 78 | let packet = s::ChatSay { 79 | id: event.entity.id() as _, 80 | text: event.packet.text.clone(), 81 | }; 82 | 83 | if plane.special.is_stealth() && special.0 { 84 | game.send_to_team_visible(team.0, pos.0, packet); 85 | } else { 86 | game.send_to_visible(pos.0, packet); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /server/src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! def_wrappers { 2 | { 3 | $( 4 | $( #[$attr:meta] )* 5 | $( ##[nocopy $( $ncvis:ident )? ] )? 6 | $vis:vis type $nty:ident = $oty:ty ; 7 | )* 8 | } => { 9 | $( 10 | $( #[$attr] )* 11 | #[derive(Clone, Debug)] 12 | #[cfg_attr(all($( __server_disabled $( $ncvis )? )?), derive(Copy))] 13 | $vis struct $nty(pub $oty); 14 | 15 | impl From<$oty> for $nty { 16 | fn from(v: $oty) -> Self { 17 | Self(v) 18 | } 19 | } 20 | 21 | impl From<$nty> for $oty { 22 | fn from(v: $nty) -> Self { 23 | v.0 24 | } 25 | } 26 | 27 | impl ::std::ops::Deref for $nty { 28 | type Target = $oty; 29 | 30 | fn deref(&self) -> &Self::Target { 31 | &self.0 32 | } 33 | } 34 | 35 | impl ::std::ops::DerefMut for $nty { 36 | fn deref_mut(&mut self) -> &mut Self::Target { 37 | &mut self.0 38 | } 39 | } 40 | )* 41 | }; 42 | } 43 | 44 | macro_rules! def_wrapper_resources { 45 | { 46 | $( 47 | $( #[$attr:meta] )* 48 | $( ##[nocopy $( $ncvis:ident )? ] )? 49 | $vis:vis type $nty:ident = $oty:ty ; 50 | )* 51 | } => { 52 | $( 53 | $( #[$attr] )* 54 | #[derive(Clone, Debug, Default)] 55 | #[cfg_attr(all($( __server_disabled $( $ncvis )? )?), derive(Copy))] 56 | $vis struct $nty(pub $oty); 57 | 58 | impl From<$oty> for $nty { 59 | fn from(v: $oty) -> Self { 60 | Self(v) 61 | } 62 | } 63 | 64 | impl From<$nty> for $oty { 65 | fn from(v: $nty) -> Self { 66 | v.0 67 | } 68 | } 69 | 70 | impl ::std::ops::Deref for $nty { 71 | type Target = $oty; 72 | 73 | fn deref(&self) -> &Self::Target { 74 | &self.0 75 | } 76 | } 77 | 78 | impl ::std::ops::DerefMut for $nty { 79 | fn deref_mut(&mut self) -> &mut Self::Target { 80 | &mut self.0 81 | } 82 | } 83 | 84 | const _: () = { 85 | #[crate::handler] 86 | fn register_resource( 87 | _: &airmash::event::ServerStartup, 88 | world: &mut airmash::AirmashGame 89 | ) { 90 | if world.resources.get::<$nty>().is_none() { 91 | world.resources.insert(<$nty>::default()); 92 | } 93 | } 94 | }; 95 | )* 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /server-macros/src/handler.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use proc_macro_crate::FoundCrate; 3 | use quote::quote; 4 | use syn::parse::Parse; 5 | use syn::{parse_macro_input, parse_quote, Expr, Ident, ItemFn, Result}; 6 | 7 | use crate::args::AttrArg; 8 | 9 | struct MacroArgs { 10 | priority: Option>, 11 | } 12 | 13 | impl Parse for MacroArgs { 14 | fn parse(input: syn::parse::ParseStream) -> Result { 15 | let mut priority = None; 16 | 17 | while !input.is_empty() { 18 | let next = match input.cursor().ident() { 19 | Some((ident, _)) => ident, 20 | None => { 21 | return Err(syn::Error::new( 22 | input.cursor().span(), 23 | "unexpected token in input", 24 | )) 25 | } 26 | }; 27 | 28 | match &*format!("{}", next) { 29 | "priority" => priority = Some(input.parse()?), 30 | x => { 31 | return Err(syn::Error::new( 32 | next.span(), 33 | format!("Unknown argument `{}`", x), 34 | )) 35 | } 36 | } 37 | } 38 | 39 | Ok(Self { priority }) 40 | } 41 | } 42 | 43 | pub fn handler( 44 | attr: proc_macro::TokenStream, 45 | item: proc_macro::TokenStream, 46 | ) -> proc_macro::TokenStream { 47 | let args = parse_macro_input!(attr as MacroArgs); 48 | let input = parse_macro_input!(item as ItemFn); 49 | 50 | match impl_handler(input, args) { 51 | Ok(tokens) => tokens.into(), 52 | Err(e) => e.to_compile_error().into(), 53 | } 54 | } 55 | 56 | fn impl_handler(item: ItemFn, args: MacroArgs) -> Result { 57 | let name = &item.sig.ident; 58 | let krate = match proc_macro_crate::crate_name("airmash").unwrap_or(FoundCrate::Itself) { 59 | FoundCrate::Itself => Ident::new("airmash", Span::call_site()), 60 | FoundCrate::Name(name) => Ident::new(&name, Span::call_site()), 61 | }; 62 | 63 | let priority = args 64 | .priority 65 | .map(|x| x.value) 66 | .unwrap_or_else(|| parse_quote! { 0 }); 67 | 68 | Ok(quote! { 69 | #item 70 | 71 | const _: () = { 72 | const PRIORITY: i32 = #priority; 73 | 74 | #[allow(non_upper_case_globals)] 75 | #[#krate::_exports::linkme::distributed_slice(#krate::_exports::AIRMASH_EVENT_HANDLERS)] 76 | #[linkme(crate = #krate::_exports::linkme)] 77 | static __: fn(&#krate::_exports::EventDispatcher) = |dispatch| { 78 | dispatch.register_with_priority(PRIORITY, #name); 79 | }; 80 | }; 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /server-config/examples/validate.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | 4 | use anyhow::Context; 5 | use serde::Deserialize; 6 | use server_config::{GameConfig, GamePrototype}; 7 | 8 | #[derive(Clone, Debug)] 9 | struct ValidationError { 10 | message: String, 11 | error: E, 12 | } 13 | 14 | impl ValidationError { 15 | pub fn new(message: D, error: E) -> Self { 16 | Self { 17 | message: message.to_string(), 18 | error, 19 | } 20 | } 21 | } 22 | 23 | impl fmt::Display for ValidationError { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | f.write_str(&self.message) 26 | } 27 | } 28 | 29 | impl Error for ValidationError { 30 | fn source(&self) -> Option<&(dyn Error + 'static)> { 31 | Some(&self.error) 32 | } 33 | } 34 | 35 | fn main() -> anyhow::Result<()> { 36 | let mut success = true; 37 | let args = std::env::args().skip(1).collect::>(); 38 | // let args = vec!["server-prototypes/configs/infinite-fire.lua"]; 39 | 40 | for config in args { 41 | let prototype = GamePrototype::default(); 42 | let lua = rlua::Lua::new(); 43 | let contents = std::fs::read_to_string(&config)?; 44 | 45 | let result = lua 46 | .context(|lua| -> anyhow::Result<()> { 47 | let value = prototype.patch_direct(lua, &contents)?; 48 | 49 | let mut track = serde_path_to_error::Track::new(); 50 | let de = serde_rlua::Deserializer::new(value); 51 | let de = serde_path_to_error::Deserializer::new(de, &mut track); 52 | 53 | let proto = match GamePrototype::deserialize(de) { 54 | Ok(proto) => proto, 55 | Err(e) => { 56 | return Err(anyhow::Error::new(ValidationError::new( 57 | format_args!("error whlie deserializing field {}", track.path()), 58 | e, 59 | ))); 60 | } 61 | }; 62 | 63 | if let Err(e) = GameConfig::new(proto) { 64 | return Err(anyhow::Error::new(ValidationError::new( 65 | "error while validating config", 66 | e, 67 | ))); 68 | } 69 | 70 | Ok(()) 71 | }) 72 | .with_context(|| format!("Config `{}` failed to validate", config)); 73 | 74 | if let Err(e) = result { 75 | eprintln!("{:?}", e); 76 | success = false; 77 | } 78 | } 79 | 80 | if !success { 81 | std::process::exit(1); 82 | } 83 | 84 | eprintln!("All configs validated successfully!"); 85 | 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /server/src/system/handler/on_event_horizon.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::{MissilePrototypeRef, MobPrototypeRef}; 3 | use crate::event::EventHorizon; 4 | use crate::AirmashGame; 5 | 6 | #[handler] 7 | fn send_missile_update(event: &EventHorizon, game: &mut AirmashGame) { 8 | use crate::protocol::server::MobUpdate; 9 | 10 | if !event.in_horizon { 11 | return; 12 | } 13 | 14 | let clock = crate::util::get_current_clock(game); 15 | 16 | let (&pos, &vel, &accel, &missile, _) = match game.world.query_one_mut::<( 17 | &Position, 18 | &Velocity, 19 | &Accel, 20 | &MissilePrototypeRef, 21 | &IsMissile, 22 | )>(event.entity) 23 | { 24 | Ok(query) => query, 25 | Err(_) => return, 26 | }; 27 | 28 | game.send_to( 29 | event.player, 30 | MobUpdate { 31 | id: event.entity.id() as _, 32 | clock, 33 | ty: missile.server_type, 34 | pos: pos.into(), 35 | speed: vel.into(), 36 | accel: accel.into(), 37 | max_speed: missile.max_speed, 38 | }, 39 | ); 40 | } 41 | 42 | #[handler] 43 | fn send_mob_update(event: &EventHorizon, game: &mut AirmashGame) { 44 | use crate::protocol::server::MobUpdateStationary; 45 | 46 | if !event.in_horizon { 47 | return; 48 | } 49 | 50 | let (&pos, &mob, _) = match game 51 | .world 52 | .query_one_mut::<(&Position, &MobPrototypeRef, &IsMob)>(event.entity) 53 | { 54 | Ok(query) => query, 55 | Err(_) => return, 56 | }; 57 | 58 | game.send_to( 59 | event.player, 60 | MobUpdateStationary { 61 | id: event.entity.id() as _, 62 | ty: mob.server_type, 63 | pos: pos.into(), 64 | }, 65 | ); 66 | } 67 | 68 | #[handler] 69 | fn send_horizon_packet(event: &EventHorizon, game: &mut AirmashGame) { 70 | use crate::protocol::{server as s, LeaveHorizonType}; 71 | 72 | if event.in_horizon { 73 | return; 74 | } 75 | 76 | if game.world.get::(event.player).is_err() { 77 | return; 78 | } 79 | 80 | let query = game 81 | .world 82 | .query_one_mut::<(Option<&IsPlayer>, Option<&IsMissile>, Option<&IsMob>)>(event.entity); 83 | 84 | let ty = match query { 85 | Ok((Some(_), None, None)) => LeaveHorizonType::Player, 86 | Ok((None, Some(_), None)) => LeaveHorizonType::Mob, 87 | Ok((None, None, Some(_))) => LeaveHorizonType::Mob, 88 | _ => return, 89 | }; 90 | 91 | game.send_to( 92 | event.player, 93 | s::EventLeaveHorizon { 94 | id: event.entity.id() as _, 95 | ty, 96 | }, 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /improvements.md: -------------------------------------------------------------------------------- 1 | 2 | # Improvements 3 | 4 | This file details a list of possible inprovements 5 | that can be done with an airmash server. 6 | 7 | ### Protocol 8 | 9 | - Use [WebRCT](https://webrtc.org/) as a primary 10 | channel for communications. 11 | - Generate protocol bindings from JSON declaration 12 | 13 | ### Anti-Cheat 14 | 15 | - Do not send mimimap positions of prowlers that 16 | are nearby to the current player (counters prowler 17 | radar) (not sure if this is an improvement) 18 | - Alternatively, turn up randomization for prowler 19 | positions by a large amount 20 | 21 | ### Prowlers 22 | Multiple ideas here 23 | - Prowlers aren't visible to anyone, not own team, 24 | not other prowlers 25 | - Don't send minimap updates for nearby prowlers, 26 | can also be based on external situations such 27 | as when the flag is out of base 28 | 29 | ### Missions? 30 | - Give new players easy missions that help the 31 | team tactically 32 | 33 | ### Missile Interactiosn 34 | - Missiles "push" friendly players 35 | - Missiles can shoot other missiles out of the air 36 | - Friendly fire 37 | 38 | ### Spectating 39 | - Let players spectate without being on a team, 40 | do this automatically or let it be opt-in 41 | - (Maybe) Have players who are spectating extend 42 | the afk timer, (allow ~5? mins with no chat, 43 | and ~20 mins with chat, which would allow for 44 | players to do C&C) 45 | 46 | ### Moderation 47 | - Allow moderators to kick, ban, votemute, etc. 48 | - Would need accounts to be implemented 49 | - Moderators should be chosen carefully, since they 50 | can make or break a community 51 | 52 | ### SpawnKilling Mitigation 53 | - Disallow spawnkilling by preventing spawnkillers 54 | from being able to shoot planes that haven't 55 | moved since spawn. Either don't show them 56 | or make them indestructible. 57 | - This should also not mess up flag carries through 58 | spawn, any mitigation could be disabled when a 59 | flag is carried through spawn 60 | 61 | ### Chat Messages 62 | - Duplicate flag taken/returned message to chatbox 63 | - Add Flag Dropped server message (maybe) 64 | 65 | ### Custom Commands 66 | - Align to ship? 67 | - Server-time, game stats 68 | 69 | ### Votemutes 70 | - Remove votemute and make it identical to ignore 71 | 72 | ### Custom Gamemodes 73 | ## Tag 74 | - Drop flag when hit with missile 75 | 76 | ### Control Protocol 77 | - Make the engine scriptable through a 78 | separate protocol 79 | - Allow a client to hook into all engine events 80 | - Allows clients to be written in other languages too 81 | -------------------------------------------------------------------------------- /ffa/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::time::Duration; 3 | 4 | use airmash::protocol::GameType; 5 | use airmash::resource::RegionName; 6 | use airmash::util::PeriodicPowerupSpawner; 7 | use airmash::*; 8 | use clap::arg; 9 | 10 | mod systems; 11 | 12 | fn set_default_var(name: &str, value: &str) { 13 | if None == env::var_os(name) { 14 | env::set_var(name, value); 15 | } 16 | } 17 | 18 | fn main() { 19 | let matches = clap::Command::new("airmash-server-ffa") 20 | .version(env!("CARGO_PKG_VERSION")) 21 | .author("STEAMROLLER") 22 | .about("Airmash FFA server") 23 | .arg(arg!(-c --config [FILE] "Provides an alternate config file")) 24 | .arg(arg!(--port [PORT] "Port that the server will listen on")) 25 | .arg(arg!(--region [REGION] "The region that this server belongs to")) 26 | .get_matches(); 27 | 28 | set_default_var("RUST_BACKTRACE", "full"); 29 | set_default_var("RUST_LOG", "info"); 30 | env_logger::init(); 31 | color_backtrace::install(); 32 | 33 | let bind_addr = format!("0.0.0.0:{}", matches.value_of("port").unwrap_or("3501")); 34 | 35 | let mut game = AirmashGame::with_network( 36 | bind_addr 37 | .parse() 38 | .expect("Unable to parse provided network port address"), 39 | ); 40 | game.resources.insert(RegionName( 41 | matches.value_of("region").unwrap_or("default").to_string(), 42 | )); 43 | game.resources.insert(GameType::FFA); 44 | 45 | // Use the provided FFA scoreboard systems. 46 | airmash::system::ffa::register_all(&mut game); 47 | 48 | // Inferno in Europe 49 | game.register(PeriodicPowerupSpawner::inferno( 50 | Vector2::new(920.0, -2800.0), 51 | Duration::from_secs(105), 52 | )); 53 | game.register(PeriodicPowerupSpawner::inferno( 54 | Vector2::new(-7440.0, -1360.0), 55 | Duration::from_secs(105), 56 | )); 57 | game.register(PeriodicPowerupSpawner::inferno( 58 | Vector2::new(6565.0, -935.0), 59 | Duration::from_secs(105), 60 | )); 61 | 62 | let mut config = airmash::config::GamePrototype::default(); 63 | if let Some(path) = matches.value_of("config") { 64 | let script = match std::fs::read_to_string(path) { 65 | Ok(script) => script, 66 | Err(e) => { 67 | eprintln!("Unable to open config file. Error was {}", e); 68 | std::process::exit(1); 69 | } 70 | }; 71 | 72 | config 73 | .patch(&script) 74 | .expect("Error while running config file"); 75 | } 76 | 77 | game 78 | .resources 79 | .insert(airmash::resource::Config::new(config).unwrap()); 80 | 81 | game.run_until_shutdown(); 82 | } 83 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_respawn.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::PlanePrototypeRef; 3 | use crate::event::{PlayerRespawn, PlayerSpawn}; 4 | use crate::util::NalgebraExt; 5 | use crate::{AirmashGame, EntitySetBuilder, Vector2}; 6 | 7 | #[handler] 8 | fn send_packet(event: &PlayerRespawn, game: &mut AirmashGame) { 9 | use crate::protocol::server::PlayerRespawn; 10 | 11 | let (&pos, &rot, upgrades, effects, _) = 12 | match game 13 | .world 14 | .query_one_mut::<(&Position, &Rotation, &Upgrades, &Effects, &IsAlive)>(event.player) 15 | { 16 | Ok(query) => query, 17 | Err(_) => return, 18 | }; 19 | 20 | let packet = PlayerRespawn { 21 | id: event.player.id() as _, 22 | pos: pos.into(), 23 | rot: rot.0, 24 | upgrades: crate::util::get_server_upgrades(upgrades, effects), 25 | }; 26 | 27 | game.send_to_entities( 28 | EntitySetBuilder::visible(game, pos.0).including(event.player), 29 | packet, 30 | ); 31 | } 32 | 33 | // Set priority to be higher than PRE_LOGIN so that other handlers making 34 | // changes don't have theirs get stomped over. 35 | #[handler(priority = crate::priority::PRE_LOGIN)] 36 | fn reset_player(event: &PlayerRespawn, game: &mut AirmashGame) { 37 | let mut query = match game.world.query_one::<( 38 | &mut Position, 39 | &mut Velocity, 40 | &mut Rotation, 41 | &mut Health, 42 | &mut Energy, 43 | &mut HealthRegen, 44 | &mut EnergyRegen, 45 | &mut IsAlive, 46 | &mut IsSpectating, 47 | &mut SpecialActive, 48 | &mut KeyState, 49 | &mut Spectating, 50 | &PlanePrototypeRef, 51 | )>(event.player) 52 | { 53 | Ok(query) => query.with::(), 54 | Err(_) => return, 55 | }; 56 | 57 | let ( 58 | pos, 59 | vel, 60 | rot, 61 | health, 62 | energy, 63 | health_regen, 64 | energy_regen, 65 | alive, 66 | spectating, 67 | active, 68 | keystate, 69 | spectgt, 70 | &plane, 71 | ) = match query.get() { 72 | Some(query) => query, 73 | None => return, 74 | }; 75 | 76 | pos.0 = Vector2::zeros(); 77 | vel.0 = Vector2::zeros(); 78 | rot.0 = 0.0; 79 | health.0 = 1.0; 80 | energy.0 = 1.0; 81 | health_regen.0 = plane.health_regen; 82 | energy_regen.0 = plane.energy_regen; 83 | *keystate = KeyState::default(); 84 | alive.0 = true; 85 | spectating.0 = false; 86 | active.0 = false; 87 | spectgt.0 = None; 88 | } 89 | 90 | #[handler] 91 | fn dispatch_player_spawn(event: &PlayerRespawn, game: &mut AirmashGame) { 92 | game.dispatch(PlayerSpawn { 93 | player: event.player, 94 | }); 95 | } 96 | -------------------------------------------------------------------------------- /server/src/event/player.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | use smallvec::SmallVec; 3 | 4 | use crate::config::{PlanePrototypeRef, PowerupPrototypeRef}; 5 | 6 | /// A new player has joined the game. 7 | #[derive(Clone, Copy, Debug)] 8 | pub struct PlayerJoin { 9 | pub player: Entity, 10 | } 11 | 12 | /// A player has left the game. 13 | /// 14 | /// Note that this event is emitted before the player's entity is despawned. 15 | #[derive(Clone, Copy, Debug)] 16 | pub struct PlayerLeave { 17 | pub player: Entity, 18 | } 19 | 20 | /// A player has fired missiles. 21 | #[derive(Clone, Debug)] 22 | pub struct PlayerFire { 23 | pub player: Entity, 24 | pub missiles: SmallVec<[Entity; 3]>, 25 | } 26 | 27 | /// A player has been killed by another player. 28 | /// 29 | /// Note that the player who fired the missile may no longer be on the server so 30 | /// `killer` is an option. 31 | #[derive(Copy, Clone, Debug)] 32 | pub struct PlayerKilled { 33 | pub player: Entity, 34 | pub missile: Entity, 35 | pub killer: Option, 36 | } 37 | 38 | /// A player has respawned. 39 | #[derive(Copy, Clone, Debug)] 40 | pub struct PlayerRespawn { 41 | pub player: Entity, 42 | /// Whether the player was alive when they respawned 43 | pub alive: bool, 44 | } 45 | 46 | /// A player has spawned. 47 | /// 48 | /// This event is fired when a player respawns and when a player joins but only 49 | /// if they spawn immediately upon joining. 50 | #[derive(Copy, Clone, Debug)] 51 | pub struct PlayerSpawn { 52 | pub player: Entity, 53 | } 54 | 55 | /// A player has switched their current plane. 56 | #[derive(Copy, Clone, Debug)] 57 | pub struct PlayerChangePlane { 58 | pub player: Entity, 59 | pub old_proto: PlanePrototypeRef, 60 | } 61 | 62 | /// A player has obtained a powerup. 63 | #[derive(Copy, Clone, Debug)] 64 | pub struct PlayerPowerup { 65 | pub player: Entity, 66 | pub powerup: PowerupPrototypeRef, 67 | } 68 | 69 | /// A goliath has used their special. 70 | #[derive(Clone, Debug)] 71 | pub struct PlayerRepel { 72 | pub player: Entity, 73 | pub repelled_players: SmallVec<[Entity; 4]>, 74 | pub repelled_missiles: SmallVec<[Entity; 4]>, 75 | } 76 | 77 | /// A player has entered spectate mode. 78 | #[derive(Copy, Clone, Debug)] 79 | pub struct PlayerSpectate { 80 | pub player: Entity, 81 | pub was_alive: bool, 82 | } 83 | 84 | #[derive(Copy, Clone, Debug)] 85 | pub struct PlayerHit { 86 | pub player: Entity, 87 | pub missile: Entity, 88 | pub damage: f32, 89 | pub attacker: Option, 90 | } 91 | 92 | /// A player's score has been updated 93 | #[derive(Copy, Clone, Debug)] 94 | pub struct PlayerScoreUpdate { 95 | pub player: Entity, 96 | pub old_score: u32, 97 | } 98 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_powerup.rs: -------------------------------------------------------------------------------- 1 | use crate::component::{IsPlayer, *}; 2 | use crate::event::PlayerPowerup; 3 | use crate::protocol::server as s; 4 | use crate::AirmashGame; 5 | 6 | #[handler] 7 | fn send_packet(event: &PlayerPowerup, game: &mut AirmashGame) { 8 | if game.world.query_one_mut::<&IsPlayer>(event.player).is_err() { 9 | return; 10 | } 11 | 12 | let (duration, ty) = match (event.powerup.duration, event.powerup.server_type) { 13 | (Some(duration), Some(ty)) => (duration, ty), 14 | _ => return, 15 | }; 16 | 17 | let duration = duration.as_secs() * 1000 + duration.subsec_millis() as u64; 18 | 19 | game.send_to( 20 | event.player, 21 | s::PlayerPowerup { 22 | duration: duration as u32, 23 | ty, 24 | }, 25 | ); 26 | } 27 | 28 | #[handler] 29 | fn update_effects(event: &PlayerPowerup, game: &mut AirmashGame) { 30 | let start_time = game.start_time(); 31 | let this_frame = game.this_frame(); 32 | 33 | let (duration, ty) = match (event.powerup.duration, event.powerup.server_type) { 34 | (Some(duration), Some(ty)) => (duration, ty), 35 | _ => return, 36 | }; 37 | 38 | let (last_update, effects, _) = match game 39 | .world 40 | .query_one_mut::<(&mut LastUpdateTime, &mut Effects, &IsPlayer)>(event.player) 41 | { 42 | Ok(query) => query, 43 | Err(_) => return, 44 | }; 45 | 46 | last_update.0 = start_time; 47 | effects.set_powerup(ty, this_frame + duration, &event.powerup.effects); 48 | } 49 | 50 | #[handler(priority = crate::priority::HIGH)] 51 | fn update_player_upgrades(event: &PlayerPowerup, game: &mut AirmashGame) { 52 | let num: u16 = event.powerup.upgrade_count(); 53 | if num == 0 { 54 | return; 55 | } 56 | 57 | let (upgrades, prev, _) = match game 58 | .world 59 | .query_one_mut::<(&mut Upgrades, &mut PrevUpgrades, &IsPlayer)>(event.player) 60 | { 61 | Ok(query) => query, 62 | Err(_) => return, 63 | }; 64 | 65 | upgrades.unused += num; 66 | prev.0 = *upgrades; 67 | } 68 | 69 | #[handler] 70 | fn send_player_upgrade(event: &PlayerPowerup, game: &mut AirmashGame) { 71 | let (upgrades, score, earnings, kills, deaths, _) = match game.world.query_one_mut::<( 72 | &Upgrades, 73 | &Score, 74 | &Earnings, 75 | &KillCount, 76 | &DeathCount, 77 | &IsPlayer, 78 | )>(event.player) 79 | { 80 | Ok(query) => query, 81 | Err(_) => return, 82 | }; 83 | 84 | let packet = s::ScoreUpdate { 85 | id: event.player.id() as _, 86 | upgrades: upgrades.unused, 87 | score: score.0, 88 | earnings: earnings.0, 89 | total_kills: kills.0, 90 | total_deaths: deaths.0, 91 | }; 92 | game.send_to(event.player, packet); 93 | } 94 | -------------------------------------------------------------------------------- /ctf/tests/game-score-reset.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::component::*; 4 | use airmash::protocol::ServerPacket; 5 | use airmash::resource::GameConfig; 6 | use airmash::test::*; 7 | use airmash_server_ctf::config::{FLAG_NO_REGRAB_TIME, RED_TEAM}; 8 | use airmash_server_ctf::resource::GameScores; 9 | 10 | #[test] 11 | fn scores_reset_on_game_win() { 12 | let (mut game, mut mock) = TestGame::new(); 13 | airmash_server_ctf::setup_ctf_server(&mut game); 14 | 15 | let mut conn = mock.open(); 16 | let ent = conn.login("test", &mut game); 17 | 18 | game.resources.write::().admin_enabled = true; 19 | game.world.insert_one(ent, Team(RED_TEAM)).unwrap(); 20 | game.run_once(); 21 | 22 | let pause_time = FLAG_NO_REGRAB_TIME + Duration::from_secs(1); 23 | 24 | // 3 caps by red team 25 | conn.send_command("teleport", "0 blue-flag"); 26 | game.run_for(pause_time); 27 | conn.send_command("teleport", "0 red-flag"); 28 | game.run_once(); 29 | conn.send_command("teleport", "0 blue-flag"); 30 | game.run_for(pause_time); 31 | conn.send_command("teleport", "0 red-flag"); 32 | game.run_once(); 33 | conn.send_command("teleport", "0 blue-flag"); 34 | game.run_for(pause_time); 35 | conn.send_command("teleport", "0 red-flag"); 36 | game.run_once(); 37 | 38 | let last_flag = conn 39 | .packets() 40 | .filter_map(|x| match x { 41 | ServerPacket::GameFlag(flag) => Some(flag), 42 | _ => None, 43 | }) 44 | .last() 45 | .expect("No GameFlag updates were sent"); 46 | 47 | println!("{:?}", *game.resources.read::()); 48 | 49 | assert_eq!(last_flag.blueteam, 0); 50 | assert_eq!(last_flag.redteam, 3); 51 | 52 | game.run_for(Duration::from_secs(61)); 53 | game.world.insert_one(ent, Team(RED_TEAM)).unwrap(); 54 | 55 | let last_flag = conn 56 | .packets() 57 | .filter_map(|x| match x { 58 | ServerPacket::GameFlag(flag) => Some(flag), 59 | _ => None, 60 | }) 61 | .last() 62 | .expect("No GameFlag updates were sent"); 63 | 64 | println!("{:?}", *game.resources.read::()); 65 | 66 | assert_eq!(last_flag.blueteam, 0); 67 | assert_eq!(last_flag.redteam, 0); 68 | 69 | game.run_for(pause_time); 70 | conn.send_command("teleport", "0 blue-flag"); 71 | game.run_once(); 72 | conn.send_command("teleport", "0 red-flag"); 73 | game.run_count(5); 74 | 75 | let last_flag = conn 76 | .packets() 77 | .filter_map(|x| match x { 78 | ServerPacket::GameFlag(flag) => Some(flag), 79 | _ => None, 80 | }) 81 | .last() 82 | .expect("No GameFlag updates were sent"); 83 | 84 | println!("{:?}", *game.resources.read::()); 85 | 86 | assert_eq!(last_flag.blueteam, 0); 87 | assert_eq!(last_flag.redteam, 1); 88 | } 89 | -------------------------------------------------------------------------------- /ctf/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::util::PeriodicPowerupSpawner; 4 | use airmash::{AirmashGame, Vector2}; 5 | 6 | fn set_default_var(name: &str, value: &str) { 7 | use std::env; 8 | 9 | if None == env::var_os(name) { 10 | env::set_var(name, value); 11 | } 12 | } 13 | 14 | fn main() { 15 | use std::env; 16 | 17 | use airmash::resource::RegionName; 18 | use clap::arg; 19 | 20 | let matches = clap::Command::new("airmash-server-ctf") 21 | .version(env!("CARGO_PKG_VERSION")) 22 | .author("STEAMROLLER") 23 | .about("Airmash CTF server") 24 | .arg(arg!(-c --config [FILE] "Provides an alternate config file")) 25 | .arg(arg!(--port [PORT] "Port that the server will listen on")) 26 | .arg(arg!(--region [REGION] "The region that this server belongs to")) 27 | .get_matches(); 28 | 29 | set_default_var("RUST_BACKTRACE", "1"); 30 | set_default_var("RUST_LOG", "info"); 31 | env_logger::init(); 32 | 33 | let bind_addr = format!("0.0.0.0:{}", matches.value_of("port").unwrap_or("3501")); 34 | 35 | let mut game = AirmashGame::with_network( 36 | bind_addr 37 | .parse() 38 | .expect("Unable to parse provided network port address"), 39 | ); 40 | game.resources.insert(RegionName( 41 | matches.value_of("region").unwrap_or("default").to_string(), 42 | )); 43 | 44 | let mut config = airmash::config::GamePrototype::default(); 45 | if let Some(path) = matches.value_of("config") { 46 | let script = match std::fs::read_to_string(path) { 47 | Ok(script) => script, 48 | Err(e) => { 49 | eprintln!("Unable to open config file. Error was {}", e); 50 | std::process::exit(1); 51 | } 52 | }; 53 | 54 | config 55 | .patch(&script) 56 | .expect("Error while running config file"); 57 | } 58 | 59 | game 60 | .resources 61 | .insert(airmash::resource::Config::new(config).unwrap()); 62 | 63 | airmash_server_ctf::setup_ctf_server(&mut game); 64 | 65 | // Inferno in Europe 66 | game.register(PeriodicPowerupSpawner::inferno( 67 | Vector2::new(920.0, -2800.0), 68 | Duration::from_secs(105), 69 | )); 70 | game.register(PeriodicPowerupSpawner::inferno( 71 | Vector2::new(-7440.0, -1360.0), 72 | Duration::from_secs(105), 73 | )); 74 | game.register(PeriodicPowerupSpawner::inferno( 75 | Vector2::new(6565.0, -935.0), 76 | Duration::from_secs(105), 77 | )); 78 | 79 | // Blue base shield 80 | game.register(PeriodicPowerupSpawner::shield( 81 | Vector2::new(-9300.0, -1480.0), 82 | Duration::from_secs(90), 83 | )); 84 | // Red base shield 85 | game.register(PeriodicPowerupSpawner::shield( 86 | Vector2::new(8350.0, -935.0), 87 | Duration::from_secs(90), 88 | )); 89 | 90 | game.run_until_shutdown(); 91 | } 92 | -------------------------------------------------------------------------------- /server/src/system/ping.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::convert::TryInto; 3 | use std::time::{Duration, Instant}; 4 | 5 | use airmash_protocol::client::Pong; 6 | 7 | use crate::component::*; 8 | use crate::event::PacketEvent; 9 | use crate::resource::ServerStats; 10 | use crate::AirmashGame; 11 | 12 | struct PingData { 13 | seqs: VecDeque<(u32, Instant)>, 14 | } 15 | 16 | impl PingData { 17 | pub fn new() -> Self { 18 | Self { 19 | seqs: Default::default(), 20 | } 21 | } 22 | 23 | fn push_seq(&mut self, seq: u32, time: Instant) { 24 | self.seqs.push_back((seq, time)); 25 | 26 | if self.seqs.len() > 10 { 27 | self.seqs.pop_front(); 28 | } 29 | } 30 | 31 | fn seq_time(&self, seq: u32) -> Option { 32 | self 33 | .seqs 34 | .iter() 35 | .find(|&&(s, _)| s == seq) 36 | .map(|&(_, time)| time) 37 | } 38 | 39 | fn last_ping(&self) -> Option { 40 | Some(self.seqs.back()?.1) 41 | } 42 | } 43 | 44 | pub fn update(game: &mut AirmashGame) { 45 | send_ping_packets(game); 46 | } 47 | 48 | fn send_ping_packets(game: &mut AirmashGame) { 49 | use crate::protocol::server::Ping; 50 | 51 | let this_frame = game.this_frame(); 52 | let clock = crate::util::get_time_clock(game, Instant::now()); 53 | let data = game 54 | .resources 55 | .entry::() 56 | .or_insert_with(PingData::new); 57 | 58 | if data 59 | .last_ping() 60 | .map(|p| this_frame.saturating_duration_since(p) < Duration::from_secs(5)) 61 | .unwrap_or(false) 62 | { 63 | return; 64 | } 65 | 66 | let seq = rand::random(); 67 | data.push_seq(seq, this_frame); 68 | 69 | game.send_to_all(Ping { clock, num: seq }); 70 | } 71 | 72 | #[handler] 73 | fn handle_ping_response(event: &PacketEvent, game: &mut AirmashGame) { 74 | use crate::protocol::server::PingResult; 75 | 76 | let num_players = game.resources.read::().num_players; 77 | let data = match game.resources.get::() { 78 | Some(data) => data, 79 | None => return, 80 | }; 81 | 82 | let ping = match data.seq_time(event.packet.num) { 83 | Some(time) => event.time.saturating_duration_since(time), 84 | None => return, 85 | }; 86 | 87 | if let Ok((player_ping, _)) = game 88 | .world 89 | .query_one_mut::<(&mut PlayerPing, &IsPlayer)>(event.entity) 90 | { 91 | player_ping.0 = ping; 92 | } 93 | 94 | game.send_to( 95 | event.entity, 96 | PingResult { 97 | ping: ping.as_millis().try_into().unwrap_or(u16::MAX), 98 | players_game: num_players, 99 | // TODO: Somehow get the total number of players from a server. 100 | players_total: num_players, 101 | }, 102 | ) 103 | } 104 | -------------------------------------------------------------------------------- /ctf/src/systems/on_player_join.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use airmash::component::*; 4 | use airmash::event::PlayerJoin; 5 | use airmash::{AirmashGame, Vector2}; 6 | 7 | use crate::component::*; 8 | use crate::config; 9 | use crate::resource::{CTFGameStats, GameScores}; 10 | 11 | #[handler(priority = airmash::priority::PRE_LOGIN)] 12 | fn setup_team_and_pos(event: &PlayerJoin, game: &mut AirmashGame) { 13 | let stats = game.resources.read::(); 14 | let score = game.resources.read::(); 15 | 16 | let team = match stats.red_players.cmp(&stats.blue_players) { 17 | Ordering::Less => config::RED_TEAM, 18 | Ordering::Greater => config::BLUE_TEAM, 19 | Ordering::Equal => match score.redteam.cmp(&score.blueteam) { 20 | Ordering::Less => config::RED_TEAM, 21 | Ordering::Greater => config::BLUE_TEAM, 22 | Ordering::Equal => match rand::random() { 23 | true => config::RED_TEAM, 24 | false => config::BLUE_TEAM, 25 | }, 26 | }, 27 | }; 28 | 29 | let offset = Vector2::new(rand::random::() - 0.5, rand::random::() - 0.5); 30 | let respawn = config::team_respawn_pos(team) + 400.0 * offset; 31 | 32 | let _ = game 33 | .world 34 | .insert(event.player, (Team(team), Position(respawn))); 35 | } 36 | 37 | #[handler] 38 | fn send_flag_position_on_join(event: &PlayerJoin, game: &mut AirmashGame) { 39 | use airmash::protocol::server::GameFlag; 40 | use airmash::protocol::FlagUpdateType; 41 | 42 | let scores = game.resources.read::(); 43 | let mut query = game 44 | .world 45 | .query::<(&Position, &Team, &FlagCarrier)>() 46 | .with::(); 47 | 48 | for (_, (pos, team, carrier)) in query.iter() { 49 | let ty = match carrier.0 { 50 | Some(_) => FlagUpdateType::Carrier, 51 | None => FlagUpdateType::Position, 52 | }; 53 | 54 | let packet = GameFlag { 55 | ty, 56 | pos: pos.into(), 57 | flag: team.0 as _, 58 | id: carrier.0.map(|x| x.id() as _), 59 | blueteam: scores.blueteam, 60 | redteam: scores.redteam, 61 | }; 62 | game.send_to(event.player, packet); 63 | } 64 | } 65 | 66 | #[handler] 67 | fn update_player_count(event: &PlayerJoin, game: &mut AirmashGame) { 68 | let mut counts = game.resources.write::(); 69 | 70 | let team = match game.world.get::(event.player) { 71 | Ok(team) => team, 72 | Err(e) => { 73 | warn!( 74 | "Newly joined player {:?} missing team component: {}", 75 | event.player, e 76 | ); 77 | return; 78 | } 79 | }; 80 | 81 | match team.0 { 82 | config::BLUE_TEAM => counts.blue_players += 1, 83 | config::RED_TEAM => counts.red_players += 1, 84 | x => warn!( 85 | "Newly joined player {:?} had unexpected team: {}", 86 | event.player, x 87 | ), 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /server/src/consts/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use std::time::Duration; 4 | 5 | use crate::config::MissilePrototypeRef; 6 | use crate::protocol::*; 7 | use crate::{FireMissileInfo, Vector2}; 8 | 9 | mod hitcircles; 10 | mod terrain; 11 | 12 | pub use self::hitcircles::hitcircles_for_plane; 13 | pub use self::terrain::TERRAIN; 14 | 15 | pub const UPGRADE_MULTIPLIERS: [f32; 6] = [1.0, 1.05, 1.1, 1.15, 1.2, 1.25]; 16 | 17 | /// The probability that, when an unupgraded player dies, they will drop an 18 | /// upgrade. 19 | pub const UPGRADE_DROP_PROBABILITY: f32 = 0.5; 20 | /// The collision radius of a mob. 21 | pub const MOB_COLLIDE_RADIUS: f32 = 10.0; 22 | 23 | /// The pred special causes negative energy regen this value is the rate at 24 | /// which it causes energy to decrease. 25 | pub const PREDATOR_SPECIAL_REGEN: EnergyRegen = -0.01; 26 | 27 | pub const GOLIATH_SPECIAL_ENERGY: Energy = 0.5; 28 | // TODO: Replace this with real value (see issue #2) 29 | /// The distance out to which a goliath repel has an effect 30 | pub const GOLIATH_SPECIAL_RADIUS_MISSILE: Distance = 225.0; 31 | pub const GOLIATH_SPECIAL_RADIUS_PLAYER: Distance = 180.0; 32 | /// The speed at which players and mobs will be going when they are reflected. 33 | pub const GOLIATH_SPECIAL_REFLECT_SPEED: Speed = 0.5; 34 | /// Minimum time between reflects. 35 | pub const GOLIATH_SPECIAL_INTERVAL: Duration = Duration::from_secs(1); 36 | 37 | // TODO: Tornado 38 | pub const TORNADO_SPECIAL_ENERGY: Energy = 0.9; 39 | pub fn tornado_missile_details(proto: MissilePrototypeRef) -> [FireMissileInfo; 3] { 40 | [ 41 | FireMissileInfo { 42 | pos_offset: Vector2::new(0.0, 40.1), 43 | rot_offset: 0.0, 44 | proto, 45 | }, 46 | FireMissileInfo { 47 | pos_offset: Vector2::new(15.0, 9.6), 48 | rot_offset: -0.05, 49 | proto, 50 | }, 51 | FireMissileInfo { 52 | pos_offset: Vector2::new(-15.0, 9.6), 53 | rot_offset: 0.05, 54 | proto, 55 | }, 56 | ] 57 | } 58 | pub fn tornado_inferno_missile_details(proto: MissilePrototypeRef) -> [FireMissileInfo; 5] { 59 | [ 60 | FireMissileInfo { 61 | pos_offset: Vector2::new(0.0, 40.1), 62 | rot_offset: 0.0, 63 | proto, 64 | }, 65 | FireMissileInfo { 66 | pos_offset: Vector2::new(30.0, 15.0), 67 | rot_offset: -0.1, 68 | proto, 69 | }, 70 | FireMissileInfo { 71 | pos_offset: Vector2::new(20.0, 25.0), 72 | rot_offset: -0.05, 73 | proto, 74 | }, 75 | FireMissileInfo { 76 | pos_offset: Vector2::new(-20.0, 25.0), 77 | rot_offset: 0.05, 78 | proto, 79 | }, 80 | FireMissileInfo { 81 | pos_offset: Vector2::new(-30.0, 15.0), 82 | rot_offset: 0.1, 83 | proto, 84 | }, 85 | ] 86 | } 87 | 88 | pub const PROWLER_SPECIAL_ENERGY: Energy = 0.6; 89 | pub const PROWLER_SPECIAL_DELAY: Duration = Duration::from_millis(1500); 90 | -------------------------------------------------------------------------------- /server-config/src/game.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{ 6 | GameConfigCommon, MissilePrototype, MobPrototype, PlanePrototype, PowerupPrototype, PrototypeRef, 7 | SpecialPrototype, StringRef, 8 | }; 9 | 10 | #[derive(Serialize, Deserialize, Clone, Debug)] 11 | #[non_exhaustive] 12 | #[serde(deny_unknown_fields)] 13 | #[serde(bound( 14 | serialize = " 15 | Ref::MissileRef: Serialize, 16 | Ref::SpecialRef: Serialize, 17 | Ref::PowerupRef: Serialize, 18 | Ref::PlaneRef: Serialize, 19 | Ref::MobRef: Serialize, 20 | ", 21 | deserialize = " 22 | Ref::MissileRef: Deserialize<'de>, 23 | Ref::SpecialRef: Deserialize<'de>, 24 | Ref::PowerupRef: Deserialize<'de>, 25 | Ref::PlaneRef: Deserialize<'de>, 26 | Ref::MobRef: Deserialize<'de>, 27 | " 28 | ))] 29 | pub struct GamePrototype<'a, Ref: PrototypeRef<'a> = StringRef> { 30 | pub planes: Vec>, 31 | pub missiles: Vec, 32 | pub specials: Vec>, 33 | pub mobs: Vec>, 34 | pub powerups: Vec, 35 | 36 | #[serde(flatten)] 37 | pub common: GameConfigCommon<'a, Ref>, 38 | } 39 | 40 | impl Default for GamePrototype<'_, StringRef> { 41 | fn default() -> Self { 42 | Self { 43 | planes: vec![ 44 | PlanePrototype::predator(), 45 | PlanePrototype::tornado(), 46 | PlanePrototype::mohawk(), 47 | PlanePrototype::goliath(), 48 | PlanePrototype::prowler(), 49 | ], 50 | missiles: vec![ 51 | MissilePrototype::predator(), 52 | MissilePrototype::tornado(), 53 | MissilePrototype::tornado_triple(), 54 | MissilePrototype::prowler(), 55 | MissilePrototype::goliath(), 56 | MissilePrototype::mohawk(), 57 | ], 58 | specials: vec![ 59 | SpecialPrototype::boost(), 60 | SpecialPrototype::multishot(), 61 | SpecialPrototype::strafe(), 62 | SpecialPrototype::repel(), 63 | SpecialPrototype::stealth(), 64 | ], 65 | mobs: vec![ 66 | MobPrototype::inferno(), 67 | MobPrototype::shield(), 68 | MobPrototype::upgrade(), 69 | ], 70 | powerups: vec![ 71 | PowerupPrototype::shield(), 72 | PowerupPrototype::spawn_shield(), 73 | PowerupPrototype::inferno(), 74 | PowerupPrototype::upgrade(), 75 | ], 76 | common: GameConfigCommon::default(), 77 | } 78 | } 79 | } 80 | 81 | impl<'a, R: PrototypeRef<'a>> Deref for GamePrototype<'a, R> { 82 | type Target = GameConfigCommon<'a, R>; 83 | 84 | fn deref(&self) -> &Self::Target { 85 | &self.common 86 | } 87 | } 88 | 89 | impl<'a, R: PrototypeRef<'a>> DerefMut for GamePrototype<'a, R> { 90 | fn deref_mut(&mut self) -> &mut Self::Target { 91 | &mut self.common 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /server/src/system/keys.rs: -------------------------------------------------------------------------------- 1 | use crate::component::*; 2 | use crate::config::PlanePrototypeRef; 3 | use crate::event::KeyEvent; 4 | use crate::protocol::KeyCode; 5 | use crate::resource::{StartTime, ThisFrame}; 6 | use crate::AirmashGame; 7 | 8 | pub fn update(game: &mut AirmashGame) { 9 | fire_missiles(game); 10 | } 11 | 12 | fn fire_missiles(game: &mut AirmashGame) { 13 | let this_frame = game.this_frame(); 14 | 15 | let mut query = game 16 | .world 17 | .query::<( 18 | &KeyState, 19 | &LastFireTime, 20 | &mut Energy, 21 | &PlanePrototypeRef, 22 | &Effects, 23 | &IsAlive, 24 | )>() 25 | .with::(); 26 | 27 | let mut events = Vec::new(); 28 | for (ent, (keystate, last_fire, energy, plane, effects, alive)) in query.iter() { 29 | if !alive.0 30 | || !keystate.fire 31 | || this_frame - last_fire.0 < plane.fire_delay 32 | || energy.0 < plane.fire_energy 33 | { 34 | continue; 35 | } 36 | 37 | energy.0 -= plane.fire_energy; 38 | 39 | let mut count = 1; 40 | if effects.has_inferno() { 41 | count = count * 2 + 1; 42 | } 43 | 44 | events.push((ent, count, plane.missile)); 45 | } 46 | 47 | drop(query); 48 | 49 | for (ent, missiles, ty) in events { 50 | let _ = game.fire_missiles_count(ent, missiles, ty); 51 | } 52 | } 53 | 54 | /// Update the keystate component when a new key event comes in 55 | #[handler(priority = crate::priority::HIGH)] 56 | fn update_keystate(event: &KeyEvent, game: &mut AirmashGame) { 57 | let this_frame = game.resources.read::().0; 58 | 59 | let (keystate, last_action, ..) = match game 60 | .world 61 | .query_one_mut::<(&mut KeyState, &mut LastActionTime, &IsPlayer)>(event.player) 62 | { 63 | Ok(query) => query, 64 | Err(_) => return, 65 | }; 66 | 67 | match event.key { 68 | KeyCode::Up => keystate.up = event.state, 69 | KeyCode::Down => keystate.down = event.state, 70 | KeyCode::Left => keystate.left = event.state, 71 | KeyCode::Right => keystate.right = event.state, 72 | KeyCode::Fire => keystate.fire = event.state, 73 | KeyCode::Special => keystate.special = event.state, 74 | _ => return, 75 | } 76 | 77 | last_action.0 = this_frame; 78 | } 79 | 80 | /// Force the physics system to emit a PlayerUpdate packet ASAP when the player 81 | /// presses a key that changes the plane's direction or speed. 82 | #[handler] 83 | fn force_update_packet(event: &KeyEvent, game: &mut AirmashGame) { 84 | // Other keys don't force updates 85 | match event.key { 86 | KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => (), 87 | _ => return, 88 | } 89 | 90 | let (last_update, ..) = match game 91 | .world 92 | .query_one_mut::<(&mut LastUpdateTime, &IsPlayer)>(event.player) 93 | { 94 | Ok(query) => query, 95 | Err(_) => return, 96 | }; 97 | 98 | last_update.0 = game.resources.read::().0; 99 | } 100 | -------------------------------------------------------------------------------- /server-config/src/mob.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::time::Duration; 3 | 4 | use protocol::MobType; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::powerup::PowerupPrototype; 8 | use crate::util::duration; 9 | use crate::{PrototypeRef, PtrRef, StringRef, ValidationError}; 10 | 11 | #[derive(Clone, Debug, Serialize, Deserialize)] 12 | #[serde(deny_unknown_fields)] 13 | #[serde(bound( 14 | serialize = " 15 | Ref::MissileRef: Serialize, 16 | Ref::SpecialRef: Serialize, 17 | Ref::PowerupRef: Serialize, 18 | Ref::PlaneRef: Serialize, 19 | Ref::MobRef: Serialize, 20 | ", 21 | deserialize = " 22 | Ref::MissileRef: Deserialize<'de>, 23 | Ref::SpecialRef: Deserialize<'de>, 24 | Ref::PowerupRef: Deserialize<'de>, 25 | Ref::PlaneRef: Deserialize<'de>, 26 | Ref::MobRef: Deserialize<'de>, 27 | " 28 | ))] 29 | pub struct MobPrototype<'a, Ref: PrototypeRef<'a> = StringRef> { 30 | /// The name that will be used to this mob. 31 | pub name: Cow<'static, str>, 32 | 33 | /// The mob type that will be communicated to the client. 34 | /// 35 | /// This will determine the entity type that the client will show for the mob. 36 | /// Setting it to the type of a missile is likely to break things. 37 | pub server_type: MobType, 38 | 39 | /// How long this mob will stick around before despawning. 40 | #[serde(with = "duration")] 41 | pub lifetime: Duration, 42 | 43 | /// The effects of colliding with this mob. 44 | pub powerup: Ref::PowerupRef, 45 | } 46 | 47 | impl MobPrototype<'_, StringRef> { 48 | pub fn inferno() -> Self { 49 | Self { 50 | name: Cow::Borrowed("inferno"), 51 | server_type: MobType::Inferno, 52 | lifetime: Duration::from_secs(60), 53 | powerup: Cow::Borrowed("inferno"), 54 | } 55 | } 56 | 57 | pub fn shield() -> Self { 58 | Self { 59 | name: Cow::Borrowed("shield"), 60 | server_type: MobType::Shield, 61 | lifetime: Duration::from_secs(60), 62 | powerup: Cow::Borrowed("shield"), 63 | } 64 | } 65 | 66 | pub fn upgrade() -> Self { 67 | Self { 68 | name: Cow::Borrowed("upgrade"), 69 | server_type: MobType::Upgrade, 70 | lifetime: Duration::from_secs(60), 71 | powerup: Cow::Borrowed("upgrade"), 72 | } 73 | } 74 | } 75 | 76 | impl MobPrototype<'_, StringRef> { 77 | pub fn resolve( 78 | self, 79 | powerups: &[PowerupPrototype], 80 | ) -> Result, ValidationError> { 81 | if self.name.is_empty() { 82 | return Err(ValidationError::custom("name", "prototype had empty name")); 83 | } 84 | 85 | let powerup = powerups 86 | .iter() 87 | .find(|proto| proto.name == self.powerup) 88 | .ok_or(ValidationError::custom( 89 | "powerup", 90 | format_args!( 91 | "mob prototype refers to nonexistant powerup prototype `{}`", 92 | self.powerup 93 | ), 94 | ))?; 95 | 96 | Ok(MobPrototype { 97 | name: self.name, 98 | server_type: self.server_type, 99 | lifetime: self.lifetime, 100 | powerup, 101 | }) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /server/src/system/mod.rs: -------------------------------------------------------------------------------- 1 | //! All systems and event handlers. 2 | //! 3 | //! Most of this module is bundled up within the [`update`] function. However, 4 | //! it also exposes some optional systems that are not registered by default but 5 | //! may be useful for certain game modes. 6 | 7 | use crate::AirmashGame; 8 | 9 | pub mod ctf; 10 | pub mod ffa; 11 | 12 | mod admin; 13 | mod collision; 14 | mod despawn; 15 | mod handler; 16 | mod keys; 17 | mod network; 18 | mod physics; 19 | mod ping; 20 | mod powerups; 21 | mod regen; 22 | mod scoreboard; 23 | mod specials; 24 | mod upgrades; 25 | mod visibility; 26 | 27 | /// Main airmash update loop. 28 | /// 29 | /// This is the main method that contains all the work done within a single 30 | /// frame of the airmash engine. Generally it is not something you should have 31 | /// to call as it will be called as a part of [`AirmashGame::run_once`] or 32 | /// [`AirmashGame::run_until_shutdown`]. 33 | /// 34 | /// [`AirmashGame::run_once`]: crate::AirmashGame::run_once 35 | /// [`AirmashGame::run_until_shutdown`]: crate::AirmashGame::run_until_shutdown 36 | pub fn update(game: &mut AirmashGame) { 37 | use crate::event::{Frame, FrameEnd, FrameStart}; 38 | 39 | game.dispatch(FrameStart); 40 | 41 | self::physics::update(game); 42 | self::regen::update(game); 43 | self::specials::update(game); 44 | 45 | self::collision::generate_collision_lookups(game); 46 | self::visibility::generate_horizon_events(game); 47 | self::collision::check_collisions(game); 48 | 49 | // Note: most events will happen here 50 | self::network::process_packets(game); 51 | 52 | self::keys::update(game); 53 | self::despawn::update(game); 54 | self::powerups::update(game); 55 | self::scoreboard::update(game); 56 | self::ping::update(game); 57 | self::upgrades::update(game); 58 | 59 | game.dispatch(Frame); 60 | 61 | update_tasks(game); 62 | cull_zombies(game); 63 | 64 | game.dispatch(FrameEnd); 65 | } 66 | 67 | /// Reusing an id soon after it was created causes problems with the airmash web 68 | /// client. To avoid this we insert a placeholder entity when despawning other 69 | /// entities. This function is responsible for deleting them after a certain 70 | /// amount of time has passed. 71 | /// 72 | /// Since nothing should be interacting with these entities it emits no events. 73 | fn cull_zombies(game: &mut AirmashGame) { 74 | use smallvec::SmallVec; 75 | 76 | use crate::component::{Expiry, IsZombie}; 77 | 78 | let this_frame = game.this_frame(); 79 | 80 | let dead = game 81 | .world 82 | .query_mut::<&Expiry>() 83 | .with::() 84 | .into_iter() 85 | .filter(|(_, expiry)| expiry.0 < this_frame) 86 | .map(|(ent, _)| ent) 87 | .collect::>(); 88 | 89 | for entity in dead { 90 | let _ = game.world.despawn(entity); 91 | } 92 | } 93 | 94 | /// This system is responsible for running delayed tasks that have been 95 | /// scheduled by other code. 96 | fn update_tasks(game: &mut AirmashGame) { 97 | use crate::resource::TaskScheduler; 98 | 99 | let task_sched = game.resources.read::().clone(); 100 | task_sched.update(game); 101 | } 102 | -------------------------------------------------------------------------------- /server/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! AIRMASH Server 2 | 3 | /// A re-export of the entirety of the `airmash_protocol` crate. 4 | pub mod protocol { 5 | pub use airmash_protocol::*; 6 | } 7 | 8 | pub mod config { 9 | pub use server_config::*; 10 | 11 | pub type GameConfigRef = &'static GameConfig; 12 | pub type PlanePrototypeRef = &'static PlanePrototype<'static, PtrRef>; 13 | pub type MissilePrototypeRef = &'static MissilePrototype; 14 | pub type SpecialPrototypeRef = &'static SpecialPrototype<'static, PtrRef>; 15 | pub type PowerupPrototypeRef = &'static PowerupPrototype; 16 | pub type MobPrototypeRef = &'static MobPrototype<'static, PtrRef>; 17 | } 18 | 19 | pub extern crate hecs; 20 | 21 | #[macro_use] 22 | extern crate log; 23 | #[macro_use] 24 | extern crate server_macros; 25 | 26 | extern crate self as airmash; 27 | 28 | #[macro_use] 29 | mod macros; 30 | 31 | mod consts; 32 | mod defaults; 33 | mod dispatch; 34 | mod mock; 35 | mod task; 36 | mod world; 37 | mod worldext; 38 | 39 | pub mod component; 40 | pub mod event; 41 | pub mod network; 42 | pub mod resource; 43 | pub mod system; 44 | pub mod util; 45 | 46 | pub use hecs::Entity; 47 | pub use server_macros::handler; 48 | 49 | pub use self::config::Vector2; 50 | pub use self::dispatch::{Event, EventDispatcher, EventHandler}; 51 | pub use self::task::{GameRef, TaskScheduler}; 52 | pub use self::world::{AirmashGame, Resources}; 53 | pub use self::worldext::{EntitySetBuilder, FireMissileInfo}; 54 | 55 | /// Exports needed by the handler macro. 56 | #[doc(hidden)] 57 | pub mod _exports { 58 | pub use crate::dispatch::{EventDispatcher, AIRMASH_EVENT_HANDLERS}; 59 | pub extern crate linkme; 60 | } 61 | 62 | /// Reexports of common items that are often needed when writing server code. 63 | pub mod prelude { 64 | pub use crate::{handler, AirmashGame, Entity}; 65 | } 66 | 67 | /// Notable priorities for event handlers. 68 | /// 69 | /// Most systems will have the default priority (0) but in some cases the order 70 | /// that things happen is really important. If those cases are relevant 71 | /// externally then their priorities will be included here. 72 | /// 73 | /// > ### Note: How priorities work 74 | /// > When the event dispatcher recieves an event it will execute the handlers 75 | /// > in order of decreasing priority. Handlers with the same priority will be 76 | /// > executed in an unspecified order so if you need a handler to execute 77 | /// > before another then it must have a higher priority. 78 | pub mod priority { 79 | pub const HIGH: i32 = 500; 80 | pub const MEDIUM: i32 = 250; 81 | 82 | pub const CLEANUP: i32 = -500; 83 | 84 | pub use crate::dispatch::DEFAULT_PRIORITY as DEFAULT; 85 | 86 | /// Priority of the handler that sends the login packet when a client attempts 87 | /// to log in. 88 | /// 89 | /// If you want to modify the state of the player before they get the login 90 | /// response then your event handler will need to have a greater priority than 91 | /// this. 92 | pub const LOGIN: i32 = 1000; 93 | pub const PRE_LOGIN: i32 = 1500; 94 | } 95 | 96 | /// Utilities to help with writing tests for server functionality. 97 | pub mod test { 98 | pub use crate::mock::*; 99 | } 100 | -------------------------------------------------------------------------------- /.github/workflows/build-project.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | push: 4 | branches-ignore: 5 | - 'dependabot/**' 6 | 7 | name: ci-linux 8 | 9 | jobs: 10 | test: 11 | name: airmash 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | toolchain: [stable, beta] 16 | config: [debug, release] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: ${{ matrix.toolchain }} 22 | profile: minimal 23 | override: true 24 | - uses: Swatinem/rust-cache@v1 25 | with: 26 | sharedKey: build-cache-${{ matrix.config }} 27 | - name: Setup cargo flags 28 | if: matrix.config == 'release' 29 | run: | 30 | echo CARGO_FLAGS=--release >> "$GITHUB_ENV" 31 | - uses: actions-rs/cargo@v1 32 | with: 33 | command: test 34 | args: --all-features --color always ${{ env.CARGO_FLAGS }} 35 | 36 | format: 37 | name: check-fmt 38 | runs-on: ubuntu-latest 39 | continue-on-error: true 40 | steps: 41 | - uses: actions/checkout@v2 42 | - uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: nightly 45 | profile: minimal 46 | override: true 47 | components: rustfmt 48 | - uses: actions-rs/cargo@v1 49 | with: 50 | command: fmt 51 | args: --all -- --check --config unstable_features=true 52 | 53 | verify-default-config: 54 | name: verify-default-config 55 | runs-on: ubuntu-latest 56 | 57 | needs: 58 | - test 59 | 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: actions-rs/toolchain@v1 63 | with: 64 | toolchain: stable 65 | profile: minimal 66 | override: true 67 | - uses: Swatinem/rust-cache@v1 68 | with: 69 | sharedKey: build-cache-release 70 | - name: Build and run 71 | run: | 72 | cargo run --release --features script --example export --color always > new-default.json 73 | - name: Diff configs 74 | run: | 75 | cp configs/default.json old-default.json 76 | diff -u new-default.json old-default.json 77 | 78 | 79 | validate-configs: 80 | name: validate-configs 81 | runs-on: ubuntu-latest 82 | continue-on-error: true 83 | 84 | needs: 85 | - test 86 | 87 | steps: 88 | - uses: actions/checkout@v2 89 | - uses: actions-rs/toolchain@v1 90 | with: 91 | toolchain: stable 92 | profile: minimal 93 | override: true 94 | components: rustfmt 95 | - uses: Swatinem/rust-cache@v1 96 | with: 97 | sharedKey: build-cache-release 98 | - name: Build and run 99 | run: | 100 | cargo run --release --features script --color always --example validate -- configs/*.lua 101 | 102 | verify-pass: 103 | name: verify-tests-pass 104 | needs: 105 | - test 106 | - format 107 | - verify-default-config 108 | - validate-configs 109 | runs-on: ubuntu-latest 110 | 111 | steps: 112 | - name: no-op 113 | run: | 114 | echo "All checks passed!" 115 | -------------------------------------------------------------------------------- /server/src/component/effect.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::time::Instant; 3 | 4 | use crate::config::EffectPrototype; 5 | use crate::protocol::PowerupType; 6 | 7 | /// Effect manager for a player. 8 | /// 9 | /// This component tracks the set of effects that a player has. It has two types 10 | /// of effects: 11 | /// 1. short-term effects associated with a powerup, and, 12 | /// 2. long-term effects that have their lifetime explicitly managed. 13 | #[derive(Clone, Debug, Default)] 14 | pub struct Effects { 15 | permanent: HashMap<&'static str, EffectPrototype>, 16 | powerup: Option, 17 | } 18 | 19 | #[derive(Clone, Debug)] 20 | struct PowerupEffects { 21 | powerup: PowerupType, 22 | expiry: Instant, 23 | effects: Vec, 24 | } 25 | 26 | impl Effects { 27 | /// Enable a new set of effects associated with the powerup. This will 28 | /// overwrite any effects associated with the previously active powerup. 29 | pub fn set_powerup( 30 | &mut self, 31 | powerup: PowerupType, 32 | expiry: Instant, 33 | effects: &[EffectPrototype], 34 | ) { 35 | self.powerup = Some(PowerupEffects { 36 | powerup, 37 | expiry, 38 | effects: effects 39 | .iter() 40 | .filter(|e| !e.is_instant()) 41 | .cloned() 42 | .collect(), 43 | }); 44 | } 45 | 46 | pub fn clear_powerup(&mut self) { 47 | self.powerup = None; 48 | } 49 | 50 | /// Get the expiry time of the current powerup. 51 | pub fn expiry(&self) -> Option { 52 | self.powerup.as_ref().map(|p| p.expiry) 53 | } 54 | 55 | /// Get the server type of the current powerup. 56 | pub fn powerup(&self) -> Option { 57 | self.powerup.as_ref().map(|p| p.powerup) 58 | } 59 | 60 | /// Add a new long-term effect. Long-term effects are deduplicated by name. 61 | pub fn add_effect(&mut self, name: &'static str, effect: EffectPrototype) { 62 | self.permanent.insert(name, effect); 63 | } 64 | 65 | /// Remove a long-term effect by prototype name. 66 | pub fn remove_effect(&mut self, name: &str) -> bool { 67 | self.permanent.remove(name).is_some() 68 | } 69 | 70 | pub fn effects<'a>(&'a self) -> impl Iterator { 71 | let permanent = self.permanent.iter().map(|x| x.1); 72 | 73 | let temporary = self 74 | .powerup 75 | .as_ref() 76 | .map(|p| p.effects.as_slice()) 77 | .unwrap_or(&[]) 78 | .iter(); 79 | 80 | permanent.chain(temporary) 81 | } 82 | } 83 | 84 | impl Effects { 85 | /// Whether any of the effects within this component are inferno effects. 86 | pub fn has_inferno(&self) -> bool { 87 | self 88 | .effects() 89 | .any(|e| matches!(e, EffectPrototype::Inferno)) 90 | } 91 | 92 | pub fn has_shield(&self) -> bool { 93 | self.damage_mult() == 0.0 94 | } 95 | 96 | pub fn damage_mult(&self) -> f32 { 97 | self 98 | .effects() 99 | .filter_map(|e| match e { 100 | EffectPrototype::Shield { damage_mult } => Some(*damage_mult), 101 | _ => None, 102 | }) 103 | .reduce(|acc, mult| acc * mult) 104 | .unwrap_or(1.0) 105 | } 106 | 107 | pub fn fixed_speed(&self) -> Option { 108 | self.effects().find_map(|e| match e { 109 | EffectPrototype::FixedSpeed { speed } => Some(*speed), 110 | _ => None, 111 | }) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /server-config/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::error::Error; 3 | use std::fmt; 4 | 5 | pub struct Path { 6 | segments: Vec, 7 | } 8 | 9 | impl Path { 10 | pub fn new(segment: Segment) -> Self { 11 | Self { 12 | segments: vec![segment], 13 | } 14 | } 15 | 16 | pub fn push(&mut self, segment: Segment) { 17 | self.segments.push(segment); 18 | } 19 | 20 | pub fn with(mut self, segment: Segment) -> Self { 21 | self.push(segment); 22 | self 23 | } 24 | } 25 | 26 | impl fmt::Display for Path { 27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 28 | let mut iter = self.segments.iter().rev(); 29 | 30 | if let Some(seg) = iter.next() { 31 | seg.fmt(f)?; 32 | } 33 | 34 | for seg in iter { 35 | f.write_str(".")?; 36 | seg.fmt(f)?; 37 | } 38 | 39 | Ok(()) 40 | } 41 | } 42 | 43 | impl fmt::Debug for Path { 44 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 45 | f.write_fmt(format_args!("\"{}\"", self)) 46 | } 47 | } 48 | 49 | #[derive(Clone, Debug, Eq, PartialEq, Hash)] 50 | pub enum Segment { 51 | Field(Cow<'static, str>), 52 | Index(usize), 53 | } 54 | 55 | impl fmt::Display for Segment { 56 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | match self { 58 | Self::Field(field) => f.write_str(field), 59 | Self::Index(index) => index.fmt(f), 60 | } 61 | } 62 | } 63 | 64 | impl From<&'static str> for Segment { 65 | fn from(field: &'static str) -> Self { 66 | Self::Field(field.into()) 67 | } 68 | } 69 | 70 | impl From for Segment { 71 | fn from(field: String) -> Self { 72 | Self::Field(field.into()) 73 | } 74 | } 75 | 76 | impl From for Segment { 77 | fn from(index: usize) -> Self { 78 | Self::Index(index) 79 | } 80 | } 81 | 82 | struct StringError(String); 83 | 84 | impl fmt::Debug for StringError { 85 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 86 | f.write_str(&self.0) 87 | } 88 | } 89 | 90 | impl fmt::Display for StringError { 91 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 92 | f.write_str(&self.0) 93 | } 94 | } 95 | 96 | impl Error for StringError {} 97 | 98 | #[derive(Debug)] 99 | pub struct ValidationError { 100 | path: Path, 101 | error: Box, 102 | } 103 | 104 | impl ValidationError { 105 | pub fn new(field: I, error: E) -> Self 106 | where 107 | I: Into, 108 | E: Error + Send + Sync + 'static, 109 | { 110 | Self { 111 | path: Path::new(field.into()), 112 | error: Box::new(error), 113 | } 114 | } 115 | 116 | pub fn custom(field: I, message: D) -> Self 117 | where 118 | I: Into, 119 | D: fmt::Display, 120 | { 121 | Self::new(field.into(), StringError(format!("{}", message))) 122 | } 123 | 124 | pub fn with(mut self, field: I) -> Self 125 | where 126 | I: Into, 127 | { 128 | self.path.push(field.into()); 129 | self 130 | } 131 | 132 | pub fn path(&self) -> &Path { 133 | &self.path 134 | } 135 | } 136 | 137 | impl fmt::Display for ValidationError { 138 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 139 | f.write_fmt(format_args!("error while validating field `{}`", self.path)) 140 | } 141 | } 142 | 143 | impl Error for ValidationError { 144 | fn source(&self) -> Option<&(dyn Error + 'static)> { 145 | Some(&*self.error) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /server/tests/behaviour/powerups.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use airmash::component::*; 4 | use airmash::protocol::{server as s, ServerPacket}; 5 | use airmash::test::TestGame; 6 | use airmash::util::NalgebraExt; 7 | use airmash::Vector2; 8 | 9 | #[test] 10 | fn player_is_upgraded_on_collision_with_upgrade() { 11 | let (mut game, mut mock) = TestGame::new(); 12 | 13 | let mut client = mock.open(); 14 | let player = client.login("test", &mut game); 15 | let powerup = game.spawn_mob(MobType::Inferno, Vector2::zeros(), Duration::from_secs(60)); 16 | 17 | game.world.get_mut::(player).unwrap().0 = Vector2::zeros(); 18 | game.run_once(); 19 | 20 | assert!( 21 | !game.world.contains(powerup), 22 | "Powerup was not despawned despite having collided with a player" 23 | ); 24 | 25 | let effects = game.world.get::(player).unwrap(); 26 | 27 | assert!(matches!(effects.powerup(), Some(PowerupType::Inferno))); 28 | } 29 | 30 | #[test] 31 | fn dual_powerup_collision() { 32 | let (mut game, mut mock) = TestGame::new(); 33 | 34 | let mut client1 = mock.open(); 35 | let mut client2 = mock.open(); 36 | 37 | let p1 = client1.login("p1", &mut game); 38 | let p2 = client2.login("p2", &mut game); 39 | 40 | game.world.get_mut::(p1).unwrap().0 = Vector2::zeros(); 41 | game.world.get_mut::(p2).unwrap().0 = Vector2::zeros(); 42 | 43 | game.run_for(Duration::from_secs(4)); 44 | let powerup = game.spawn_mob(MobType::Inferno, Vector2::zeros(), Duration::from_secs(60)); 45 | game.run_once(); 46 | 47 | assert!( 48 | !game.world.contains(powerup), 49 | "Powerup was not despawned despite having collided with a player" 50 | ); 51 | 52 | let p1pow = game.world.get::(p1).unwrap(); 53 | let p2pow = game.world.get::(p2).unwrap(); 54 | 55 | assert!(p1pow.powerup().is_some() != p2pow.powerup().is_some()); 56 | } 57 | 58 | #[test] 59 | fn inferno_slows_down_plane() { 60 | let (mut game, mut mock) = TestGame::new(); 61 | 62 | let mut client = mock.open(); 63 | let entity = client.login("test", &mut game); 64 | 65 | game.world.get_mut::(entity).unwrap().0 = Vector2::zeros(); 66 | game.spawn_mob(MobType::Inferno, Vector2::zeros(), Duration::from_secs(60)); 67 | game.run_for(Duration::from_secs(2)); 68 | 69 | assert!(client.packets().any(|p| matches!( 70 | p, 71 | ServerPacket::PlayerPowerup(s::PlayerPowerup { 72 | ty: PowerupType::Inferno, 73 | .. 74 | }) 75 | ))); 76 | 77 | let has_inferno = client 78 | .packets() 79 | .filter_map(|p| match p { 80 | ServerPacket::PlayerUpdate(p) => Some(p), 81 | _ => None, 82 | }) 83 | .any(|p| p.upgrades.inferno); 84 | assert!(has_inferno); 85 | } 86 | 87 | #[test] 88 | fn first_spawn_has_2s_shield() { 89 | let (mut game, mut mock) = TestGame::new(); 90 | 91 | let mut client = mock.open(); 92 | client.login("test", &mut game); 93 | game.run_once(); 94 | 95 | assert!(client.packets().any(|p| matches!( 96 | p, 97 | ServerPacket::PlayerPowerup(s::PlayerPowerup { 98 | ty: PowerupType::Shield, 99 | .. 100 | }) 101 | ))); 102 | 103 | game.run_for(Duration::from_secs(3)); 104 | let _ = client.packets().count(); 105 | game.run_for(Duration::from_secs(3)); 106 | 107 | let no_shield = !client 108 | .packets() 109 | .filter_map(|p| match p { 110 | ServerPacket::PlayerUpdate(p) => Some(p), 111 | _ => None, 112 | }) 113 | .any(|p| p.upgrades.shield); 114 | assert!(no_shield); 115 | } 116 | -------------------------------------------------------------------------------- /server-config/src/script.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | macro_rules! declare_userdata { 4 | { 5 | $( struct $st:ident : $type:ty => [$( $name:ident ),* $(,)? ] ; )* 6 | } => { 7 | $( 8 | struct $st; 9 | impl ::rlua::UserData for $st { 10 | fn add_methods<'lua, T: ::rlua::UserDataMethods<'lua, Self>>(methods: &mut T) { 11 | $( 12 | methods.add_function(stringify!($name), |lua, ()| { 13 | serde_rlua::to_value(lua, &<$type>::$name()) 14 | }); 15 | )* 16 | } 17 | } 18 | )* 19 | } 20 | } 21 | 22 | fn patch_defaults<'lua>(lua: rlua::Context<'lua>) -> rlua::Result> { 23 | declare_userdata! { 24 | struct PlaneDefaults : PlanePrototype<'_, StringRef> => [ 25 | predator, 26 | tornado, 27 | mohawk, 28 | goliath, 29 | prowler, 30 | ]; 31 | 32 | struct MissileDefaults : MissilePrototype => [ 33 | predator, 34 | tornado, 35 | tornado_triple, 36 | mohawk, 37 | goliath, 38 | prowler, 39 | ]; 40 | 41 | struct SpecialDefaults : SpecialPrototype<'_, StringRef> => [ 42 | none, 43 | boost, 44 | multishot, 45 | repel, 46 | strafe, 47 | stealth 48 | ]; 49 | } 50 | 51 | let table = lua.create_table()?; 52 | table.set("plane", PlaneDefaults)?; 53 | table.set("missile", MissileDefaults)?; 54 | table.set("special", SpecialDefaults)?; 55 | 56 | Ok(table) 57 | } 58 | 59 | impl<'a> GamePrototype<'a, StringRef> { 60 | /// Similar to [`patch`] but it instead returns the rlua [`Value`] object 61 | /// directly. This isn't usually needed but it is useful if you want more 62 | /// control over the deserialization process. For example, the export binary 63 | /// uses this method so that it can get some additional information on which 64 | /// part failed to deserialize and use that for better error messages. 65 | /// 66 | /// [`patch`]: crate::GamePrototype::patch 67 | /// [`Value`]: rlua::Value 68 | pub fn patch_direct<'lua>( 69 | &self, 70 | lua: rlua::Context<'lua>, 71 | script: &str, 72 | ) -> rlua::Result> { 73 | let globals = lua.globals(); 74 | globals.set("data", serde_rlua::to_value(lua, self)?)?; 75 | globals.set("defaults", patch_defaults(lua)?)?; 76 | 77 | lua.load(script).exec()?; 78 | 79 | globals.get("data") 80 | } 81 | 82 | /// Run a LUA script which can modify this GamePrototype instance. 83 | /// 84 | /// The script itself is just a LUA file which modifies a few globals that are 85 | /// injected into its environment. These are 86 | /// - `data` - The serialized form of this `GamePrototype` instance. The 87 | /// script will modify this and at the end it will be assigned back to 88 | /// `self`. 89 | /// - `defaults` - A collection of user functions that will return various 90 | /// builtin prototypes so that they can be modified. There are no stability 91 | /// guarantees around the addition of new fields to any of the prototypes so 92 | /// using these functions to get one of the buitin prototypes and modifying 93 | /// them is the only reliable way to build completely new class types. 94 | pub fn patch(&mut self, script: &str) -> rlua::Result<()> { 95 | use rlua::{Lua, StdLib}; 96 | 97 | let libs = StdLib::ALL_NO_DEBUG; 98 | let lua = Lua::new_with(libs); 99 | *self = lua.context(|ctx| -> rlua::Result<_> { 100 | Ok(serde_rlua::from_value(self.patch_direct(ctx, script)?)?) 101 | })?; 102 | 103 | Ok(()) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /server-config/src/missile.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use protocol::MobType; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::ValidationError; 7 | 8 | #[derive(Clone, Debug, Serialize, Deserialize)] 9 | #[serde(deny_unknown_fields)] 10 | pub struct MissilePrototype { 11 | /// The name with which to refer to this missile prototype. It must be unique 12 | /// among all missile prototypes. 13 | pub name: Cow<'static, str>, 14 | 15 | /// The mob type that will be communicated to the client. 16 | /// 17 | /// This will determine the entity type that the client will show for the mob. 18 | /// Setting this to a non-missile type will not result in the client showing 19 | /// those mob types as moving mobs. 20 | pub server_type: MobType, 21 | 22 | /// The maximum speed at which this missile can travel. 23 | pub max_speed: f32, 24 | /// The base speed that this missile would be fired at if the plane firing it 25 | /// was not moving at all. 26 | pub base_speed: f32, 27 | /// The fraction of the speed of the parent that the missile inherits when it 28 | /// is fired. 29 | pub inherit_factor: f32, 30 | /// The rate of acceleration of the missile. 31 | pub accel: f32, 32 | /// The amount of damage that this missile would do to a goliath. 33 | pub damage: f32, 34 | /// The maximum distance that this missile will travel before it despawns. 35 | pub distance: f32, 36 | } 37 | 38 | impl MissilePrototype { 39 | pub const fn predator() -> Self { 40 | Self { 41 | name: Cow::Borrowed("predator"), 42 | server_type: MobType::PredatorMissile, 43 | max_speed: 9.0, 44 | base_speed: 4.05, 45 | inherit_factor: 0.3, 46 | accel: 0.105, 47 | damage: 0.4, 48 | distance: 1104.0, 49 | } 50 | } 51 | 52 | pub const fn tornado() -> Self { 53 | Self { 54 | name: Cow::Borrowed("tornado-single"), 55 | server_type: MobType::TornadoSingleMissile, 56 | max_speed: 7.0, 57 | base_speed: 3.5, 58 | inherit_factor: 0.3, 59 | accel: 0.0875, 60 | damage: 0.4, 61 | distance: 997.0, 62 | } 63 | } 64 | 65 | pub const fn prowler() -> Self { 66 | Self { 67 | name: Cow::Borrowed("prowler"), 68 | server_type: MobType::ProwlerMissile, 69 | max_speed: 7.0, 70 | base_speed: 2.8, 71 | inherit_factor: 0.3, 72 | accel: 0.07, 73 | damage: 0.45, 74 | distance: 819.0, 75 | } 76 | } 77 | 78 | pub const fn mohawk() -> Self { 79 | Self { 80 | name: Cow::Borrowed("mohawk"), 81 | server_type: MobType::MohawkMissile, 82 | max_speed: 9.0, 83 | base_speed: 5.7, 84 | inherit_factor: 0.3, 85 | accel: 0.14, 86 | damage: 0.2, 87 | distance: 1161.0, 88 | } 89 | } 90 | 91 | pub const fn goliath() -> Self { 92 | Self { 93 | name: Cow::Borrowed("goliath"), 94 | server_type: MobType::GoliathMissile, 95 | max_speed: 6.0, 96 | base_speed: 2.1, 97 | inherit_factor: 0.3, 98 | accel: 0.0375, 99 | damage: 1.2, 100 | distance: 1076.0, 101 | } 102 | } 103 | 104 | pub const fn tornado_triple() -> Self { 105 | Self { 106 | name: Cow::Borrowed("tornado-triple"), 107 | server_type: MobType::TornadoTripleMissile, 108 | max_speed: 7.0, 109 | base_speed: 3.5, 110 | inherit_factor: 0.3, 111 | accel: 0.0875, 112 | damage: 0.3, 113 | distance: 581.0, 114 | } 115 | } 116 | } 117 | 118 | impl MissilePrototype { 119 | pub(crate) fn resolve(self) -> Result { 120 | if self.name.is_empty() { 121 | return Err(ValidationError::custom("name", "prototype had empty name")); 122 | } 123 | 124 | Ok(self) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /ctf/src/shuffle/mod.rs: -------------------------------------------------------------------------------- 1 | use airmash::component::*; 2 | use airmash::{AirmashGame, Entity}; 3 | use rand::prelude::SliceRandom; 4 | 5 | use crate::config::{BLUE_TEAM, RED_TEAM}; 6 | 7 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 8 | pub struct TeamChangeEntry { 9 | pub player: Entity, 10 | pub team: u16, 11 | } 12 | 13 | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Hash)] 14 | #[allow(dead_code)] 15 | pub enum ShuffleType { 16 | AlternatingScore, 17 | AlternatingEarnings, 18 | EvenRandom, 19 | } 20 | 21 | /// A shuffle that alternates players in order of current score 22 | pub fn alternating_score_shuffle(game: &mut AirmashGame) -> Vec { 23 | let mut players = game 24 | .world 25 | .query_mut::<&Score>() 26 | .with::() 27 | .into_iter() 28 | .map(|(player, score)| (player, score.0)) 29 | .collect::>(); 30 | 31 | let start = if rand::random() { 0 } else { 1 }; 32 | let teams = [BLUE_TEAM, RED_TEAM]; 33 | players.sort_unstable_by_key(|p| p.1); 34 | 35 | players 36 | .into_iter() 37 | .enumerate() 38 | .filter_map(|(index, (player, _))| { 39 | let old_team = game.world.get::(player).ok()?.0; 40 | let new_team = teams[(index + start) % 2]; 41 | 42 | match old_team == new_team { 43 | true => None, 44 | false => Some(TeamChangeEntry { 45 | player, 46 | team: new_team, 47 | }), 48 | } 49 | }) 50 | .collect() 51 | } 52 | 53 | /// A shuffle that alternates players in order of total earnings 54 | pub fn alternating_earnings_shuffle(game: &mut AirmashGame) -> Vec { 55 | let mut players = game 56 | .world 57 | .query_mut::<&Earnings>() 58 | .with::() 59 | .into_iter() 60 | .map(|(player, score)| (player, score.0)) 61 | .collect::>(); 62 | 63 | let start = if rand::random() { 0 } else { 1 }; 64 | let teams = [BLUE_TEAM, RED_TEAM]; 65 | players.sort_unstable_by_key(|p| p.1); 66 | 67 | players 68 | .into_iter() 69 | .enumerate() 70 | .filter_map(|(index, (player, _))| { 71 | let old_team = game.world.get::(player).ok()?.0; 72 | let new_team = teams[(index + start) % 2]; 73 | 74 | match old_team == new_team { 75 | true => None, 76 | false => Some(TeamChangeEntry { 77 | player, 78 | team: new_team, 79 | }), 80 | } 81 | }) 82 | .collect() 83 | } 84 | 85 | /// A random shuffle where each team ends up with the same number of players 86 | pub fn even_random_shuffle(game: &mut AirmashGame) -> Vec { 87 | let mut players = game 88 | .world 89 | .query_mut::<()>() 90 | .with::() 91 | .into_iter() 92 | .map(|(player, _)| player) 93 | .collect::>(); 94 | 95 | let teams = if rand::random() { 96 | [BLUE_TEAM, RED_TEAM] 97 | } else { 98 | [RED_TEAM, BLUE_TEAM] 99 | }; 100 | let half = players.len() / 2; 101 | 102 | players.shuffle(&mut rand::thread_rng()); 103 | 104 | players 105 | .into_iter() 106 | .enumerate() 107 | .filter_map(|(index, player)| { 108 | let old_team = game.world.get::(player).ok()?.0; 109 | let new_team = teams[if index < half { 0 } else { 1 }]; 110 | 111 | match old_team == new_team { 112 | true => None, 113 | false => Some(TeamChangeEntry { 114 | player, 115 | team: new_team, 116 | }), 117 | } 118 | }) 119 | .collect() 120 | } 121 | 122 | pub fn shuffle(game: &mut AirmashGame, ty: ShuffleType) -> Vec { 123 | match ty { 124 | ShuffleType::AlternatingScore => alternating_score_shuffle(game), 125 | ShuffleType::AlternatingEarnings => alternating_earnings_shuffle(game), 126 | ShuffleType::EvenRandom => even_random_shuffle(game), 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /server/src/system/handler/on_player_missile_collision.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use smallvec::SmallVec; 4 | 5 | use crate::component::*; 6 | use crate::config::{MissilePrototypeRef, PlanePrototypeRef}; 7 | use crate::event::{PlayerHit, PlayerKilled, PlayerMissileCollision}; 8 | use crate::resource::GameConfig; 9 | use crate::AirmashGame; 10 | 11 | #[handler(priority = crate::priority::MEDIUM)] 12 | fn damage_player(event: &PlayerMissileCollision, game: &mut AirmashGame) { 13 | let query = game 14 | .world 15 | .query_one_mut::<(&MissilePrototypeRef, &Owner, &IsMissile)>(event.missile); 16 | let (&mob, &owner, _) = match query { 17 | Ok(query) => query, 18 | Err(_) => return, 19 | }; 20 | 21 | let game_config = game.resources.read::(); 22 | let attacker = game.world.get::(owner.0).ok().map(|_| owner.0); 23 | 24 | let mut events = SmallVec::<[_; 16]>::new(); 25 | let mut hits = SmallVec::<[_; 16]>::new(); 26 | let mut killed = HashSet::new(); 27 | for player in event.players.iter().copied() { 28 | let query = game.world.query_one::<( 29 | &mut Health, 30 | &PlanePrototypeRef, 31 | &Effects, 32 | &Upgrades, 33 | &mut IsAlive, 34 | )>(player); 35 | let mut query = match query { 36 | Ok(query) => query.with::(), 37 | Err(_) => continue, 38 | }; 39 | 40 | if let Some((health, &plane, effects, upgrades, alive)) = query.get() { 41 | // No damage can be done if the player is dead 42 | if !alive.0 { 43 | continue; 44 | } 45 | 46 | let damage = match game_config.allow_damage { 47 | true => { 48 | mob.damage * plane.damage_factor 49 | / crate::consts::UPGRADE_MULTIPLIERS[upgrades.defense as usize] 50 | * effects.damage_mult() 51 | } 52 | false => 0.0, 53 | }; 54 | health.0 -= damage; 55 | 56 | hits.push(PlayerHit { 57 | player, 58 | missile: event.missile, 59 | damage, 60 | attacker, 61 | }); 62 | 63 | if health.0 <= 0.0 { 64 | // Avoid double-kills if multiple missiles hit the player in the same frame. 65 | if !killed.insert(player) { 66 | continue; 67 | } 68 | 69 | let owner = game.world.get::(owner.0).ok().map(|_| owner.0); 70 | 71 | events.push(PlayerKilled { 72 | missile: event.missile, 73 | player, 74 | killer: owner, 75 | }); 76 | } 77 | } 78 | } 79 | 80 | drop(game_config); 81 | 82 | game.dispatch_many(hits); 83 | game.dispatch_many(events); 84 | } 85 | 86 | #[handler] 87 | fn send_player_hit(event: &PlayerMissileCollision, game: &mut AirmashGame) { 88 | use crate::protocol::server::{PlayerHit, PlayerHitPlayer}; 89 | 90 | let query = game 91 | .world 92 | .query_one_mut::<(&MissilePrototypeRef, &Owner, &Position, &IsMissile)>(event.missile); 93 | let (&mob, &owner, &pos, _) = match query { 94 | Ok(query) => query, 95 | Err(_) => return, 96 | }; 97 | 98 | let players = event 99 | .players 100 | .iter() 101 | .filter_map(|&player| { 102 | let query = game.world.query_one::<(&Health, &HealthRegen)>(player); 103 | let mut query = match query { 104 | Ok(query) => query.with::(), 105 | Err(_) => return None, 106 | }; 107 | 108 | query.get().map(|(health, regen)| PlayerHitPlayer { 109 | id: player.id() as _, 110 | health: health.0, 111 | health_regen: regen.0, 112 | }) 113 | }) 114 | .collect(); 115 | 116 | let packet = PlayerHit { 117 | id: event.missile.id() as _, 118 | owner: owner.0.id() as _, 119 | pos: pos.into(), 120 | ty: mob.server_type, 121 | players, 122 | }; 123 | 124 | game.send_to_visible(packet.pos.into(), packet); 125 | } 126 | -------------------------------------------------------------------------------- /server/src/system/admin.rs: -------------------------------------------------------------------------------- 1 | use bstr::BString; 2 | use smallvec::SmallVec; 3 | 4 | use crate::component::*; 5 | use crate::event::PacketEvent; 6 | use crate::protocol::client::Command; 7 | use crate::protocol::server::CommandReply; 8 | use crate::protocol::CommandReplyType; 9 | use crate::resource::GameConfig; 10 | use crate::{AirmashGame, Vector2}; 11 | 12 | #[handler] 13 | fn teleport(event: &PacketEvent, game: &mut AirmashGame) { 14 | #[derive(Copy, Clone, Debug, PartialEq)] 15 | pub struct ParsedCommand { 16 | pub id: Option, 17 | pub pos_x: f32, 18 | pub pos_y: f32, 19 | } 20 | 21 | fn named_positions(s: &[u8]) -> Option { 22 | let (x, y) = match s { 23 | b"blue-flag" => (-9670.0, -1470.0), 24 | b"red-flag" => (8600.0, -940.0), 25 | b"greenland-spa-and-lounge" => (-5000.0, -7000.0), 26 | b"greenland" => (-5000.0, -7000.0), 27 | b"crimea" => (2724.0, -2321.0), 28 | // The exact origin of how this name was 29 | // determined is shrouded in mystery. 30 | b"mt-detect" => (3550.0, -850.0), 31 | b"red-spawn" => (7818.0, -2930.0), 32 | b"blue-spawn" => (-8878.0, -2971.0), 33 | _ => return None, 34 | }; 35 | 36 | Some(Vector2::new(x, y)) 37 | } 38 | 39 | fn parse_command_data(s: &BString) -> Result { 40 | let args: SmallVec<[_; 3]> = s.split(|&x| x == b' ').collect(); 41 | 42 | fn parse_arg(bytes: &[u8], err: &'static str) -> Result { 43 | std::str::from_utf8(bytes) 44 | .map_err(|_| err)? 45 | .parse() 46 | .map_err(|_| err) 47 | } 48 | 49 | let id = match parse_arg(args[0], "Player ID was not a number")? { 50 | 0 => None, 51 | x => Some(x), 52 | }; 53 | 54 | let command = if args.len() == 3 { 55 | ParsedCommand { 56 | id, 57 | pos_x: parse_arg(args[1], "Couldn't parse position")?, 58 | pos_y: parse_arg(args[2], "Couldn't parse position")?, 59 | } 60 | } else { 61 | let pos = match named_positions(args[1]) { 62 | Some(pos) => pos, 63 | None => return Err("Unknown named position".to_string()), 64 | }; 65 | 66 | ParsedCommand { 67 | id, 68 | pos_x: pos.x, 69 | pos_y: pos.y, 70 | } 71 | }; 72 | 73 | if command.pos_x.abs() > 16384.0 { 74 | return Err(format!("{} is out of bounds", command.pos_x)); 75 | } 76 | if command.pos_y.abs() > 8192.0 { 77 | return Err(format!("{} is out of bounds", command.pos_y)); 78 | } 79 | 80 | Ok(command) 81 | } 82 | 83 | if !game.resources.read::().admin_enabled { 84 | return; 85 | } 86 | 87 | if event.packet.com != "teleport" { 88 | return; 89 | } 90 | 91 | let command = match parse_command_data(&event.packet.data) { 92 | Ok(command) => command, 93 | Err(e) => { 94 | game.send_to( 95 | event.entity, 96 | CommandReply { 97 | ty: CommandReplyType::ShowInConsole, 98 | text: e.into(), 99 | }, 100 | ); 101 | return; 102 | } 103 | }; 104 | 105 | let target = match command.id { 106 | Some(id) => match game.find_entity_by_id(id) { 107 | Some(ent) => ent, 108 | None => { 109 | game.send_to( 110 | event.entity, 111 | CommandReply { 112 | ty: CommandReplyType::ShowInConsole, 113 | text: "Unknown entity".into(), 114 | }, 115 | ); 116 | return; 117 | } 118 | }, 119 | None => event.entity, 120 | }; 121 | 122 | let start_time = game.start_time(); 123 | 124 | if let Ok(mut pos) = game.world.get_mut::(target) { 125 | pos.x = command.pos_x; 126 | pos.y = command.pos_y; 127 | } 128 | 129 | // If we've teleported a player then have an update happen right away. 130 | if let Ok(mut last_update) = game.world.get_mut::(target) { 131 | last_update.0 = start_time; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /server-config/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::cell::Cell; 2 | use std::mem::ManuallyDrop; 3 | use std::ops::{Deref, DerefMut}; 4 | 5 | pub(crate) mod duration { 6 | use std::time::Duration; 7 | 8 | use serde::{Deserialize, Deserializer, Serializer}; 9 | 10 | pub(crate) fn serialize(dur: &Duration, ser: S) -> Result { 11 | ser.serialize_f64(dur.as_secs_f64()) 12 | } 13 | 14 | pub(crate) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result { 15 | f64::deserialize(de).map(Duration::from_secs_f64) 16 | } 17 | } 18 | 19 | pub(crate) mod option_duration { 20 | use std::time::Duration; 21 | 22 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 23 | 24 | pub(crate) fn serialize( 25 | dur: &Option, 26 | ser: S, 27 | ) -> Result { 28 | dur.map(|d| d.as_secs_f64()).serialize(ser) 29 | } 30 | 31 | pub(crate) fn deserialize<'de, D: Deserializer<'de>>( 32 | de: D, 33 | ) -> Result, D::Error> { 34 | Ok(Option::deserialize(de)?.map(Duration::from_secs_f64)) 35 | } 36 | } 37 | 38 | pub(crate) mod vector { 39 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 40 | 41 | use crate::Vector2; 42 | 43 | pub(crate) fn serialize(v: &Vector2, ser: S) -> Result { 44 | [v.x, v.y].serialize(ser) 45 | } 46 | 47 | pub(crate) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result { 48 | <[f32; 2]>::deserialize(de).map(From::from) 49 | } 50 | } 51 | 52 | /// Wrapper type around [`ManuallyDrop`] which drops the contained value unless 53 | /// it is explicitly prevented from doing so. 54 | pub(crate) struct MaybeDrop { 55 | item: ManuallyDrop, 56 | flag: Cell, 57 | } 58 | 59 | impl MaybeDrop { 60 | pub fn new(item: T) -> Self { 61 | Self { 62 | item: ManuallyDrop::new(item), 63 | flag: Cell::new(true), 64 | } 65 | } 66 | 67 | /// Prevent the contained value from being dropped when this `MaybeDrop` is 68 | /// dropped. 69 | pub fn cancel_drop(slot: &Self) { 70 | slot.flag.set(false) 71 | } 72 | } 73 | 74 | impl From> for MaybeDrop { 75 | fn from(item: ManuallyDrop) -> Self { 76 | Self::new(ManuallyDrop::into_inner(item)) 77 | } 78 | } 79 | 80 | impl Drop for MaybeDrop { 81 | fn drop(&mut self) { 82 | if self.flag.get() { 83 | // SAFETY: This is the only place where self.item is dropped so there is no 84 | // possibility of double-drops. 85 | unsafe { ManuallyDrop::drop(&mut self.item) } 86 | } 87 | } 88 | } 89 | 90 | impl Deref for MaybeDrop { 91 | type Target = T; 92 | 93 | fn deref(&self) -> &Self::Target { 94 | &self.item 95 | } 96 | } 97 | 98 | impl DerefMut for MaybeDrop { 99 | fn deref_mut(&mut self) -> &mut Self::Target { 100 | &mut self.item 101 | } 102 | } 103 | 104 | /// RAII wrapper that drops whatever the stored pointer points to. 105 | pub(crate) struct DropPtr(*mut ManuallyDrop); 106 | 107 | impl DropPtr { 108 | /// # Safety 109 | /// `ptr` must be valid to drop until the `DropPtr` instance drops or is 110 | /// forgotten. 111 | pub unsafe fn new(ptr: *mut ManuallyDrop) -> Self { 112 | Self(ptr) 113 | } 114 | } 115 | 116 | impl Drop for DropPtr { 117 | fn drop(&mut self) { 118 | // SAFETY: The safety contract for DropPtr::new guarantees that this is safe. 119 | unsafe { ManuallyDrop::drop(&mut *self.0) } 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | mod tests { 125 | use super::*; 126 | 127 | struct DropWrite<'a>(&'a mut bool); 128 | 129 | impl Drop for DropWrite<'_> { 130 | fn drop(&mut self) { 131 | *self.0 = true; 132 | } 133 | } 134 | 135 | #[test] 136 | fn maybedrop_drops_by_default() { 137 | let mut check = false; 138 | 139 | { 140 | let _drop = MaybeDrop::new(DropWrite(&mut check)); 141 | } 142 | 143 | assert!(check); 144 | } 145 | 146 | #[test] 147 | fn maybedrop_no_drop_when_disabled() { 148 | let mut check = false; 149 | 150 | { 151 | let drop = MaybeDrop::new(DropWrite(&mut check)); 152 | MaybeDrop::cancel_drop(&drop); 153 | } 154 | 155 | assert!(!check); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /notes/movement-prediction.md: -------------------------------------------------------------------------------- 1 | 2 | # Movement Prediction 3 | 4 | This is deobfuscated code from ``engine.js``, it executes at least logically 5 | for every frame, but may execute partially according to some external clock 6 | (``performance.now()``?). 7 | 8 | It is responsible for maintaining the previous server position/keystate update 9 | for a client until that client changes state causing the server to emit a new 10 | canonical update. 11 | 12 | Variables heavily renamed. It looks like the body of the ``for`` loop has been 13 | inlined by Closure Compiler from some additional function, the same code could 14 | obviously be much cleaner. Hunch is that the loop body used to execute at a 15 | constant rate but that caused lag issues, and so they wrapped it in the weird 16 | loop to sync it to display or network clock. 17 | 18 | ````javascript 19 | 20 | // NFRAMES = fractional number of frames to process 21 | 22 | // RNDFRAMES = 23 | // 1 if NFRAMES<0.51, 24 | // else mostly whole number of frames 25 | 26 | // FRAMEFRAC = fractional increase of power/speed/rotation per loop 27 | 28 | var RNDFRAMES = NFRAMES > .51 ? Math.round(NFRAMES) : 1; 29 | var FRAMEFRAC = NFRAMES / RNDFRAMES; 30 | var TWOPI = 2 * Math.PI; 31 | var BOOSTMUL = this.boost ? 1.5 : 1; 32 | 33 | for (var frameI = 0; frameI < RNDFRAMES; frameI++) { 34 | this.energy += FRAMEFRAC * this.energyRegen; 35 | this.energy >= 1 && (this.energy = 1); 36 | this.health += FRAMEFRAC * this.healthRegen; 37 | this.health >= 1 && (this.health = 1); 38 | 39 | var speedDeltaAngle = -999; 40 | if(this.strafe) { 41 | if(this.keystate.LEFT) { 42 | speedDeltaAngle = this.rotation - .5 * Math.PI; 43 | } 44 | if(this.keystate.RIGHT) { 45 | speedDeltaAngle = this.rotation + .5 * Math.PI; 46 | } 47 | } else { 48 | if(this.keystate.LEFT) { 49 | this.rotation += -FRAMEFRAC * ship.turnFactor 50 | } 51 | if(this.keystate.RIGHT) { 52 | this.rotation += FRAMEFRAC * ship.turnFactor 53 | } 54 | } 55 | 56 | if(this.keystate.UP) { 57 | if(speedDeltaAngle == -999) { 58 | speedDeltaAngle = this.rotation; 59 | } else { 60 | speedDeltaAngle += Math.PI * (this.keystate.RIGHT ? -0.25 : 0.25); 61 | } 62 | } else if(this.keystate.DOWN) { 63 | if(speedDeltaAngle == -999) { 64 | speedDeltaAngle = this.rotation + Math.PI; 65 | } else { 66 | speedDeltaAngle = += Math.PI * (this.keystate.RIGHT ? 0.25 : -0.25) 67 | } 68 | } 69 | 70 | var speedX = this.speed.x; 71 | var speedY = this.speed.y; 72 | if(speedDeltaAngle != -999) { 73 | this.speed.x += Math.sin(speedDeltaAngle) * ship.accelFactor * FRAMEFRAC * BOOSTMUL; 74 | this.speed.y -= Math.cos(speedDeltaAngle) * ship.accelFactor * FRAMEFRAC * BOOSTMUL; 75 | } 76 | 77 | 78 | var curShipMaxSpeed = ship.maxSpeed * BOOSTMUL * ship.upgrades.speed.factor[this.speedupgrade]; 79 | if(this.powerups.rampage) { 80 | curShipMaxSpeed *= 0.75; 81 | } 82 | 83 | if(this.flagspeed) { 84 | curShipMaxSpeed = 5; 85 | } 86 | 87 | var speedVecLength = this.speed.length(); 88 | if(speedVecLength > curShipMaxSpeed) { 89 | this.speed.multiply(curShipMaxSpeed / speedVecLength); 90 | } else if(this.speed.x > ship.minSpeed || this.speed.x < -ship.minSpeed || 91 | this.speed.y > ship.minSpeed || this.speed.y < -ship.minSpeed) { 92 | this.speed.x *= 1 - ship.brakeFactor * FRAMEFRAC; 93 | this.speed.y *= 1 - ship.brakeFactor * FRAMEFRAC; 94 | } else { 95 | this.speed.x = 0; 96 | this.speed.y = 0; 97 | } 98 | 99 | this.pos.x += FRAMEFRAC * speedX + .5 * (this.speed.x - speedX) * FRAMEFRAC * FRAMEFRAC; 100 | this.pos.y += FRAMEFRAC * speedY + .5 * (this.speed.y - speedY) * FRAMEFRAC * FRAMEFRAC; 101 | this.clientCalcs(FRAMEFRAC); 102 | } 103 | 104 | this.rotation = ((this.rotation % TWOPI) + TWOPI) % TWOPI); 105 | if(-1 != game.gameType) { 106 | (this.pos.x < -16352 && (this.pos.x = -16352), 107 | this.pos.x > 16352 && (this.pos.x = 16352), 108 | this.pos.y < -8160 && (this.pos.y = -8160), 109 | this.pos.y > 8160 && (this.pos.y = 8160)) 110 | } else { 111 | (this.pos.x < -16384 && (this.pos.x += 32768), 112 | this.pos.x > 16384 && (this.pos.x -= 32768), 113 | this.pos.y < -8192 && (this.pos.y += 16384), 114 | this.pos.y > 8192 && (this.pos.y -= 16384)); 115 | } 116 | 117 | ```` 118 | --------------------------------------------------------------------------------