├── rustfmt.toml ├── binaries ├── flo-worker │ ├── resource.rc │ ├── Cargo.toml │ └── build.rs ├── flo-worker-ui │ ├── src │ │ ├── gui │ │ │ ├── element │ │ │ │ └── mod.rs │ │ │ └── style.rs │ │ ├── core.rs │ │ ├── log.rs │ │ ├── main.rs │ │ └── flo │ │ │ └── mod.rs │ ├── resources │ │ ├── res.rc │ │ └── flo.ico │ ├── build.rs │ └── Cargo.toml ├── flo-stats-service │ ├── src │ │ └── env.rs │ └── Cargo.toml ├── flo-ping │ └── Cargo.toml ├── flo │ ├── src │ │ └── main.rs │ └── Cargo.toml ├── flo-node-service │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── flo-controller-service │ └── Cargo.toml └── flo-cli │ ├── src │ ├── map.rs │ ├── grpc.rs │ ├── env.rs │ ├── main.rs │ └── replay.rs │ └── Cargo.toml ├── crates ├── controller │ ├── src │ │ ├── host.rs │ │ ├── macros.rs │ │ ├── db.rs │ │ ├── version.rs │ │ ├── migration.rs │ │ ├── player │ │ │ ├── mod.rs │ │ │ ├── session.rs │ │ │ ├── state │ │ │ │ ├── conn.rs │ │ │ │ ├── ping.rs │ │ │ │ └── mod.rs │ │ │ └── token.rs │ │ ├── node │ │ │ ├── mod.rs │ │ │ └── db.rs │ │ ├── game │ │ │ ├── state │ │ │ │ ├── player.rs │ │ │ │ ├── node.rs │ │ │ │ └── slot.rs │ │ │ ├── mod.rs │ │ │ └── token.rs │ │ ├── lib.rs │ │ ├── client │ │ │ ├── handshake.rs │ │ │ └── sender.rs │ │ ├── state │ │ │ ├── actor_map.rs │ │ │ └── mod.rs │ │ └── map │ │ │ ├── db.rs │ │ │ └── mod.rs │ ├── build.rs │ ├── examples │ │ └── node_request.rs │ └── Cargo.toml ├── codegen │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── event │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── debug │ ├── src │ │ ├── lib.rs │ │ └── error.rs │ └── Cargo.toml ├── net │ ├── src │ │ ├── connect │ │ │ ├── mod.rs │ │ │ └── packets.rs │ │ ├── constants.rs │ │ ├── common.rs │ │ ├── version.rs │ │ ├── time.rs │ │ ├── observer │ │ │ └── mod.rs │ │ ├── proto │ │ │ ├── common.proto │ │ │ └── observer.proto │ │ ├── lib.rs │ │ ├── error.rs │ │ ├── node │ │ │ └── mod.rs │ │ └── listener.rs │ ├── build.rs │ └── Cargo.toml ├── w3c │ ├── src │ │ ├── types │ │ │ ├── mod.rs │ │ │ └── w3c.rs │ │ ├── lib.rs │ │ ├── proxy.rs │ │ └── blacklist.rs │ └── Cargo.toml ├── otel │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── client │ ├── src │ │ ├── tcphealth │ │ │ └── mod.rs │ │ ├── version.rs │ │ ├── observer │ │ │ └── source │ │ │ │ ├── mod.rs │ │ │ │ ├── archive_file.rs │ │ │ │ └── memory.rs │ │ ├── node │ │ │ └── mod.rs │ │ ├── message │ │ │ ├── stream.rs │ │ │ └── mod.rs │ │ ├── game │ │ │ └── mod.rs │ │ ├── lib.rs │ │ └── controller │ │ │ └── stream_test.rs │ ├── build.rs │ └── Cargo.toml ├── kinesis │ ├── src │ │ ├── lib.rs │ │ └── error.rs │ └── Cargo.toml ├── task │ ├── src │ │ ├── lib.rs │ │ └── spawn_scope.rs │ └── Cargo.toml ├── node │ ├── src │ │ ├── version.rs │ │ ├── env.rs │ │ ├── state │ │ │ ├── types.rs │ │ │ └── event.rs │ │ ├── echo.rs │ │ ├── game │ │ │ └── host │ │ │ │ └── broadcast.rs │ │ ├── lib.rs │ │ ├── scheduler_fairness.rs │ │ ├── constants.rs │ │ └── error.rs │ ├── build.rs │ └── Cargo.toml ├── types │ ├── src │ │ ├── lib.rs │ │ ├── ping.rs │ │ ├── node.rs │ │ └── health.rs │ └── Cargo.toml ├── w3gs │ ├── build.rs │ ├── src │ │ ├── lib.rs │ │ ├── protocol │ │ │ ├── mod.rs │ │ │ ├── desync.rs │ │ │ ├── w3gs.proto │ │ │ ├── lag.rs │ │ │ ├── leave.rs │ │ │ ├── ping.rs │ │ │ └── map.rs │ │ ├── error.rs │ │ └── net │ │ │ └── codec.rs │ └── Cargo.toml ├── observer-edge │ ├── src │ │ ├── version.rs │ │ ├── constants.rs │ │ ├── services.rs │ │ ├── error.rs │ │ ├── broadcast.rs │ │ ├── env.rs │ │ └── controller.rs │ ├── build.rs │ └── Cargo.toml ├── w3map │ ├── src │ │ ├── minimap │ │ │ └── icons │ │ │ │ ├── cross.png │ │ │ │ ├── cross.rgba │ │ │ │ ├── gold.png │ │ │ │ ├── gold.rgba │ │ │ │ ├── neutral.png │ │ │ │ └── neutral.rgba │ │ ├── constants.rs │ │ └── error.rs │ └── Cargo.toml ├── log │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── blp │ └── Cargo.toml ├── platform │ ├── src │ │ ├── path │ │ │ ├── mod.rs │ │ │ ├── macos.rs │ │ │ └── windows.rs │ │ ├── common.hpp │ │ ├── error.rs │ │ └── windows_bindings.rs │ ├── build.rs │ └── Cargo.toml ├── lan │ ├── src │ │ ├── wc3.proto │ │ ├── lib.rs │ │ ├── mdns │ │ │ └── mod.rs │ │ └── error.rs │ ├── build.rs │ └── Cargo.toml ├── constants │ ├── Cargo.toml │ └── src │ │ ├── version.rs │ │ └── lib.rs ├── state │ ├── src │ │ ├── error.rs │ │ └── reply.rs │ └── Cargo.toml ├── observer │ ├── src │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── kinesis.rs │ │ └── token.rs │ └── Cargo.toml ├── log-subscriber │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── w3replay │ ├── Cargo.toml │ └── src │ │ ├── constants.rs │ │ └── error.rs ├── config │ ├── Cargo.toml │ └── src │ │ └── error.rs ├── util │ ├── Cargo.toml │ └── src │ │ ├── uptime.rs │ │ ├── lib.rs │ │ ├── dword_string.rs │ │ └── error.rs ├── observer-archiver │ ├── Cargo.toml │ └── src │ │ └── error.rs ├── replay │ ├── Cargo.toml │ └── src │ │ └── error.rs ├── w3storage │ ├── Cargo.toml │ └── src │ │ └── error.rs └── observer-fs │ ├── src │ └── error.rs │ └── Cargo.toml ├── .dockerignore ├── .tokeignore ├── migrations ├── 2021-01-12-042955_ban │ ├── down.sql │ └── up.sql ├── 2020-12-24-225738_mute │ ├── down.sql │ └── up.sql ├── 2020-07-27-060705_map_checksum │ ├── down.sql │ └── up.sql ├── 2020-08-21-020955_game_used_slot │ ├── down.sql │ └── up.sql ├── 2024-07-15-234850_game-12p │ ├── down.sql │ └── up.sql ├── 2020-11-22-055448_game_locked │ ├── down.sql │ └── up.sql ├── 2020-08-15-121500_node_disabled │ ├── down.sql │ └── up.sql ├── 2022-10-28-005332_index_slot_status │ ├── down.sql │ └── up.sql ├── 2025-05-23-235104_add_ban_author │ ├── down.sql │ └── up.sql ├── 2020-08-08-101837_node_country_id │ ├── down.sql │ └── up.sql ├── 2020-08-23-104611_game_random_seed │ ├── down.sql │ └── up.sql ├── 2025-05-08-015019_player_ban_indexes │ ├── down.sql │ └── up.sql ├── 2020-11-22-051912_api_player_index │ ├── down.sql │ └── up.sql ├── 2025-06-06-032524_add_internal_ip_to_node │ ├── down.sql │ └── up.sql ├── 2025-07-16-104820_add-flotv-password │ ├── down.sql │ └── up.sql ├── 2023-11-01-135902_game-flo-tv-delay │ ├── down.sql │ └── up.sql ├── 2020-09-26-130556_game_created_by_non_nullable │ ├── down.sql │ └── up.sql ├── 2022-10-16-011439_game_enable_ping_equalizer │ ├── down.sql │ └── up.sql ├── 2020-07-11-023655_initial │ ├── down.sql │ └── up.sql ├── 2025-05-08-015729_node_indexes │ ├── down.sql │ └── up.sql ├── 2021-05-06-154721_game_mask_player_names_version │ ├── down.sql │ └── up.sql ├── 2020-08-22-170841_game_used_slot_client_status_node_conn_id │ ├── down.sql │ └── up.sql ├── 2020-12-09-023437_game_updated_at_trigger │ ├── down.sql │ └── up.sql ├── 2020-11-22-105901_player_api_client_id │ ├── down.sql │ └── up.sql ├── 2025-05-24-014753_multiple_global_mute_bans │ ├── up.sql │ └── down.sql ├── 2025-07-12-205202_add-node-cleanup-indizies │ ├── down.sql │ └── up.sql ├── 2020-08-22-174404_game_node_id │ ├── down.sql │ └── up.sql └── 00000000000000_diesel_initial_setup │ ├── down.sql │ └── up.sql ├── .cargo └── config ├── deps └── bonjour-sdk-windows │ └── Lib │ └── x64 │ └── dnssd.lib ├── diesel.toml ├── .gitmodules ├── .gitignore ├── local-init ├── db-initializer.Dockerfile └── init-db.sh ├── LICENSE ├── .github └── workflows │ └── release-docker.yml ├── azure-pipelines.yml └── Cargo.toml /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 -------------------------------------------------------------------------------- /binaries/flo-worker/resource.rc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/controller/src/host.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /crates/controller/src/macros.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | .env 3 | .github -------------------------------------------------------------------------------- /crates/codegen/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.tokeignore: -------------------------------------------------------------------------------- 1 | deps/wc3-samples 2 | deps/bonjour-sdk-windows -------------------------------------------------------------------------------- /crates/event/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod event; 2 | pub use event::*; 3 | -------------------------------------------------------------------------------- /migrations/2021-01-12-042955_ban/down.sql: -------------------------------------------------------------------------------- 1 | drop table player_ban; -------------------------------------------------------------------------------- /migrations/2020-12-24-225738_mute/down.sql: -------------------------------------------------------------------------------- 1 | drop table player_mute; -------------------------------------------------------------------------------- /crates/debug/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod player_emulator; 3 | -------------------------------------------------------------------------------- /crates/net/src/connect/mod.rs: -------------------------------------------------------------------------------- 1 | mod packets; 2 | pub use packets::*; 3 | -------------------------------------------------------------------------------- /crates/w3c/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(non_snake_case)] 2 | pub mod w3c; 3 | -------------------------------------------------------------------------------- /migrations/2020-07-27-060705_map_checksum/down.sql: -------------------------------------------------------------------------------- 1 | drop table map_checksum; -------------------------------------------------------------------------------- /migrations/2020-08-21-020955_game_used_slot/down.sql: -------------------------------------------------------------------------------- 1 | drop table game_used_slot; -------------------------------------------------------------------------------- /crates/otel/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod builder; 2 | pub mod grpc; 3 | pub mod otel; 4 | -------------------------------------------------------------------------------- /binaries/flo-worker-ui/src/gui/element/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod menu; 2 | pub mod settings; 3 | -------------------------------------------------------------------------------- /crates/client/src/tcphealth/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod tcp_health; 2 | pub mod tcp_health_actor; 3 | -------------------------------------------------------------------------------- /crates/client/src/version.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/flo_version.rs")); 2 | -------------------------------------------------------------------------------- /crates/kinesis/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod data_stream; 2 | pub mod error; 3 | pub mod iterator; -------------------------------------------------------------------------------- /migrations/2024-07-15-234850_game-12p/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE game DROP COLUMN map_twelve_p; -------------------------------------------------------------------------------- /migrations/2020-11-22-055448_game_locked/down.sql: -------------------------------------------------------------------------------- 1 | alter table game 2 | drop column locked; -------------------------------------------------------------------------------- /migrations/2020-08-15-121500_node_disabled/down.sql: -------------------------------------------------------------------------------- 1 | alter table node 2 | drop column disabled; -------------------------------------------------------------------------------- /migrations/2022-10-28-005332_index_slot_status/down.sql: -------------------------------------------------------------------------------- 1 | drop index game_used_slot_client_status; -------------------------------------------------------------------------------- /migrations/2025-05-23-235104_add_ban_author/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE player_ban 2 | DROP COLUMN author; -------------------------------------------------------------------------------- /binaries/flo-worker-ui/resources/res.rc: -------------------------------------------------------------------------------- 1 | #define IDI_ICON 0x101 2 | 3 | IDI_ICON ICON "flo.ico" 4 | -------------------------------------------------------------------------------- /crates/task/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod spawn_scope; 2 | pub use spawn_scope::{SpawnScope, SpawnScopeHandle}; 3 | -------------------------------------------------------------------------------- /migrations/2020-08-08-101837_node_country_id/down.sql: -------------------------------------------------------------------------------- 1 | alter table node 2 | drop column country_id; -------------------------------------------------------------------------------- /migrations/2020-08-23-104611_game_random_seed/down.sql: -------------------------------------------------------------------------------- 1 | alter table game 2 | drop column random_seed; -------------------------------------------------------------------------------- /migrations/2025-05-23-235104_add_ban_author/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE player_ban 2 | ADD COLUMN author TEXT; -------------------------------------------------------------------------------- /.cargo/config: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | rustflags = [ 3 | "-Ctarget-feature=+crt-static" 4 | ] -------------------------------------------------------------------------------- /migrations/2025-05-08-015019_player_ban_indexes/down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS idx_player_ban_active; 2 | -------------------------------------------------------------------------------- /migrations/2020-11-22-051912_api_player_index/down.sql: -------------------------------------------------------------------------------- 1 | drop index player_api_realm; 2 | drop index player_source; -------------------------------------------------------------------------------- /migrations/2025-06-06-032524_add_internal_ip_to_node/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE node DROP COLUMN internal_address; 2 | -------------------------------------------------------------------------------- /migrations/2025-07-16-104820_add-flotv-password/down.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE game DROP COLUMN flo_tv_password_sha256; 2 | -------------------------------------------------------------------------------- /crates/node/src/version.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | include!(concat!(env!("OUT_DIR"), "/flo_node_version.rs")); 4 | -------------------------------------------------------------------------------- /crates/types/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod game; 2 | pub mod health; 3 | pub mod node; 4 | pub mod ping; 5 | pub mod observer; -------------------------------------------------------------------------------- /migrations/2024-07-15-234850_game-12p/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE game ADD COLUMN map_twelve_p boolean NOT NULL DEFAULT false; -------------------------------------------------------------------------------- /migrations/2025-06-06-032524_add_internal_ip_to_node/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE node ADD COLUMN internal_address TEXT; 2 | -------------------------------------------------------------------------------- /migrations/2020-11-22-055448_game_locked/up.sql: -------------------------------------------------------------------------------- 1 | alter table game 2 | add column locked boolean default false not null; -------------------------------------------------------------------------------- /migrations/2023-11-01-135902_game-flo-tv-delay/down.sql: -------------------------------------------------------------------------------- 1 | alter table "game" 2 | drop column flo_tv_delay_override_secs; -------------------------------------------------------------------------------- /migrations/2025-07-16-104820_add-flotv-password/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE game ADD COLUMN flo_tv_password_sha256 TEXT NULL; 2 | -------------------------------------------------------------------------------- /crates/controller/src/db.rs: -------------------------------------------------------------------------------- 1 | pub use bs_diesel_utils::{lock::transaction_with_advisory_lock, DbConn, Executor, ExecutorRef}; 2 | -------------------------------------------------------------------------------- /crates/controller/src/version.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | include!(concat!(env!("OUT_DIR"), "/flo_lobby_version.rs")); 4 | -------------------------------------------------------------------------------- /crates/w3gs/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | prost_build::compile_protos(&["src/protocol/w3gs.proto"], &["src/"]).unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /migrations/2020-08-08-101837_node_country_id/up.sql: -------------------------------------------------------------------------------- 1 | alter table node 2 | add column country_id text not null default 'US'; -------------------------------------------------------------------------------- /migrations/2020-08-15-121500_node_disabled/up.sql: -------------------------------------------------------------------------------- 1 | alter table node 2 | add column disabled boolean default FALSE not null; -------------------------------------------------------------------------------- /migrations/2020-08-23-104611_game_random_seed/up.sql: -------------------------------------------------------------------------------- 1 | alter table game 2 | add column random_seed integer default 0 not null; -------------------------------------------------------------------------------- /migrations/2020-09-26-130556_game_created_by_non_nullable/down.sql: -------------------------------------------------------------------------------- 1 | alter table game 2 | alter column created_by drop not null; -------------------------------------------------------------------------------- /migrations/2020-09-26-130556_game_created_by_non_nullable/up.sql: -------------------------------------------------------------------------------- 1 | alter table game 2 | alter column created_by set not null; -------------------------------------------------------------------------------- /migrations/2022-10-16-011439_game_enable_ping_equalizer/down.sql: -------------------------------------------------------------------------------- 1 | alter table "game" 2 | drop column enable_ping_equalizer; -------------------------------------------------------------------------------- /migrations/2023-11-01-135902_game-flo-tv-delay/up.sql: -------------------------------------------------------------------------------- 1 | alter table "game" 2 | add column flo_tv_delay_override_secs integer; -------------------------------------------------------------------------------- /binaries/flo-worker-ui/resources/flo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3champions/flo/HEAD/binaries/flo-worker-ui/resources/flo.ico -------------------------------------------------------------------------------- /crates/observer-edge/src/version.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | include!(concat!(env!("OUT_DIR"), "/flo_observer_version.rs")); 4 | -------------------------------------------------------------------------------- /crates/w3gs/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | pub mod net; 3 | pub mod protocol; 4 | 5 | pub use protocol::*; 6 | pub mod actions; 7 | -------------------------------------------------------------------------------- /crates/w3map/src/minimap/icons/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3champions/flo/HEAD/crates/w3map/src/minimap/icons/cross.png -------------------------------------------------------------------------------- /crates/w3map/src/minimap/icons/cross.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3champions/flo/HEAD/crates/w3map/src/minimap/icons/cross.rgba -------------------------------------------------------------------------------- /crates/w3map/src/minimap/icons/gold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3champions/flo/HEAD/crates/w3map/src/minimap/icons/gold.png -------------------------------------------------------------------------------- /crates/w3map/src/minimap/icons/gold.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3champions/flo/HEAD/crates/w3map/src/minimap/icons/gold.rgba -------------------------------------------------------------------------------- /migrations/2020-07-11-023655_initial/down.sql: -------------------------------------------------------------------------------- 1 | drop table api_client; 2 | drop table node; 3 | drop table game; 4 | drop table player; -------------------------------------------------------------------------------- /crates/w3map/src/minimap/icons/neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3champions/flo/HEAD/crates/w3map/src/minimap/icons/neutral.png -------------------------------------------------------------------------------- /crates/w3map/src/minimap/icons/neutral.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3champions/flo/HEAD/crates/w3map/src/minimap/icons/neutral.rgba -------------------------------------------------------------------------------- /deps/bonjour-sdk-windows/Lib/x64/dnssd.lib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/w3champions/flo/HEAD/deps/bonjour-sdk-windows/Lib/x64/dnssd.lib -------------------------------------------------------------------------------- /migrations/2025-05-08-015729_node_indexes/down.sql: -------------------------------------------------------------------------------- 1 | DROP INDEX IF EXISTS idx_node_active; 2 | DROP INDEX IF EXISTS idx_node_location_name; 3 | -------------------------------------------------------------------------------- /migrations/2022-10-16-011439_game_enable_ping_equalizer/up.sql: -------------------------------------------------------------------------------- 1 | alter table "game" 2 | add column enable_ping_equalizer boolean default false not null; -------------------------------------------------------------------------------- /migrations/2022-10-28-005332_index_slot_status/up.sql: -------------------------------------------------------------------------------- 1 | create index if not exists game_used_slot_client_status 2 | on game_used_slot (client_status); -------------------------------------------------------------------------------- /migrations/2021-05-06-154721_game_mask_player_names_version/down.sql: -------------------------------------------------------------------------------- 1 | alter table game 2 | drop column mask_player_names, 3 | drop column game_version; -------------------------------------------------------------------------------- /migrations/2020-08-22-170841_game_used_slot_client_status_node_conn_id/down.sql: -------------------------------------------------------------------------------- 1 | alter table game_used_slot 2 | drop column client_status_synced_node_conn_id; -------------------------------------------------------------------------------- /binaries/flo-worker-ui/build.rs: -------------------------------------------------------------------------------- 1 | extern crate embed_resource; 2 | 3 | fn main() { 4 | #[cfg(windows)] 5 | embed_resource::compile("resources/res.rc"); 6 | } 7 | -------------------------------------------------------------------------------- /migrations/2020-08-22-170841_game_used_slot_client_status_node_conn_id/up.sql: -------------------------------------------------------------------------------- 1 | alter table game_used_slot 2 | add column client_status_synced_node_conn_id bigint; -------------------------------------------------------------------------------- /migrations/2020-11-22-051912_api_player_index/up.sql: -------------------------------------------------------------------------------- 1 | create index player_source on player(source); 2 | create index player_api_realm on player(realm) where source = 2; -------------------------------------------------------------------------------- /crates/net/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const PING_INTERVAL_MS: u32 = 10 * 1000; 2 | pub const KEEP_ALIVE_TIMEOUT_MS: u32 = 30 * 1000; 3 | pub const MAX_PAYLOAD_LEN: usize = 16384; 4 | -------------------------------------------------------------------------------- /migrations/2020-12-09-023437_game_updated_at_trigger/down.sql: -------------------------------------------------------------------------------- 1 | DROP TRIGGER update_game_updated_at_slot ON game_used_slot; 2 | DROP FUNCTION update_game_updated_at_from_slot_proc(); -------------------------------------------------------------------------------- /crates/client/src/observer/source/mod.rs: -------------------------------------------------------------------------------- 1 | mod network; 2 | mod memory; 3 | mod archive_file; 4 | 5 | pub use self::network::NetworkSource; 6 | pub use self::archive_file::ArchiveFileSource; -------------------------------------------------------------------------------- /crates/log/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-log" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | tracing = "0.1" 9 | -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "crates/controller/src/schema.rs" 6 | -------------------------------------------------------------------------------- /migrations/2021-05-06-154721_game_mask_player_names_version/up.sql: -------------------------------------------------------------------------------- 1 | alter table game 2 | add column mask_player_names boolean default false not null, 3 | add column game_version text; -------------------------------------------------------------------------------- /binaries/flo-stats-service/src/env.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use std::env; 3 | 4 | pub static ADMIN_SECRET: Lazy = 5 | Lazy::new(|| env::var("FLO_ADMIN_SECRET").ok().unwrap_or_default()); 6 | -------------------------------------------------------------------------------- /crates/w3c/src/lib.rs: -------------------------------------------------------------------------------- 1 | static STATISTIC_SERVICE: &str = "https://statistic-service.w3champions.com/api"; 2 | 3 | mod types; 4 | mod utils; 5 | pub mod stats; 6 | 7 | #[cfg(feature = "blacklist")] 8 | pub mod blacklist; 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/wc3-samples"] 2 | path = deps/wc3-samples 3 | url = https://github.com/wc3tools/wc3-samples.git 4 | [submodule "deps/flo-grpc"] 5 | path = deps/flo-grpc 6 | url = https://github.com/w3champions/flo-grpc.git 7 | -------------------------------------------------------------------------------- /crates/blp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-blp" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-util = { path = "../util" } 9 | 10 | image = "0.23" 11 | -------------------------------------------------------------------------------- /crates/platform/src/path/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | mod windows; 3 | #[cfg(windows)] 4 | pub use self::windows::*; 5 | 6 | #[cfg(target_os = "macos")] 7 | mod macos; 8 | #[cfg(target_os = "macos")] 9 | pub use self::macos::*; 10 | -------------------------------------------------------------------------------- /crates/controller/src/migration.rs: -------------------------------------------------------------------------------- 1 | use crate::db::DbConn; 2 | use crate::error::*; 3 | 4 | embed_migrations!("../../migrations"); 5 | 6 | pub fn run(conn: &DbConn) -> Result<()> { 7 | embedded_migrations::run(conn)?; 8 | Ok(()) 9 | } 10 | -------------------------------------------------------------------------------- /migrations/2020-11-22-105901_player_api_client_id/down.sql: -------------------------------------------------------------------------------- 1 | alter table player 2 | drop constraint player_api_client_source_source_id_key, 3 | drop column api_client_id, 4 | add constraint player_source_source_id_key unique (source, source_id); -------------------------------------------------------------------------------- /migrations/2025-05-24-014753_multiple_global_mute_bans/up.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE player_ban DROP CONSTRAINT player_ban_player_id_ban_type_key; 2 | 3 | ALTER TABLE player_ban ADD CONSTRAINT player_ban_unique_ban 4 | UNIQUE (player_id, ban_type, ban_expires_at); -------------------------------------------------------------------------------- /migrations/2020-07-27-060705_map_checksum/up.sql: -------------------------------------------------------------------------------- 1 | create table map_checksum ( 2 | id serial not null primary key, 3 | sha1 text not null, 4 | checksum bytea not null, 5 | unique(sha1) 6 | ); 7 | 8 | create index map_checksum_sha1 on map_checksum(sha1); -------------------------------------------------------------------------------- /crates/controller/src/player/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db; 2 | pub mod session; 3 | pub(crate) mod state; 4 | pub mod token; 5 | mod types; 6 | 7 | pub mod message { 8 | pub use super::state::ping::{GetPlayersPingSnapshot, UpdatePing}; 9 | } 10 | 11 | pub use types::*; 12 | -------------------------------------------------------------------------------- /crates/event/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-event" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | tokio = { workspace = true, features = ["sync"] } 9 | tracing = "0.1" 10 | futures = "0.3.24" 11 | -------------------------------------------------------------------------------- /crates/observer-edge/src/constants.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | 3 | pub static FLO_STATS_MAX_IN_MEMORY_GAMES: Lazy = Lazy::new(|| { 4 | std::env::var("FLO_STATS_MAX_IN_MEMORY_GAMES") 5 | .ok() 6 | .and_then(|v| v.parse().ok()) 7 | .unwrap_or(100) 8 | }); -------------------------------------------------------------------------------- /crates/client/src/node/mod.rs: -------------------------------------------------------------------------------- 1 | mod registry; 2 | pub mod stream; 3 | pub use registry::{ 4 | AddNode, ClearNodeAddrOverrides, GetNode, GetNodePingMap, NodeInfo, NodeRegistry, RemoveNode, 5 | SetActiveNode, SetNodeAddrOverrides, UpdateAddressesAndGetNodePingMap, UpdateNodes, 6 | }; 7 | -------------------------------------------------------------------------------- /migrations/2025-07-12-205202_add-node-cleanup-indizies/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | 3 | -- Drop indexes in reverse order of creation 4 | DROP INDEX IF EXISTS idx_game_used_slot_player_client_updated; 5 | DROP INDEX IF EXISTS idx_game_status_updated_at; 6 | -------------------------------------------------------------------------------- /crates/lan/src/wc3.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package wc3; 4 | 5 | message GameInfoEntry { 6 | string key = 1; 7 | string value = 2; 8 | } 9 | 10 | message GameInfo { 11 | string name = 1; 12 | int32 message_id = 2; 13 | repeated GameInfoEntry entries = 3; 14 | } -------------------------------------------------------------------------------- /crates/constants/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-constants" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /crates/net/src/common.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | 3 | use crate::proto::flo_common::Version; 4 | 5 | impl Display for Version { 6 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 7 | write!(f, "{}.{}.{}", self.major, self.minor, self.patch) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/state/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug, PartialEq)] 4 | pub enum Error { 5 | #[error("worker gone")] 6 | WorkerGone, 7 | #[error("send timeout")] 8 | SendTimeout, 9 | } 10 | 11 | pub type Result = std::result::Result; 12 | -------------------------------------------------------------------------------- /migrations/2020-08-22-174404_game_node_id/down.sql: -------------------------------------------------------------------------------- 1 | update game set node_id = null; 2 | drop index game_node_id; 3 | alter table game 4 | drop constraint game_node_id_fkey; 5 | alter table game 6 | rename column node_id to node; 7 | alter table game 8 | alter column node type jsonb using null; -------------------------------------------------------------------------------- /migrations/2025-05-24-014753_multiple_global_mute_bans/down.sql: -------------------------------------------------------------------------------- 1 | -- Revert unique constraint back to (player_id, ban_type) 2 | 3 | ALTER TABLE player_ban DROP CONSTRAINT player_ban_unique_ban; 4 | 5 | ALTER TABLE player_ban ADD CONSTRAINT player_ban_player_id_ban_type_key 6 | UNIQUE (player_id, ban_type); 7 | -------------------------------------------------------------------------------- /crates/log/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! result_ok { 3 | ($prefix:literal, $result:expr) => { 4 | match $result { 5 | Ok(value) => Some(value), 6 | Err(err) => { 7 | tracing::error!("{}: {}", $prefix, err); 8 | None 9 | } 10 | } 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /crates/platform/src/common.hpp: -------------------------------------------------------------------------------- 1 | #define WIN32_LEAN_AND_MEAN 2 | #include 3 | 4 | class AutoCloseHandle { 5 | public: 6 | AutoCloseHandle(const HANDLE handle): handle(handle) {} 7 | ~AutoCloseHandle() { 8 | CloseHandle(handle); 9 | } 10 | private: 11 | HANDLE handle; 12 | }; -------------------------------------------------------------------------------- /crates/platform/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | fn main() { 3 | cc::Build::new() 4 | .file("src/windows_bindings.cpp") 5 | .compile("windows_bindings"); 6 | println!("cargo:rustc-link-lib=Version"); 7 | println!("cargo:rustc-link-lib=User32"); 8 | } 9 | 10 | #[cfg(not(windows))] 11 | fn main() {} 12 | -------------------------------------------------------------------------------- /binaries/flo-ping/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-ping" 3 | version = "0.1.0" 4 | edition = "2018" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1" 10 | rand = "0.8.5" 11 | clap = { version = "4.0.18", features = ["derive"] } 12 | -------------------------------------------------------------------------------- /crates/codegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-codegen" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | proc-macro2 = "1" 12 | quote = "1" 13 | syn = { version = "1", features = ["parsing", "visit-mut"] } 14 | darling = "0.11" 15 | -------------------------------------------------------------------------------- /crates/lan/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod game_info; 2 | mod mdns; 3 | mod proto { 4 | include!(concat!(env!("OUT_DIR"), "/wc3.rs")); 5 | } 6 | 7 | pub mod error; 8 | 9 | pub use self::game_info::GameInfo; 10 | pub use self::mdns::publisher::{MdnsEvent, MdnsEventSender, MdnsPublisher}; 11 | pub use self::mdns::search::{search_lan_games, LanGame}; 12 | -------------------------------------------------------------------------------- /crates/net/src/version.rs: -------------------------------------------------------------------------------- 1 | impl From for crate::proto::flo_common::Version { 2 | fn from(v: flo_constants::version::Version) -> Self { 3 | crate::proto::flo_common::Version { 4 | major: v.major as i32, 5 | minor: v.minor as i32, 6 | patch: v.patch as i32, 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /crates/task/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-task" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | tokio = { workspace = true, features = ["sync", "macros"] } 11 | -------------------------------------------------------------------------------- /migrations/2025-05-08-015729_node_indexes/up.sql: -------------------------------------------------------------------------------- 1 | -- Composite index for ordering by location and name (get_all_nodes uses this) 2 | CREATE INDEX idx_node_location_name ON node(location, name); 3 | 4 | -- Partial index for active nodes (get_all_nodes uses this) 5 | CREATE INDEX idx_node_active ON node(location, name) 6 | WHERE disabled = false; 7 | -------------------------------------------------------------------------------- /crates/observer/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("observer token expired")] 6 | ObserverTokenExpired, 7 | #[error("json web token: {0}")] 8 | JsonWebToken(#[from] jsonwebtoken::errors::Error), 9 | } 10 | 11 | pub type Result = std::result::Result; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | samples 3 | flo.toml 4 | .env* 5 | *.iml 6 | .idea/modules.xml 7 | .idea/workspace.xml 8 | .idea/usage.statistics.xml 9 | .idea/tasks.xml 10 | /build/release 11 | /logs 12 | /flo-logs 13 | /data 14 | /crates/observer-consumer/data 15 | /crates/observer-fs/data 16 | 17 | # Config files 18 | *.json 19 | 20 | /.idea 21 | .DS_Store 22 | /.vs 23 | -------------------------------------------------------------------------------- /crates/w3gs/src/protocol/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod action; 2 | pub mod chat; 3 | pub mod constants; 4 | pub mod desync; 5 | pub mod game; 6 | pub mod join; 7 | pub mod lag; 8 | pub mod leave; 9 | pub mod map; 10 | pub mod packet; 11 | pub mod ping; 12 | pub mod player; 13 | pub mod slot; 14 | 15 | mod protobuf { 16 | include!(concat!(env!("OUT_DIR"), "/w3gs.rs")); 17 | } 18 | -------------------------------------------------------------------------------- /crates/controller/src/node/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db; 2 | mod state; 3 | mod types; 4 | 5 | pub use state::conn::NodeConnActor; 6 | pub use state::request::PlayerLeaveResponse; 7 | pub use state::NodeRegistry; 8 | pub use types::*; 9 | pub mod messages { 10 | pub use crate::node::state::conn::{NodeCreateGame, NodePlayerLeave}; 11 | pub use crate::node::state::ListNode; 12 | } 13 | -------------------------------------------------------------------------------- /crates/log-subscriber/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-log-subscriber" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | tracing = "0.1" 11 | tracing-futures = "0.2" 12 | tracing-subscriber = "0.3.18" 13 | -------------------------------------------------------------------------------- /crates/net/src/time.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct StopWatch { 5 | start: Instant, 6 | } 7 | 8 | impl StopWatch { 9 | pub fn new() -> Self { 10 | StopWatch { 11 | start: Instant::now(), 12 | } 13 | } 14 | 15 | pub fn elapsed_ms(&self) -> u32 { 16 | self.start.elapsed().as_millis() as u32 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /crates/node/src/env.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use std::env; 3 | 4 | #[derive(Debug)] 5 | pub struct Env { 6 | pub secret_key: String, 7 | } 8 | 9 | impl Env { 10 | pub fn get() -> &'static Env { 11 | static INSTANCE: Lazy = Lazy::new(|| Env { 12 | secret_key: env::var("FLO_NODE_SECRET").unwrap_or_default(), 13 | }); 14 | &INSTANCE 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /binaries/flo/src/main.rs: -------------------------------------------------------------------------------- 1 | #[tokio::main] 2 | async fn main() { 3 | flo_log_subscriber::init_env_override("debug"); 4 | 5 | let task = flo_client::start_ws(Default::default()).await.unwrap(); 6 | let join = tokio::spawn(task.serve()); 7 | let ctrl_c = tokio::signal::ctrl_c(); 8 | 9 | tokio::select! { 10 | res = join => res.unwrap(), 11 | _ = ctrl_c => {}, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/observer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | mod kinesis; 3 | pub mod record; 4 | pub mod token; 5 | 6 | use once_cell::sync::Lazy; 7 | 8 | pub use kinesis::KINESIS_CLIENT; 9 | pub static KINESIS_STREAM_NAME: Lazy = Lazy::new(|| { 10 | std::env::var("AWS_KINESIS_STREAM_NAME") 11 | .ok() 12 | .and_then(|v| v.parse().ok()) 13 | .unwrap_or("flo".to_string()) 14 | }); 15 | -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /crates/client/src/message/stream.rs: -------------------------------------------------------------------------------- 1 | use super::messages::{IncomingMessage, OutgoingMessage}; 2 | use crate::error::Result; 3 | use flo_state::async_trait; 4 | 5 | #[async_trait] 6 | pub trait MessageStream: Send + Sync + 'static { 7 | async fn send(&mut self, msg: OutgoingMessage) -> Result<()>; 8 | async fn recv(&mut self) -> Option; 9 | async fn flush(&mut self); 10 | } 11 | -------------------------------------------------------------------------------- /crates/w3replay/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-w3replay" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-util = { path = "../util" } 9 | flo-w3gs = { path = "../w3gs" } 10 | 11 | flate2 = { version = "1.0", features = ["zlib"], default-features = false } 12 | thiserror = "1" 13 | bitflags = "1" 14 | bytes = "1.2.1" 15 | -------------------------------------------------------------------------------- /migrations/2020-11-22-105901_player_api_client_id/up.sql: -------------------------------------------------------------------------------- 1 | alter table player 2 | drop constraint player_source_source_id_key, 3 | add column api_client_id integer references api_client(id), 4 | add constraint player_api_client_source_source_id_key unique (api_client_id, source, source_id); 5 | update player set api_client_id = 1; 6 | alter table player 7 | alter column api_client_id set not null; -------------------------------------------------------------------------------- /crates/net/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let mut prost_build = prost_build::Config::new(); 3 | prost_build.type_attribute(".", "#[derive(Serialize, Deserialize)]"); 4 | prost_build 5 | .compile_protos( 6 | &[ 7 | "src/proto/connect.proto", 8 | "src/proto/node.proto", 9 | "src/proto/observer.proto", 10 | ], 11 | &["src"], 12 | ) 13 | .unwrap(); 14 | } 15 | -------------------------------------------------------------------------------- /crates/net/src/observer/mod.rs: -------------------------------------------------------------------------------- 1 | pub use crate::proto::flo_observer::*; 2 | 3 | packet_type!(ObserverConnect, PacketObserverConnect); 4 | packet_type!(ObserverConnectAccept, PacketObserverConnectAccept); 5 | packet_type!(ObserverConnectReject, PacketObserverConnectReject); 6 | packet_type!(ObserverPasswordRequest, PacketObserverPasswordRequest); 7 | packet_type!(ObserverPasswordResponse, PacketObserverPasswordResponse); -------------------------------------------------------------------------------- /migrations/2020-08-22-174404_game_node_id/up.sql: -------------------------------------------------------------------------------- 1 | update game set node = null; 2 | alter table game 3 | rename column node to node_id; 4 | alter table game 5 | alter column node_id type integer using null; 6 | alter table game 7 | add constraint game_node_id_fkey 8 | foreign key (node_id) 9 | references node(id); 10 | create index game_node_id on game(node_id) where node_id is not null; 11 | -------------------------------------------------------------------------------- /crates/w3c/src/proxy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crate::types::w3c::Proxy; 3 | 4 | pub async fn get_proxies() -> Result> { 5 | let url = format!("{}/flo/proxies", crate::MATCHMAKING_SERVICE); 6 | let res: Vec = reqwest::get(&url).await?.error_for_status()?.json().await?; 7 | Ok(res) 8 | } 9 | 10 | #[tokio::test] 11 | async fn test_get_proxies() { 12 | get_proxies().await.unwrap(); 13 | } -------------------------------------------------------------------------------- /crates/config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-config" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | flo-constants = { path = "../constants" } 11 | 12 | serde = { version = "1.0", features = ["derive"] } 13 | toml = "0.5" 14 | thiserror = "1" 15 | -------------------------------------------------------------------------------- /crates/util/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-util" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-codegen = { path = "../codegen" } 9 | 10 | thiserror = "1" 11 | tokio-util = { workspace = true, features = ["codec"] } 12 | bytes = "1.2.1" 13 | pretty-hex = "0.2" 14 | enumflags2 = "0.6" 15 | lazy_static = "1" 16 | impl-trait-for-tuples = "0.2" 17 | -------------------------------------------------------------------------------- /migrations/2021-01-12-042955_ban/up.sql: -------------------------------------------------------------------------------- 1 | create table player_ban ( 2 | id serial not null primary key, 3 | player_id integer not null references player(id), 4 | ban_type integer not null, 5 | ban_expires_at timestamp with time zone, 6 | created_at timestamp with time zone default now() not null, 7 | unique(player_id, ban_type) 8 | ); 9 | 10 | create index player_ban_player_id on player_ban(player_id); -------------------------------------------------------------------------------- /crates/controller/src/player/session.rs: -------------------------------------------------------------------------------- 1 | use flo_net::proto::flo_connect::{PacketPlayerSessionUpdate, PlayerStatus}; 2 | 3 | pub(super) fn get_session_update_packet(game_id: Option) -> PacketPlayerSessionUpdate { 4 | PacketPlayerSessionUpdate { 5 | status: if game_id.is_some() { 6 | PlayerStatus::InGame.into() 7 | } else { 8 | PlayerStatus::Idle.into() 9 | }, 10 | game_id, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /crates/observer-archiver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-observer-archiver" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | rusoto_s3 = "0.47.0" 8 | rusoto_core = "0.47.0" 9 | tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread"] } 10 | backoff = { version = "0.4" } 11 | bytes = "1.2.1" 12 | md5 = "0.7.0" 13 | tracing = "0.1" 14 | futures = "0.3.24" 15 | thiserror = "1.0" 16 | -------------------------------------------------------------------------------- /crates/observer-edge/src/services.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::Controller; 2 | use flo_observer_archiver::ArchiverHandle; 3 | 4 | #[derive(Clone)] 5 | pub struct Services { 6 | pub controller: Controller, 7 | pub archiver: Option, 8 | } 9 | 10 | impl Services { 11 | pub fn from_env() -> Self { 12 | Self { 13 | controller: Controller::from_env(), 14 | archiver: None, 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/replay/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-replay" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | flo-net = { path = "../net" } 8 | flo-w3gs = { path = "../w3gs" } 9 | flo-types = { path = "../types" } 10 | flo-w3replay = { path = "../w3replay" } 11 | flo-observer = { path = "../observer" } 12 | flo-observer-fs = { path = "../observer-fs" } 13 | bytes = "1.2.1" 14 | thiserror = "1.0" 15 | tracing = "0.1" 16 | -------------------------------------------------------------------------------- /crates/w3storage/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-w3storage" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-platform = { path = "../platform" } 9 | casclib = "0.2" 10 | thiserror = "1.0" 11 | parking_lot = "0.11" 12 | glob = "0.3" 13 | bytes = "1.2.1" 14 | serde = { version = "1", features = ["derive"] } 15 | walkdir = "2" 16 | 17 | [dev-dependencies] 18 | dotenv = "0.15" 19 | -------------------------------------------------------------------------------- /crates/lan/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | prost_build::compile_protos(&["src/wc3.proto"], &["src/"]).unwrap(); 3 | #[cfg(target_os = "windows")] 4 | { 5 | let dir = 6 | std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").expect("env CARGO_MANIFEST_DIR")) 7 | .join("../../deps/bonjour-sdk-windows"); 8 | println!( 9 | "cargo:rustc-link-search=native={}", 10 | dir.join("Lib/x64").display() 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /crates/w3c/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-w3c" 3 | version = "0.0.1" 4 | authors = ["Miezhiko "] 5 | edition = "2018" 6 | 7 | [features] 8 | default = [] 9 | blacklist = ["sled"] 10 | 11 | [dependencies] 12 | ureq = { version = "2", features = ["json"] } 13 | serde = { version = "1", features = ["derive"] } 14 | serde_json = "1.0" 15 | anyhow = "1.0" 16 | once_cell = "1.15" 17 | 18 | sled = { version = "0.34", optional = true } 19 | -------------------------------------------------------------------------------- /crates/types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-types" 3 | version = "0.2.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | flo-net = { path = "../net" } 11 | flo-w3gs = { path = "../w3gs" } 12 | flo-grpc = { path = "../../deps/flo-grpc" } 13 | 14 | s2-grpc-utils = "0.2" 15 | serde = { version = "1.0", features = ["derive"] } 16 | -------------------------------------------------------------------------------- /binaries/flo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo" 3 | version = "0.0.3" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-client = { path = "../../crates/client" } 9 | flo-log-subscriber = { path = "../../crates/log-subscriber" } 10 | 11 | dotenv = "0.15" 12 | tokio = { workspace = true, features = ["time", "net", "macros", "sync", "signal", "rt", "rt-multi-thread"] } 13 | tokio-stream = { workspace = true, features = ["time", "net"] } 14 | -------------------------------------------------------------------------------- /migrations/2020-12-24-225738_mute/up.sql: -------------------------------------------------------------------------------- 1 | create table player_mute ( 2 | id serial not null primary key, 3 | player_id integer not null references player(id), 4 | mute_player_id integer not null references player(id), 5 | created_at timestamp with time zone default now() not null, 6 | unique(player_id, mute_player_id) 7 | ); 8 | 9 | create index player_mute_player_id on player_mute(player_id); 10 | create index player_mute_mute_player_id on player_mute(mute_player_id); -------------------------------------------------------------------------------- /crates/observer-archiver/src/error.rs: -------------------------------------------------------------------------------- 1 | use rusoto_core::RusotoError; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum Error { 6 | #[error("io: {0}")] 7 | Io(#[from] std::io::Error), 8 | #[error("get archived object: {0}")] 9 | GetArchivedObject(#[from] RusotoError), 10 | #[error("invalid S3 credentials: {0}")] 11 | InvalidS3Credentials(&'static str), 12 | } 13 | 14 | pub type Result = std::result::Result; 15 | -------------------------------------------------------------------------------- /crates/config/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("parse int: {0}")] 6 | ParseInt(#[from] std::num::ParseIntError), 7 | 8 | #[error("io: {0}")] 9 | Io(#[from] std::io::Error), 10 | 11 | #[error("toml serialize: {0}")] 12 | TomlSer(#[from] toml::ser::Error), 13 | 14 | #[error("toml deserialize: {0}")] 15 | TomlDe(#[from] toml::de::Error), 16 | } 17 | 18 | pub type Result = std::result::Result; 19 | -------------------------------------------------------------------------------- /migrations/2020-12-09-023437_game_updated_at_trigger/up.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION update_game_updated_at_from_slot_proc() 2 | RETURNS TRIGGER AS $$ 3 | BEGIN 4 | UPDATE game SET updated_at = now() WHERE id = NEW."game_id"; 5 | RETURN NEW; 6 | END; 7 | $$ language 'plpgsql'; 8 | 9 | DROP TRIGGER IF EXISTS update_game_updated_at_slot ON game_used_slot; 10 | CREATE TRIGGER update_game_updated_at_slot BEFORE UPDATE ON game_used_slot FOR EACH ROW EXECUTE PROCEDURE update_game_updated_at_from_slot_proc(); -------------------------------------------------------------------------------- /crates/controller/src/game/state/player.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::game::state::GameActor; 3 | 4 | use flo_state::{async_trait, Context, Handler, Message}; 5 | 6 | pub struct GetGamePlayers; 7 | 8 | impl Message for GetGamePlayers { 9 | type Result = Result>; 10 | } 11 | 12 | #[async_trait] 13 | impl Handler for GameActor { 14 | async fn handle(&mut self, _: &mut Context, _: GetGamePlayers) -> Result> { 15 | Ok(self.players.clone()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/types/src/ping.rs: -------------------------------------------------------------------------------- 1 | use s2_grpc_utils::{S2ProtoPack, S2ProtoUnpack}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, S2ProtoPack, S2ProtoUnpack, Serialize, Deserialize, Default)] 5 | #[s2_grpc(message_type(flo_net::proto::flo_connect::PingStats, flo_grpc::player::PingStats))] 6 | pub struct PingStats { 7 | pub min: Option, 8 | pub max: Option, 9 | pub avg: Option, 10 | pub stddev: Option, 11 | pub current: Option, 12 | pub loss_rate: f32, 13 | } 14 | -------------------------------------------------------------------------------- /crates/kinesis/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-kinesis" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | flo-observer = { path = "../observer" } 8 | tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread"] } 9 | tokio-stream = { workspace = true } 10 | rusoto_core = "0.48.0" 11 | rusoto_kinesis = "0.48.0" 12 | thiserror = "1.0" 13 | backoff = "0.3" 14 | tracing = "0.1" 15 | 16 | [dev-dependencies] 17 | dotenv = "0.15" 18 | flo-log-subscriber = { path = "../log-subscriber"} 19 | -------------------------------------------------------------------------------- /crates/w3map/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-w3map" 3 | version = "0.2.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [features] 8 | w3storage = [ 9 | "flo-w3storage" 10 | ] 11 | 12 | [dependencies] 13 | flo-util = { path = "../util" } 14 | flo-blp = { path = "../blp" } 15 | flo-w3storage = { path = "../w3storage", optional = true } 16 | 17 | stormlib = "0.1" 18 | thiserror = "1" 19 | bitflags = "1" 20 | image = "0.23" 21 | lazy_static = "1" 22 | ceres-mpq = "0.1" 23 | crc32fast = "1.3" 24 | sha1 = "0.6" 25 | -------------------------------------------------------------------------------- /crates/w3storage/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("invalid path: {0}")] 6 | InvalidPath(String), 7 | #[error("platform: {0}")] 8 | Platform(#[from] flo_platform::error::Error), 9 | #[error("io: {0}")] 10 | Io(#[from] std::io::Error), 11 | #[error("glob pattern error: {0}")] 12 | GlobPattern(#[from] glob::PatternError), 13 | #[error("casc: {0}")] 14 | Casc(#[from] casclib::CascError), 15 | } 16 | 17 | pub type Result = std::result::Result; 18 | -------------------------------------------------------------------------------- /crates/w3gs/src/protocol/desync.rs: -------------------------------------------------------------------------------- 1 | use flo_util::binary::*; 2 | use flo_util::{BinDecode, BinEncode}; 3 | 4 | use crate::protocol::constants::PacketTypeId; 5 | use crate::protocol::packet::PacketPayload; 6 | 7 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 8 | pub struct Desync { 9 | pub unknown_1: u32, 10 | #[bin(eq = 4)] 11 | pub unknown_2: u8, 12 | pub unknown_3: u32, 13 | #[bin(eq = 0)] 14 | pub unknown_4: u8, 15 | } 16 | 17 | impl PacketPayload for Desync { 18 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::Desync; 19 | } 20 | -------------------------------------------------------------------------------- /crates/replay/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("Game has no player")] 6 | GameHasNoPlayer, 7 | #[error("flo observer slot occupied")] 8 | FloObserverSlotOccupied, 9 | #[error("w3gs: {0}")] 10 | W3GS(#[from] flo_w3gs::error::Error), 11 | #[error("observer fs: {0}")] 12 | ObserverFs(#[from] flo_observer_fs::error::Error), 13 | #[error("w3replay: {0}")] 14 | W3Replay(#[from] flo_w3replay::error::Error), 15 | } 16 | 17 | pub type Result = std::result::Result; 18 | -------------------------------------------------------------------------------- /crates/log-subscriber/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Once; 2 | pub use tracing::{debug, error, info, instrument, span, warn, Level}; 3 | pub use tracing_futures::Instrument; 4 | 5 | static INIT: Once = Once::new(); 6 | 7 | pub fn init() { 8 | INIT.call_once(|| { 9 | #[cfg(debug_assertions)] 10 | tracing_subscriber::fmt::init(); 11 | 12 | #[cfg(not(debug_assertions))] 13 | tracing_subscriber::fmt::fmt().with_ansi(false).init(); 14 | }); 15 | } 16 | 17 | pub fn init_env_override(env: &str) { 18 | std::env::set_var("RUST_LOG", env); 19 | init(); 20 | } 21 | -------------------------------------------------------------------------------- /crates/observer-fs/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("Invalid buffer file")] 6 | InvalidBufferFile, 7 | #[error("Invalid chunk file")] 8 | InvalidChunkFile, 9 | #[error("decode game record: {0}")] 10 | DecodeGameRecord(#[from] flo_observer::record::RecordError), 11 | #[error("decode archive header: {0}")] 12 | DecodeArchiveHeader(flo_util::binary::BinDecodeError), 13 | #[error("io: {0}")] 14 | Io(#[from] std::io::Error), 15 | } 16 | 17 | pub type Result = std::result::Result; 18 | -------------------------------------------------------------------------------- /crates/observer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-observer" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-net = { path = "../net" } 9 | flo-w3gs = { path = "../w3gs" } 10 | flo-util = { path = "../util" } 11 | bytes = "1.2.1" 12 | prost = "0.9" 13 | thiserror = "1.0" 14 | once_cell = "1.15" 15 | rusoto_core = "0.48.0" 16 | rusoto_kinesis = "0.48.0" 17 | tracing = "0.1" 18 | jsonwebtoken = "7.2" 19 | serde = { version = "1.0", features = ["derive"] } 20 | chrono = { version = "0.4", features = ["serde"] } 21 | -------------------------------------------------------------------------------- /migrations/2025-05-08-015019_player_ban_indexes/up.sql: -------------------------------------------------------------------------------- 1 | -- Partial index for active bans (ban_expires_at > now() OR ban_expires_at IS NULL) 2 | -- This specifically targets the filter condition in get_ban_list_map 3 | CREATE INDEX idx_player_ban_active ON player_ban(player_id, ban_type, ban_expires_at) 4 | WHERE ban_expires_at IS NULL; 5 | 6 | -- Additional index for the case where ban_expires_at has a value 7 | -- The query planner can use this for the condition where ban_expires_at > current timestamp 8 | CREATE INDEX idx_player_ban_with_expiry ON player_ban(player_id, ban_type, ban_expires_at); 9 | -------------------------------------------------------------------------------- /binaries/flo-node-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-node-service" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-log-subscriber = { path = "../../crates/log-subscriber" } 9 | flo-node = { path = "../../crates/node" } 10 | 11 | dotenv = "0.15" 12 | tokio = { workspace = true, features = ["time", "sync", "macros", "rt-multi-thread"] } 13 | tokio-stream = { workspace = true, features = ["time"] } 14 | tracing = "0.1" 15 | 16 | [target.'cfg(windows)'.dependencies] 17 | winapi = { version = "0.3", features = ["processthreadsapi", "timeapi"] } 18 | -------------------------------------------------------------------------------- /crates/observer/src/kinesis.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use rusoto_core::{credential::StaticProvider, request::HttpClient}; 3 | use rusoto_kinesis::KinesisClient; 4 | use std::env; 5 | 6 | pub static KINESIS_CLIENT: Lazy = Lazy::new(|| { 7 | let provider = StaticProvider::new( 8 | env::var("AWS_ACCESS_KEY_ID").unwrap(), 9 | env::var("AWS_SECRET_ACCESS_KEY").unwrap(), 10 | None, 11 | None, 12 | ); 13 | let client = HttpClient::new().unwrap(); 14 | let region = env::var("AWS_KINESIS_REGION").unwrap().parse().unwrap(); 15 | KinesisClient::new_with(client, provider, region) 16 | }); -------------------------------------------------------------------------------- /crates/w3gs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-w3gs" 3 | version = "0.2.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-util = { path = "../util" } 9 | 10 | bitflags = "1" 11 | thiserror = "1" 12 | lazy_static = "1" 13 | prost = "0.9" 14 | futures = "0.3.24" 15 | tokio = { workspace = true, features = ["net", "io-util"] } 16 | tokio-stream = { workspace = true, features = ["net"] } 17 | tokio-util = { workspace = true, features = ["codec", "net"] } 18 | socket2 = "0.5" 19 | rand = "0.8" 20 | crc32fast = "1.3" 21 | 22 | [build-dependencies] 23 | prost-build = "0.9" 24 | -------------------------------------------------------------------------------- /crates/controller/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(debug_assertions))] 2 | #[macro_use] 3 | extern crate diesel_migrations; 4 | #[cfg(not(debug_assertions))] 5 | pub mod migration; 6 | 7 | #[macro_use] 8 | mod macros; 9 | mod version; 10 | 11 | #[macro_use] 12 | extern crate diesel; 13 | 14 | mod db; 15 | mod schema; 16 | 17 | mod client; 18 | mod config; 19 | pub mod error; 20 | pub mod game; 21 | mod grpc; 22 | pub mod host; 23 | pub mod map; 24 | pub mod node; 25 | pub mod player; 26 | mod state; 27 | 28 | pub use client::serve as serve_socket; 29 | pub use grpc::serve as serve_grpc; 30 | pub use state::{ControllerState, ControllerStateRef}; 31 | -------------------------------------------------------------------------------- /binaries/flo-controller-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-controller-service" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-log-subscriber = { path = "../../crates/log-subscriber" } 9 | flo-controller = { path = "../../crates/controller" } 10 | flo-otel = { path = "../../crates/otel" } 11 | 12 | tracing = "0.1" 13 | bs-diesel-utils = { git = "https://github.com/BSpaceinc/bs-diesel-utils.git" } 14 | dotenv = "0.15" 15 | tokio = { workspace = true, features = ["time", "sync", "macros", "signal", "rt-multi-thread"] } 16 | tokio-stream = { workspace = true, features = ["time"] } 17 | -------------------------------------------------------------------------------- /binaries/flo-cli/src/map.rs: -------------------------------------------------------------------------------- 1 | use flo_w3map::W3Map; 2 | use std::path::PathBuf; 3 | use structopt::StructOpt; 4 | 5 | use crate::Result; 6 | 7 | #[derive(Debug, StructOpt)] 8 | pub enum Command { 9 | Inspect { path: PathBuf }, 10 | } 11 | 12 | impl Command { 13 | pub async fn run(&self) -> Result<()> { 14 | match *self { 15 | Command::Inspect { ref path } => { 16 | let (map, checksum) = W3Map::open_with_checksum(path)?; 17 | println!("Checksum: {:?}", checksum); 18 | println!("Map Name: {}", map.name()); 19 | println!("Map Players: {:?}", map.get_players().len()); 20 | } 21 | } 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /crates/platform/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-platform" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-config = { path = "../config" } 9 | thiserror = "1" 10 | dotenv = "0.15" 11 | serde = { version = "1", features = ["derive"] } 12 | tracing = "0.1" 13 | 14 | [target.'cfg(windows)'.dependencies] 15 | winapi = { version = "0.3", features = ["processthreadsapi", "shlobj", "knownfolders", "winerror", "combaseapi"] } 16 | widestring = "0.4" 17 | 18 | [target.'cfg(target_os = "macos")'.dependencies] 19 | plist = "1.3" 20 | home-dir = "0.1" 21 | 22 | [build-dependencies] 23 | cc = "1.0" 24 | -------------------------------------------------------------------------------- /local-init/db-initializer.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1-bullseye AS builder 2 | 3 | WORKDIR /usr/local/build 4 | 5 | RUN cargo install diesel_cli --no-default-features --features "postgres" 6 | 7 | FROM debian:bullseye-slim 8 | 9 | RUN apt-get update && apt-get install -y ca-certificates libpq-dev netcat postgresql-client dos2unix && rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /usr/local/flo 12 | 13 | # Copy diesel binary 14 | COPY --from=builder /usr/local/cargo/bin/diesel /usr/local/cargo/bin/diesel 15 | 16 | COPY local-init/init-db.sh /usr/local/flo/init-db.sh 17 | RUN dos2unix /usr/local/flo/init-db.sh 18 | 19 | CMD ["/bin/bash", "-c", "/usr/local/flo/init-db.sh"] -------------------------------------------------------------------------------- /binaries/flo-node-service/src/main.rs: -------------------------------------------------------------------------------- 1 | use flo_node::serve; 2 | 3 | #[tokio::main] 4 | async fn main() -> Result<(), Box> { 5 | #[cfg(windows)] 6 | unsafe { 7 | winapi::um::timeapi::timeBeginPeriod(1); 8 | } 9 | 10 | #[cfg(debug_assertions)] 11 | { 12 | dotenv::dotenv()?; 13 | flo_log_subscriber::init_env_override("flo_node_service=debug,flo_node=debug,flo_net=debug"); 14 | // flo_log_subscriber::init_env_override("flo_node=info"); 15 | } 16 | #[cfg(not(debug_assertions))] 17 | { 18 | flo_log_subscriber::init(); 19 | } 20 | 21 | tracing::info!("starting."); 22 | 23 | serve().await?; 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /crates/controller/src/node/db.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | 3 | use crate::db::DbConn; 4 | use crate::error::*; 5 | use crate::node::types::Node; 6 | use crate::schema::node; 7 | 8 | pub fn get_all_nodes(conn: &DbConn) -> Result> { 9 | use node::dsl; 10 | let nodes = node::table 11 | .filter(dsl::disabled.eq(false)) 12 | .order((dsl::location, dsl::name)) 13 | .load(conn)?; 14 | Ok(nodes) 15 | } 16 | 17 | pub fn get_node(conn: &DbConn, node_id: i32) -> Result { 18 | node::table 19 | .find(node_id) 20 | .first::(conn) 21 | .optional()? 22 | .ok_or_else(|| Error::NodeNotFound) 23 | .map_err(Into::into) 24 | } 25 | -------------------------------------------------------------------------------- /crates/constants/src/version.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug, Clone, Copy, PartialOrd, PartialEq)] 4 | pub struct Version { 5 | pub major: i32, 6 | pub minor: i32, 7 | pub patch: i32, 8 | } 9 | 10 | impl Version { 11 | pub fn parse(v: &'static str) -> Self { 12 | let parts: Vec = v.split('.').map(|v| v.parse::().unwrap()).collect(); 13 | assert_eq!(parts.len(), 3); 14 | Version { 15 | major: parts[0], 16 | minor: parts[1], 17 | patch: parts[2], 18 | } 19 | } 20 | } 21 | 22 | impl fmt::Display for Version { 23 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 24 | write!(f, "{}.{}.{}", self.major, self.minor, self.patch) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /crates/state/Cargo.toml: -------------------------------------------------------------------------------- 1 | # Forked from https://github.com/wc3tools/flo-state/tree/7ac69995b7a4e055f58bb654ce6ac747e5bce4ae 2 | [package] 3 | name = "flo-state" 4 | version = "1.1.1" 5 | authors = ["Flux Xu "] 6 | edition = "2018" 7 | description = "Lightweight actor library." 8 | license = "MIT" 9 | repository = "https://github.com/wc3tools/flo-state.git" 10 | 11 | [dependencies] 12 | tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread", "time"] } 13 | tokio-util = { workspace = true } 14 | futures = "0.3" 15 | thiserror = "1.0" 16 | async-trait = "0.1" 17 | 18 | [dev-dependencies] 19 | tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread", "time"] } 20 | once_cell = "1.4" -------------------------------------------------------------------------------- /crates/node/src/state/types.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | #[derive(Debug, PartialEq, Hash, Eq, Clone)] 4 | pub struct PlayerToken([u8; 16]); 5 | 6 | impl PlayerToken { 7 | pub fn new_uuid() -> Self { 8 | let uuid = Uuid::new_v4(); 9 | Self(*uuid.as_bytes()) 10 | } 11 | 12 | pub fn from_vec(bytes: Vec) -> Option { 13 | if bytes.len() != 16 { 14 | return None; 15 | } 16 | let mut token = PlayerToken([0; 16]); 17 | token.0.copy_from_slice(&bytes[..]); 18 | Some(token) 19 | } 20 | 21 | pub fn to_vec(&self) -> Vec { 22 | self.0.to_vec() 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | pub struct RegisteredPlayer { 28 | pub player_id: i32, 29 | pub game_id: i32, 30 | } 31 | -------------------------------------------------------------------------------- /crates/observer-fs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-observer-fs" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-observer = { path = "../observer" } 9 | flo-util = { path = "../util" } 10 | 11 | rusoto_core = "0.48.0" 12 | rusoto_kinesis = "0.48.0" 13 | thiserror = "1.0" 14 | tokio = { workspace = true, features = [ 15 | "fs", 16 | "macros", 17 | "time", 18 | "rt-multi-thread", 19 | ] } 20 | once_cell = "1.15" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_json = "1.0" 23 | bytes = "1.2.1" 24 | flate2 = "1.0" 25 | tracing = "0.1" 26 | backoff = "0.3" 27 | 28 | [dev-dependencies] 29 | flo-log-subscriber = { path = "../log-subscriber" } 30 | -------------------------------------------------------------------------------- /crates/node/src/echo.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use std::net::{Ipv4Addr, SocketAddrV4}; 3 | use tokio::net::UdpSocket; 4 | const ALLOWED_ECHO_DATAGRAM_LEN: &[usize] = &[4, 8]; 5 | const MAX_RECV_BUF: usize = 8; 6 | 7 | use flo_constants::NODE_ECHO_PORT; 8 | 9 | pub async fn serve_echo() -> Result<()> { 10 | let socket = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, NODE_ECHO_PORT)).await?; 11 | 12 | let mut recv_buf = [0_u8; MAX_RECV_BUF]; 13 | 14 | loop { 15 | if let Some((size, peer)) = socket.recv_from(&mut recv_buf).await.ok() { 16 | if !ALLOWED_ECHO_DATAGRAM_LEN.contains(&size) { 17 | continue; 18 | } 19 | socket.send_to(&recv_buf[..size], &peer).await.ok(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /migrations/2025-07-12-205202_add-node-cleanup-indizies/up.sql: -------------------------------------------------------------------------------- 1 | -- Composite index for game status and updated_at columns 2 | -- Optimizes queries in get_expired_games that filter by status and updated_at 3 | -- Covers both game creation timeout and game play timeout queries 4 | CREATE INDEX idx_game_status_updated_at ON game(status, updated_at); 5 | 6 | -- Composite index for game_used_slot player_id, client_status, and updated_at columns 7 | -- Optimizes the slot timeout query in get_expired_games that filters by: 8 | -- - player_id IS NOT NULL 9 | -- - client_status != SlotClientStatus::Left (4) 10 | -- - updated_at < timeout_threshold 11 | CREATE INDEX idx_game_used_slot_player_client_updated ON game_used_slot(player_id, client_status, updated_at); 12 | -------------------------------------------------------------------------------- /crates/controller/src/game/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db; 2 | mod slots; 3 | pub(crate) mod state; 4 | pub mod token; 5 | mod types; 6 | 7 | pub mod messages { 8 | pub use super::state::cancel::CancelGame; 9 | pub use super::state::create::CreateGame; 10 | pub use super::state::join::PlayerJoin; 11 | pub use super::state::leave::PlayerLeave; 12 | pub use super::state::node::SelectNode; 13 | pub use super::state::player::GetGamePlayers; 14 | pub use super::state::registry::{ 15 | AddGamePlayer, Register, Remove, RemoveGamePlayer, ResolveGamePlayerPingBroadcastTargets, 16 | }; 17 | pub use super::state::slot::UpdateSlot; 18 | pub use super::state::start::{StartGameCheck, StartGamePlayerAck}; 19 | } 20 | 21 | pub use slots::Slots; 22 | pub use types::*; 23 | -------------------------------------------------------------------------------- /crates/debug/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-debug" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | flo-lan = { path = "../lan" } 11 | flo-w3storage = { path = "../w3storage" } 12 | flo-w3map = { path = "../w3map", features = ["w3storage"] } 13 | flo-w3gs = { path = "../w3gs" } 14 | flo-net = { path = "../net" } 15 | 16 | tokio = { workspace = true, features = ["time", "net", "macros", "sync", "rt", "rt-multi-thread"] } 17 | futures = "0.3.24" 18 | thiserror = "1.0" 19 | tracing = "0.1" 20 | 21 | [dev-dependencies] 22 | dotenv = "0.15" 23 | flo-log-subscriber = { path = "../log-subscriber" } 24 | -------------------------------------------------------------------------------- /binaries/flo-stats-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-stats-service" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | flo-constants = { path = "../../crates/constants" } 8 | flo-observer-edge = { path = "../../crates/observer-edge" } 9 | flo-observer = { path = "../../crates/observer" } 10 | flo-log-subscriber = { path = "../../crates/log-subscriber" } 11 | 12 | tokio = { workspace = true, features = ["time", "sync", "macros", "rt-multi-thread"] } 13 | tokio-stream = { workspace = true, features = ["time"] } 14 | tracing = "0.1" 15 | async-graphql = { version = "4.0", features = ["chrono"] } 16 | async-graphql-axum = "4.0" 17 | axum = "0.5" 18 | tower-http = { version = "0.2.0", features = ["cors"] } 19 | dotenv = "0.15" 20 | once_cell = "1.15.0" 21 | http = "0.2.8" 22 | -------------------------------------------------------------------------------- /crates/otel/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-otel" 3 | version = "0.1.0" 4 | authors = ["Jonas Krüger Svensson ", "Marco de Abreu"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | tracing = { version = "0.1", default-features = false, features = ["log"]} 9 | tracing-core = {version = "0.1"} 10 | tracing-opentelemetry = { version = "0.30" } 11 | tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "json"] } 12 | opentelemetry = { version = "0.29", features = ["trace"] } 13 | opentelemetry_sdk = { version = "0.29" } 14 | opentelemetry-semantic-conventions = { version = "0.29.0", features = ["semconv_experimental"]} 15 | opentelemetry-otlp = { version = "0.29.0", default-features = false, features = ["trace", "grpc-tonic", "metrics"]} 16 | tonic = { version = "0.6" } 17 | -------------------------------------------------------------------------------- /crates/net/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-net" 3 | version = "0.2.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-util = { path = "../util" } 9 | flo-constants = { path = "../constants" } 10 | flo-w3gs = { path = "../w3gs" } 11 | flo-state = "1" 12 | 13 | bytes = "1.2.1" 14 | thiserror = "1" 15 | prometheus = "0.9" 16 | prost = "0.9" 17 | prost-types = "0.9" 18 | tokio = { workspace = true, features = ["time", "net", "macros", "sync"] } 19 | tokio-stream = { workspace = true, features = ["time", "net"] } 20 | tokio-util = { workspace = true, features = ["codec", "net"] } 21 | futures = "0.3.24" 22 | tracing = "0.1" 23 | serde = { version = "1", features = ["derive"] } 24 | bitflags = "1.3" 25 | once_cell = "1.15" 26 | 27 | [build-dependencies] 28 | prost-build = "0.9" 29 | -------------------------------------------------------------------------------- /crates/w3gs/src/protocol/w3gs.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package w3gs; 4 | 5 | message PlayerProfileMessage { 6 | uint32 player_id = 1; 7 | string battle_tag = 2; 8 | string clan = 3; 9 | string portrait = 4; 10 | PlayerProfileRealm realm = 5; 11 | string unknown_1 = 6; 12 | } 13 | 14 | message PlayerProfileListMessage { repeated PlayerProfileMessage items = 1; } 15 | 16 | enum PlayerProfileRealm { 17 | OFFLINE = 0; 18 | AMERICAS = 10; 19 | EUROPE = 20; 20 | ASIA = 30; 21 | } 22 | 23 | message PlayerSkinsMessage { 24 | uint32 player_id = 1; 25 | repeated PlayerSkin skins = 2; 26 | } 27 | 28 | message PlayerSkin { 29 | uint64 unit = 1; 30 | uint64 skin = 2; 31 | string collection = 3; 32 | } 33 | 34 | message PlayerUnknown5Message { 35 | uint32 player_id = 1; 36 | uint32 unknown_1 = 2; 37 | } -------------------------------------------------------------------------------- /crates/w3map/src/constants.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | 3 | bitflags! { 4 | pub struct MapFlags: u32 { 5 | const HIDE_MINIMAP = 0x0001; 6 | const MODIFY_ALLY_PRIORITIES = 0x0002; 7 | const MELEE = 0x0004; 8 | const REVEAL_TERRAIN = 0x0010; 9 | const FIXED_PLAYER_SETTINGS = 0x0020; 10 | const CUSTOM_FORCES = 0x0040; 11 | const CUSTOM_TECH_TREE = 0x0080; 12 | const CUSTOM_ABILITIES = 0x0100; 13 | const CUSTOM_UPGRADES = 0x0200; 14 | const WATER_WAVES_ON_CLIFF_SHORES = 0x0800; 15 | const WATER_WAVES_ON_SLOPE_SHORES = 0x1000; 16 | const HAS_TERRAIN_FOG = 0x2000; 17 | const REQUIRES_EXPANSION = 0x4000; 18 | const ITEM_CLASSIFICATION = 0x8000; 19 | const WATER_TINTING = 0x10000; 20 | const ACCURATE_RANDOM = 0x20000; 21 | const ABILITY_SKINS = 0x40000; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /crates/client/build.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | 4 | fn main() { 5 | let out_dir = std::env::var_os("OUT_DIR").unwrap(); 6 | let pkg_version = env!("CARGO_PKG_VERSION"); 7 | let version = flo_constants::version::Version::parse(pkg_version); 8 | let version_path = Path::new(&out_dir).join("flo_version.rs"); 9 | fs::write( 10 | version_path, 11 | format!( 12 | r#"pub const FLO_VERSION: flo_constants::version::Version = flo_constants::version::Version {{ 13 | major: {major}, 14 | minor: {minor}, 15 | patch: {patch}, 16 | }}; 17 | pub const FLO_VERSION_STRING: &str = "{version_str}"; 18 | "#, 19 | major = version.major, 20 | minor = version.minor, 21 | patch = version.patch, 22 | version_str = pkg_version 23 | ), 24 | ) 25 | .unwrap() 26 | } 27 | -------------------------------------------------------------------------------- /crates/kinesis/src/error.rs: -------------------------------------------------------------------------------- 1 | use rusoto_core::RusotoError; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum Error { 6 | #[error("Cancelled")] 7 | Cancelled, 8 | #[error("No shards")] 9 | NoShards, 10 | #[error("List shards iterator: {0}")] 11 | ListShards(#[from] RusotoError), 12 | #[error("No shard iterator: {0}")] 13 | NoShardIterator(String), 14 | #[error("Game data lost: {0}")] 15 | GameDataLost(i32), 16 | #[error("decode game record: {0}")] 17 | DecodeGameRecord(#[from] flo_observer::record::RecordError), 18 | #[error("Get shard iterator: {0}")] 19 | GetShardIterator(#[from] RusotoError), 20 | #[error("io: {0}")] 21 | Io(#[from] std::io::Error), 22 | } 23 | 24 | pub type Result = std::result::Result; 25 | -------------------------------------------------------------------------------- /crates/node/build.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | 4 | fn main() { 5 | let out_dir = std::env::var_os("OUT_DIR").unwrap(); 6 | let pkg_version = env!("CARGO_PKG_VERSION"); 7 | let version = flo_constants::version::Version::parse(pkg_version); 8 | let version_path = Path::new(&out_dir).join("flo_node_version.rs"); 9 | fs::write( 10 | version_path, 11 | format!( 12 | r#"pub const FLO_NODE_VERSION: flo_constants::version::Version = flo_constants::version::Version {{ 13 | major: {major}, 14 | minor: {minor}, 15 | patch: {patch}, 16 | }}; 17 | pub const FLO_NODE_VERSION_STRING: &str = "{version_str}"; 18 | "#, 19 | major = version.major, 20 | minor = version.minor, 21 | patch = version.patch, 22 | version_str = pkg_version 23 | ), 24 | ) 25 | .unwrap() 26 | } 27 | -------------------------------------------------------------------------------- /crates/platform/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("unable to determine the Warcraft III user data path")] 6 | NoUserDataPath, 7 | 8 | #[error("unable to determine the Warcraft III installation path")] 9 | NoInstallationFolder, 10 | 11 | #[error("unable to get Warcraft III version")] 12 | GetWar3Version, 13 | 14 | #[error("unable to get running Warcraft III path ({0})")] 15 | GetRunningWar3Path(u32), 16 | 17 | #[error("config: {0}")] 18 | Config(#[from] flo_config::error::Error), 19 | 20 | #[cfg(target_os = "macos")] 21 | #[error("plist: {0}")] 22 | PList(#[from] plist::Error), 23 | 24 | #[cfg(target_os = "linux")] 25 | #[error("no Warcraft 3 version number set")] 26 | NoVersionNumber 27 | } 28 | 29 | pub type Result = std::result::Result; 30 | -------------------------------------------------------------------------------- /crates/controller/build.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | 4 | fn main() { 5 | let out_dir = std::env::var_os("OUT_DIR").unwrap(); 6 | let pkg_version = env!("CARGO_PKG_VERSION"); 7 | let version = flo_constants::version::Version::parse(pkg_version); 8 | let version_path = Path::new(&out_dir).join("flo_lobby_version.rs"); 9 | fs::write( 10 | version_path, 11 | format!( 12 | r#"pub const FLO_LOBBY_VERSION: flo_constants::version::Version = flo_constants::version::Version {{ 13 | major: {major}, 14 | minor: {minor}, 15 | patch: {patch}, 16 | }}; 17 | pub const FLO_LOBBY_VERSION_STRING: &str = "{version_str}"; 18 | "#, 19 | major = version.major, 20 | minor = version.minor, 21 | patch = version.patch, 22 | version_str = pkg_version 23 | ), 24 | ) 25 | .unwrap() 26 | } 27 | -------------------------------------------------------------------------------- /crates/observer-edge/build.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | 4 | fn main() { 5 | let out_dir = std::env::var_os("OUT_DIR").unwrap(); 6 | let pkg_version = env!("CARGO_PKG_VERSION"); 7 | let version = flo_constants::version::Version::parse(pkg_version); 8 | let version_path = Path::new(&out_dir).join("flo_observer_version.rs"); 9 | fs::write( 10 | version_path, 11 | format!( 12 | r#"pub const FLO_OBSERVER_VERSION: flo_constants::version::Version = flo_constants::version::Version {{ 13 | major: {major}, 14 | minor: {minor}, 15 | patch: {patch}, 16 | }}; 17 | pub const FLO_OBSERVER_VERSION_STRING: &str = "{version_str}"; 18 | "#, 19 | major = version.major, 20 | minor = version.minor, 21 | patch = version.patch, 22 | version_str = pkg_version 23 | ), 24 | ) 25 | .unwrap() 26 | } 27 | -------------------------------------------------------------------------------- /crates/util/src/uptime.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use std::time::Instant; 3 | 4 | lazy_static! { 5 | static ref INITIAL: Instant = Instant::now(); 6 | } 7 | 8 | pub fn initialize() { 9 | lazy_static::initialize(&INITIAL); 10 | } 11 | 12 | pub fn uptime_ms() -> u32 { 13 | Instant::now() 14 | .checked_duration_since(*INITIAL) 15 | .map(|d| d.as_millis() as u32) 16 | .unwrap_or(0) 17 | } 18 | 19 | #[test] 20 | fn test_uptime_ms() { 21 | assert_eq!(uptime_ms(), 0); 22 | std::thread::sleep(std::time::Duration::from_millis(100)); 23 | assert!(uptime_ms() >= 100); 24 | } 25 | 26 | #[test] 27 | fn test_uptime_ms_inited() { 28 | initialize(); 29 | std::thread::sleep(std::time::Duration::from_millis(100)); 30 | assert!(uptime_ms() >= 100); 31 | std::thread::sleep(std::time::Duration::from_millis(100)); 32 | assert!(uptime_ms() >= 200); 33 | } 34 | -------------------------------------------------------------------------------- /crates/state/src/reply.rs: -------------------------------------------------------------------------------- 1 | use futures::task::Context; 2 | use futures::FutureExt; 3 | use std::future::Future; 4 | use std::pin::Pin; 5 | use std::task; 6 | use tokio::sync::oneshot; 7 | 8 | pub struct FutureReply(oneshot::Receiver); 9 | 10 | impl FutureReply { 11 | pub fn channel() -> (FutureReplySender, FutureReply) { 12 | let (tx, rx) = oneshot::channel(); 13 | (FutureReplySender(tx), FutureReply(rx)) 14 | } 15 | } 16 | 17 | pub struct FutureReplySender(oneshot::Sender); 18 | impl FutureReplySender { 19 | pub fn send(self, value: T) -> Result<(), T> { 20 | self.0.send(value) 21 | } 22 | } 23 | 24 | impl Future for FutureReply { 25 | type Output = Option; 26 | 27 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> task::Poll { 28 | self.0.poll_unpin(cx).map(|res| res.ok()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/client/src/message/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod messages; 2 | mod session; 3 | mod stream; 4 | use flo_state::Message; 5 | pub use session::Session; 6 | 7 | use crate::error::Error; 8 | 9 | pub mod embed; 10 | #[cfg(feature = "ws")] 11 | pub mod ws; 12 | 13 | #[derive(Debug)] 14 | pub enum MessageEvent { 15 | ConnectController(ConnectController), 16 | Disconnect, 17 | WorkerError(Error), 18 | } 19 | 20 | impl Message for MessageEvent { 21 | type Result = (); 22 | } 23 | 24 | #[derive(Debug)] 25 | pub struct ConnectController { 26 | pub token: String, 27 | } 28 | 29 | impl Message for ConnectController { 30 | type Result = (); 31 | } 32 | 33 | impl From> for Error { 34 | fn from(_: tokio::sync::mpsc::error::SendError) -> Error { 35 | Error::TaskCancelled(anyhow::format_err!("WsEvent dropped")) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/debug/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("Unexpected w3gs packet: {0:?}")] 6 | UnexpectedW3GSPacket(flo_w3gs::packet::Packet), 7 | #[error("Stream closed unexpectedly")] 8 | StreamClosed, 9 | #[error("player emulator: {0}")] 10 | PlayerEmulator(#[from] crate::player_emulator::PlayerEmulatorError), 11 | #[error("Lan: {0}")] 12 | Lan(#[from] flo_lan::error::Error), 13 | #[error("W3GS: {0}")] 14 | W3GS(#[from] flo_w3gs::error::Error), 15 | #[error("Map: {0}")] 16 | War3Map(#[from] flo_w3map::error::Error), 17 | #[error("War3 data: {0}")] 18 | War3Data(#[from] flo_w3storage::error::Error), 19 | #[error("Net: {0}")] 20 | Net(#[from] flo_net::error::Error), 21 | #[error("Io: {0}")] 22 | Io(#[from] std::io::Error), 23 | } 24 | 25 | pub type Result = std::result::Result; 26 | -------------------------------------------------------------------------------- /crates/node/src/game/host/broadcast.rs: -------------------------------------------------------------------------------- 1 | pub trait BroadcastTarget { 2 | fn contains(&self, player_id: i32) -> bool; 3 | } 4 | 5 | pub struct Everyone; 6 | impl BroadcastTarget for Everyone { 7 | fn contains(&self, _player_id: i32) -> bool { 8 | true 9 | } 10 | } 11 | 12 | pub struct AllowList<'a>(pub &'a [i32]); 13 | impl<'a> BroadcastTarget for AllowList<'a> { 14 | fn contains(&self, player_id: i32) -> bool { 15 | self.0.contains(&player_id) 16 | } 17 | } 18 | 19 | pub struct DenyList<'a>(pub &'a [i32]); 20 | impl<'a> BroadcastTarget for DenyList<'a> { 21 | fn contains(&self, player_id: i32) -> bool { 22 | !self.0.contains(&player_id) 23 | } 24 | } 25 | 26 | pub struct Filter(F); 27 | impl BroadcastTarget for Filter 28 | where 29 | F: Fn(i32) -> bool, 30 | { 31 | fn contains(&self, player_id: i32) -> bool { 32 | (self.0)(player_id) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/lan/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-lan" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-log = { path = "../log" } 9 | flo-util = { path = "../util" } 10 | flo-w3gs = { path = "../w3gs" } 11 | flo-w3map = { path = "../w3map" } 12 | flo-w3storage = { path = "../w3storage" } 13 | flo-w3replay = { path = "../w3replay" } 14 | flo-platform = { path = "../platform" } 15 | 16 | tokio = { workspace = true, features = ["time", "sync", "macros"] } 17 | tokio-stream = { workspace = true, features = ["time"] } 18 | hostname = "^0.3" 19 | pretty-hex = "0.1" 20 | bytes = "1.2.1" 21 | prost = "0.9" 22 | base64 = "0.12" 23 | thiserror = "1" 24 | lazy_static = "1" 25 | parking_lot = "0.11" 26 | tracing = "0.1" 27 | tracing-futures = "0.2" 28 | futures = "0.3.24" 29 | async-dnssd = "0.5.0" 30 | 31 | [build-dependencies] 32 | prost-build = "0.9" 33 | -------------------------------------------------------------------------------- /crates/platform/src/path/macos.rs: -------------------------------------------------------------------------------- 1 | use home_dir::HomeDirExt; 2 | use std::path::PathBuf; 3 | 4 | pub fn detect_user_data_path(ptr: bool) -> Option { 5 | let path = PathBuf::from( 6 | if ptr {"~/Library/Application Support/Blizzard/Warcraft III Public Test"} 7 | else {"~/Library/Application Support/Blizzard/Warcraft III"}) 8 | .expand_home() 9 | .ok()?; 10 | if std::fs::metadata(&path).is_ok() { 11 | Some(path) 12 | } else { 13 | None 14 | } 15 | } 16 | 17 | pub fn detect_installation_path() -> Option { 18 | let path = PathBuf::from("/Applications/Warcraft III"); 19 | if std::fs::metadata(path.join("Warcraft III Launcher.app")).is_ok() { 20 | return Some(path); 21 | } 22 | None 23 | } 24 | 25 | #[test] 26 | fn test_macos() { 27 | assert!(dbg!(detect_user_data_path()).is_some()); 28 | assert!(dbg!(detect_installation_path()).is_some()); 29 | } 30 | -------------------------------------------------------------------------------- /crates/codegen/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | use darling::FromDeriveInput; 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::{parse_macro_input, DeriveInput}; 6 | 7 | mod derive_binary; 8 | 9 | #[proc_macro_derive(BinDecode, attributes(bin))] 10 | pub fn derive_bin_decode(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 11 | let input = parse_macro_input!(input as DeriveInput); 12 | let receiver = derive_binary::DecodeInputReceiver::from_derive_input(&input).unwrap(); 13 | TokenStream::from(quote!(#receiver)) 14 | } 15 | 16 | #[proc_macro_derive(BinEncode, attributes(bin))] 17 | pub fn derive_bin_encode(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 18 | let input = parse_macro_input!(input as DeriveInput); 19 | let receiver = derive_binary::EncodeInputReceiver::from_derive_input(&input).unwrap(); 20 | TokenStream::from(quote!(#receiver)) 21 | } 22 | -------------------------------------------------------------------------------- /binaries/flo-cli/src/grpc.rs: -------------------------------------------------------------------------------- 1 | use crate::env::ENV; 2 | pub use flo_grpc::controller::flo_controller_client::FloControllerClient; 3 | use flo_grpc::Channel; 4 | use tonic::service::{Interceptor, interceptor::InterceptedService}; 5 | 6 | pub async fn get_grpc_client() -> FloControllerClient> { 7 | let host = ENV.controller_host.clone(); 8 | let channel = Channel::from_shared(format!("tcp://{}:3549", host)) 9 | .unwrap() 10 | .connect() 11 | .await 12 | .unwrap(); 13 | FloControllerClient::with_interceptor(channel, WithSecret) 14 | } 15 | 16 | #[derive(Clone)] 17 | pub struct WithSecret; 18 | 19 | impl Interceptor for WithSecret { 20 | fn call(&mut self, mut req: tonic::Request<()>) -> Result, tonic::Status> { 21 | req 22 | .metadata_mut() 23 | .insert("x-flo-secret", ENV.controller_secret.parse().unwrap()); 24 | Ok(req) 25 | } 26 | } -------------------------------------------------------------------------------- /migrations/2020-08-21-020955_game_used_slot/up.sql: -------------------------------------------------------------------------------- 1 | create table game_used_slot ( 2 | id serial not null primary key, 3 | game_id integer not null references game(id) on delete cascade, 4 | player_id integer references player(id) on delete cascade, 5 | slot_index integer not null, 6 | team integer not null, 7 | color integer not null, 8 | computer integer not null, 9 | handicap integer not null, 10 | status integer not null, 11 | race integer not null, 12 | client_status integer not null, 13 | node_token bytea, 14 | created_at timestamp with time zone default now() not null, 15 | updated_at timestamp with time zone default now() not null, 16 | unique(game_id, player_id), 17 | unique(game_id, slot_index) 18 | ); 19 | SELECT diesel_manage_updated_at('game_used_slot'); 20 | 21 | create index game_used_slot_game_id on game_used_slot(game_id); 22 | create index game_used_slot_player_id on game_used_slot(player_id); -------------------------------------------------------------------------------- /binaries/flo-worker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-worker" 3 | version = "0.18.3" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [features] 8 | default = [] 9 | blacklist = ["flo-client/blacklist"] 10 | 11 | [dependencies] 12 | flo-client = { path = "../../crates/client", features = ["worker"] } 13 | flo-constants = { path = "../../crates/constants" } 14 | structopt = { version = "0.3", default-features = false } 15 | tokio = { workspace = true, features = [ 16 | "rt", 17 | "rt-multi-thread", 18 | "signal", 19 | "fs", 20 | "time", 21 | ] } 22 | tracing-subscriber = { version = "0.3.18", features = ["env-filter", "fmt"] } 23 | tracing-appender = "0.2" 24 | once_cell = "1.15" 25 | serde_json = "1.0" 26 | anyhow = "1.0" 27 | tracing = "0.1" 28 | 29 | [target.'cfg(windows)'.dependencies] 30 | winapi = { version = "0.3", features = ["processthreadsapi"] } 31 | 32 | [target.'cfg(windows)'.build-dependencies] 33 | embed-resource = "1.7" 34 | -------------------------------------------------------------------------------- /crates/net/src/proto/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package flo_common; 3 | 4 | message Version { 5 | int32 major = 1; 6 | int32 minor = 2; 7 | int32 patch = 3; 8 | } 9 | 10 | message SlotSettings { 11 | int32 team = 1; 12 | int32 color = 2; 13 | Computer computer = 3; 14 | int32 handicap = 4; 15 | SlotStatus status = 5; 16 | Race race = 6; 17 | } 18 | 19 | enum SlotStatus { 20 | SlotStatusOpen = 0; 21 | SlotStatusClosed = 1; 22 | SlotStatusOccupied = 2; 23 | } 24 | 25 | enum Race { 26 | RaceHuman = 0; 27 | RaceOrc = 1; 28 | RaceNightElf = 2; 29 | RaceUndead = 3; 30 | RaceRandom = 4; 31 | } 32 | 33 | enum Computer { 34 | ComputerEasy = 0; 35 | ComputerNormal = 1; 36 | ComputerInsane = 2; 37 | } 38 | 39 | enum SlotClientStatus { 40 | SlotClientStatusPending = 0; 41 | SlotClientStatusConnected = 1; 42 | SlotClientStatusJoined = 2; 43 | SlotClientStatusLoading = 3; 44 | SlotClientStatusLoaded = 4; 45 | SlotClientStatusDisconnected = 5; 46 | SlotClientStatusLeft = 6; 47 | } -------------------------------------------------------------------------------- /crates/w3replay/src/constants.rs: -------------------------------------------------------------------------------- 1 | use flo_util::binary::*; 2 | use flo_util::{BinDecode, BinEncode}; 3 | 4 | pub const SIGNATURE: [u8; 28] = *b"Warcraft III recorded game\x1A\0"; 5 | pub const SUPPORTED_BLOCK_SIZE: usize = 8192; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, BinEncode, BinDecode)] 8 | #[bin(enum_repr(u8))] 9 | pub enum RecordTypeId { 10 | #[bin(value = 0x10)] 11 | GameInfo, 12 | #[bin(value = 0x16)] 13 | PlayerInfo, 14 | #[bin(value = 0x17)] 15 | PlayerLeft, 16 | #[bin(value = 0x19)] 17 | SlotInfo, 18 | #[bin(value = 0x1A)] 19 | CountDownStart, 20 | #[bin(value = 0x1B)] 21 | CountDownEnd, 22 | #[bin(value = 0x1C)] 23 | GameStart, 24 | #[bin(value = 0x1E)] 25 | TimeSlotFragment, 26 | #[bin(value = 0x1F)] 27 | TimeSlot, 28 | #[bin(value = 0x20)] 29 | ChatMessage, 30 | #[bin(value = 0x22)] 31 | TimeSlotAck, 32 | #[bin(value = 0x23)] 33 | Desync, 34 | #[bin(value = 0x2F)] 35 | EndTimer, 36 | #[bin(value = 0x38)] 37 | ProtoBuf, 38 | UnknownValue(u8), 39 | } 40 | -------------------------------------------------------------------------------- /crates/types/src/node.rs: -------------------------------------------------------------------------------- 1 | use s2_grpc_utils::{S2ProtoEnum, S2ProtoUnpack}; 2 | use serde::Serialize; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Debug, S2ProtoEnum, PartialEq, Copy, Clone, Serialize)] 6 | #[s2_grpc(proto_enum_type = "flo_net::proto::flo_node::NodeGameStatus")] 7 | pub enum NodeGameStatus { 8 | Created = 0, 9 | Waiting = 1, 10 | Loading = 2, 11 | Running = 3, 12 | Ended = 4, 13 | } 14 | 15 | #[derive(Debug, S2ProtoEnum, PartialEq, Copy, Clone, Serialize)] 16 | #[s2_grpc(proto_enum_type = "flo_net::proto::flo_connect::SlotClientStatus")] 17 | pub enum SlotClientStatus { 18 | Pending = 0, 19 | Connected = 1, 20 | Joined = 2, 21 | Loading = 3, 22 | Loaded = 4, 23 | Disconnected = 5, 24 | Left = 6, 25 | } 26 | 27 | #[derive(Debug, S2ProtoUnpack)] 28 | #[s2_grpc(message_type = "flo_net::proto::flo_node::PacketClientConnectAccept")] 29 | pub struct NodeGameStatusSnapshot { 30 | pub game_id: i32, 31 | pub game_status: NodeGameStatus, 32 | pub player_game_client_status_map: HashMap, 33 | } 34 | -------------------------------------------------------------------------------- /crates/client/src/observer/source/archive_file.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use flo_observer::record::GameRecordData; 3 | use flo_observer_fs::GameDataArchiveReader; 4 | use futures::{Stream, StreamExt}; 5 | use std::{ 6 | path::Path, 7 | pin::Pin, 8 | task::{Context, Poll}, 9 | }; 10 | 11 | use super::memory::MemorySource; 12 | 13 | pub struct ArchiveFileSource { 14 | inner: MemorySource, 15 | } 16 | 17 | impl ArchiveFileSource { 18 | pub async fn load>(path: P) -> Result { 19 | let inner = MemorySource::new(GameDataArchiveReader::open(path) 20 | .await? 21 | .records() 22 | .collect_vec() 23 | .await 24 | ?); 25 | 26 | tracing::debug!("archive duration: {}ms", inner.remaining_millis()); 27 | 28 | Ok(Self { 29 | inner 30 | }) 31 | } 32 | } 33 | 34 | impl Stream for ArchiveFileSource { 35 | type Item = Result; 36 | 37 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 38 | self.inner.poll_next_unpin(cx) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /crates/w3replay/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("unsupported game version: {0}")] 6 | UnsupportedGameVersion(String), 7 | #[error("read header: {0}")] 8 | ReadHeader(std::io::Error), 9 | #[error("unsupported block size, expected 8192, got {0}")] 10 | UnsupportedBlockSize(usize), 11 | #[error("read block header: {0}")] 12 | ReadBlockHeader(std::io::Error), 13 | #[error("invalid checksum: subject = {subject}, expected = {expected}, got = {got}")] 14 | InvalidChecksum { 15 | subject: &'static str, 16 | expected: u16, 17 | got: u16, 18 | }, 19 | #[error("no game info record")] 20 | NoGameInfoRecord, 21 | #[error("no slot info record")] 22 | NoSlotInfoRecord, 23 | #[error("decompress: {0}")] 24 | Decompress(#[from] flate2::DecompressError), 25 | #[error("bin decode: {0}")] 26 | BinDecode(#[from] flo_util::binary::BinDecodeError), 27 | #[error("io: {0}")] 28 | Io(#[from] std::io::Error), 29 | } 30 | 31 | pub type Result = std::result::Result; 32 | -------------------------------------------------------------------------------- /binaries/flo-worker-ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-worker-ui" 3 | version = "0.1.0" 4 | authors = ["Miezhiko "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | # flo worker based: 9 | flo-client = { path = "../../crates/client", features = ["worker"] } 10 | flo-constants = { path = "../../crates/constants" } 11 | structopt = { version = "0.3", default-features = false } 12 | tokio = { workspace = true, features = ["rt", "rt-multi-thread", "signal"] } 13 | tracing-subscriber = "0.3.18" 14 | tracing-appender = "0.2" 15 | tracing = "0.1" 16 | once_cell = "1.15" 17 | serde_json = "1" 18 | serde = "1" 19 | anyhow = "1.0" 20 | 21 | # for UI 22 | iced_native = "0.4" 23 | image = "0.23" 24 | webbrowser = "0.5" 25 | 26 | # alternatively wgpu instead of glow 27 | [dependencies.iced] 28 | version = "0.3" 29 | default-features = false 30 | features = ["glow_default_system_font", "glow"] 31 | 32 | [target.'cfg(windows)'.dependencies] 33 | winapi = { version = "0.3", features = ["processthreadsapi"] } 34 | 35 | [build-dependencies] 36 | embed-resource = "1.7" 37 | -------------------------------------------------------------------------------- /crates/client/src/game/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | use flo_types::game::{GameInfo, LocalGameInfo}; 3 | 4 | pub fn local_game_from_game_info(player_id: i32, game: &GameInfo) -> Result { 5 | Ok(LocalGameInfo { 6 | name: game.name.clone(), 7 | game_id: game.id, 8 | random_seed: game.random_seed, 9 | node_id: game.node.as_ref().map(|v| v.id).clone(), 10 | player_id, 11 | map_path: game.map.path.clone(), 12 | map_sha1: { 13 | if game.map.sha1.len() != 20 { 14 | return Err(Error::InvalidMapInfo); 15 | } 16 | let mut value = [0_u8; 20]; 17 | value.copy_from_slice(&game.map.sha1[..]); 18 | value 19 | }, 20 | map_checksum: game.map.checksum, 21 | players: game 22 | .slots 23 | .iter() 24 | .filter_map(|slot| slot.player.clone().map(|player| (player.id, player))) 25 | .collect(), 26 | slots: game.slots.clone(), 27 | host_player: game.created_by.clone(), 28 | mask_player_names: game.mask_player_names, 29 | map_twelve_p: game.map.twelve_p, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /crates/w3map/src/error.rs: -------------------------------------------------------------------------------- 1 | use flo_util::binary::BinDecodeError; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum Error { 6 | #[error("map script not found")] 7 | MapScriptNotFound, 8 | #[error("storage file not found: {0}")] 9 | StorageFileNotFound(String), 10 | #[cfg(feature = "w3storage")] 11 | #[error("storage: {0}")] 12 | Storage(#[from] flo_w3storage::error::Error), 13 | #[error("stormlib: {0}")] 14 | Storm(#[from] stormlib::error::StormError), 15 | #[error("ceres_mpq: {0}")] 16 | CeresMpq(#[from] ceres_mpq::Error), 17 | #[error("invalid utf8 bytes: {0}")] 18 | Utf8(#[from] std::str::Utf8Error), 19 | #[error("read map info: {0}")] 20 | ReadInfo(BinDecodeError), 21 | #[error("read map image: {0}")] 22 | ReadImage(BinDecodeError), 23 | #[error("read map minimap icons: {0}")] 24 | ReadMinimapIcons(BinDecodeError), 25 | #[error("read map trigger strings: {0}")] 26 | ReadTriggerStrings(BinDecodeError), 27 | #[error("io: {0}")] 28 | Io(#[from] std::io::Error), 29 | } 30 | 31 | pub type Result = std::result::Result; 32 | -------------------------------------------------------------------------------- /crates/w3gs/src/protocol/lag.rs: -------------------------------------------------------------------------------- 1 | use flo_util::{BinDecode, BinEncode}; 2 | 3 | use crate::protocol::constants::PacketTypeId; 4 | use crate::protocol::packet::PacketPayload; 5 | 6 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 7 | pub struct StartLag { 8 | _num_of_players: u8, 9 | #[bin(repeat = "_num_of_players")] 10 | players: Vec, 11 | } 12 | 13 | impl StartLag { 14 | pub fn new(players: Vec) -> Self { 15 | StartLag { 16 | _num_of_players: players.len() as u8, 17 | players, 18 | } 19 | } 20 | 21 | pub fn players(&self) -> &[LagPlayer] { 22 | self.players.as_ref() 23 | } 24 | } 25 | 26 | impl PacketPayload for StartLag { 27 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::StartLag; 28 | } 29 | 30 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 31 | pub struct LagPlayer { 32 | pub player_id: u8, 33 | pub lag_duration_ms: u32, 34 | } 35 | 36 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 37 | pub struct StopLag(pub LagPlayer); 38 | 39 | impl PacketPayload for StopLag { 40 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::StopLag; 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 W3Champions 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 | -------------------------------------------------------------------------------- /crates/util/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod binary; 2 | pub mod chat; 3 | pub mod dword_string; 4 | pub mod error; 5 | pub mod stat_string; 6 | pub mod uptime; 7 | 8 | pub use flo_codegen::*; 9 | 10 | pub fn dump_hex(data: T) 11 | where 12 | T: AsRef<[u8]>, 13 | { 14 | use pretty_hex::*; 15 | println!("{:?}", data.as_ref().hex_dump()); 16 | } 17 | 18 | #[macro_export] 19 | macro_rules! sample_path { 20 | ( 21 | $($rpath:expr),+ 22 | ) => {{ 23 | use std::path::Path; 24 | use std::ffi::OsStr; 25 | let mut manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); 26 | let mut path = manifest_dir.ancestors() 27 | .find(|path| path.file_name() == Some(OsStr::new("crates"))) 28 | .and_then(|path| path.parent()) 29 | .map(|path| path.to_owned()) 30 | .unwrap(); 31 | path.push("deps"); 32 | path.push("wc3-samples"); 33 | $( 34 | path.push($rpath); 35 | )* 36 | path 37 | }}; 38 | } 39 | 40 | #[macro_export] 41 | macro_rules! sample_bytes { 42 | ( 43 | $($rpath:expr),+ 44 | ) => { 45 | std::fs::read( 46 | $crate::sample_path!($($rpath),*) 47 | ).unwrap() 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /crates/controller/src/client/handshake.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use flo_net::connect::*; 4 | use flo_net::packet::*; 5 | use flo_net::stream::FloStream; 6 | 7 | use crate::error::*; 8 | use crate::game::Game; 9 | use crate::player::token::validate_player_token; 10 | use flo_constants::version::Version; 11 | 12 | pub async fn handle_handshake(stream: &mut FloStream) -> Result { 13 | let req: PacketClientConnect = stream.recv_timeout(Duration::from_secs(3)).await?; 14 | let client_version = req.connect_version.extract()?; 15 | 16 | tracing::debug!("client version = {}", client_version); 17 | 18 | let token = validate_player_token(&req.token)?; 19 | 20 | tracing::debug!(token.player_id); 21 | 22 | Ok(ConnectState { 23 | player_id: token.player_id, 24 | joined_game: None, 25 | client_version: Version { 26 | major: client_version.major, 27 | minor: client_version.minor, 28 | patch: client_version.patch, 29 | }, 30 | }) 31 | } 32 | 33 | #[derive(Debug)] 34 | pub struct ConnectState { 35 | pub player_id: i32, 36 | pub joined_game: Option, 37 | pub client_version: Version, 38 | } 39 | -------------------------------------------------------------------------------- /crates/node/src/state/event.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::mpsc::{Receiver, Sender}; 2 | 3 | use flo_event::*; 4 | 5 | use crate::controller::ControllerServerHandle; 6 | use crate::error::*; 7 | use crate::state::GlobalStateRef; 8 | 9 | pub type GlobalEventSender = Sender; 10 | 11 | // Infrequent events that can be processed offline quickly 12 | #[derive(Debug)] 13 | pub enum GlobalEvent { 14 | // A game has ended, remove the session from global state 15 | GameEnded(i32), 16 | } 17 | 18 | impl FloEvent for GlobalEvent { 19 | const NAME: &'static str = "NodeEvent"; 20 | } 21 | 22 | #[derive(Debug)] 23 | pub struct FloNodeEventContext { 24 | pub state: GlobalStateRef, 25 | pub ctrl: ControllerServerHandle, 26 | } 27 | 28 | pub async fn handle_global_events( 29 | ctx: FloNodeEventContext, 30 | mut event_receiver: Receiver, 31 | ) -> Result<()> { 32 | while let Some(event) = event_receiver.recv().await { 33 | match event { 34 | GlobalEvent::GameEnded(game_id) => { 35 | tracing::info!(game_id, "game ended: {}", game_id); 36 | ctx.state.end_game(game_id); 37 | } 38 | } 39 | } 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /binaries/flo-cli/src/env.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | 3 | pub static ENV: Lazy = Lazy::new(|| { 4 | let controller_host = std::env::var("FLO_CONTROLLER_HOST") 5 | .ok() 6 | .unwrap_or_else(|| "127.0.0.1".to_string()); 7 | let controller_secret = std::env::var("FLO_CONTROLLER_SECRET") 8 | .ok() 9 | .unwrap_or_else(|| "TEST".to_string()); 10 | let stats_host = std::env::var("FLO_STATS_HOST") 11 | .ok() 12 | .unwrap_or_else(|| "127.0.0.1".to_string()); 13 | Env { 14 | controller_host, 15 | controller_secret, 16 | stats_host, 17 | aws_s3_region: std::env::var("AWS_S3_REGION").ok(), 18 | aws_s3_bucket: std::env::var("AWS_S3_BUCKET").ok(), 19 | aws_access_key_id: std::env::var("AWS_ACCESS_KEY_ID").ok(), 20 | aws_secret_access_key: std::env::var("AWS_SECRET_ACCESS_KEY").ok(), 21 | } 22 | }); 23 | 24 | pub struct Env { 25 | pub controller_host: String, 26 | pub controller_secret: String, 27 | pub stats_host: String, 28 | pub aws_s3_region: Option, 29 | pub aws_s3_bucket: Option, 30 | pub aws_access_key_id: Option, 31 | pub aws_secret_access_key: Option, 32 | } 33 | -------------------------------------------------------------------------------- /binaries/flo-worker-ui/src/core.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use structopt::StructOpt; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | pub const JSON_CONF_FNAME: &str = "flo-config.json"; 7 | 8 | #[derive(Debug, StructOpt, Clone, Serialize, Deserialize)] 9 | #[structopt(name = "flo-worker", about = "Flo worker process.")] 10 | pub struct Opt { 11 | #[structopt(long)] 12 | pub debug: bool, 13 | 14 | #[structopt(long)] 15 | pub token: Option, 16 | 17 | #[structopt(long, parse(from_os_str))] 18 | pub installation_path: Option, 19 | 20 | #[structopt(long, parse(from_os_str))] 21 | pub user_data_path: Option, 22 | 23 | #[structopt(long)] 24 | pub controller_host: Option, 25 | 26 | #[structopt(long)] 27 | pub use_flo_web: bool, 28 | 29 | #[structopt(long)] 30 | pub ptr: Option, 31 | } 32 | 33 | impl Default for Opt { 34 | fn default() -> Self { 35 | Self { 36 | debug: false, 37 | token: None, 38 | installation_path: None, 39 | user_data_path: None, 40 | controller_host: None, 41 | use_flo_web: true, 42 | ptr: None 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/controller/src/game/state/node.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::game::state::GameActor; 3 | 4 | use flo_net::packet::FloPacket; 5 | use flo_net::proto; 6 | use flo_state::{async_trait, Context, Handler, Message}; 7 | 8 | pub struct SelectNode { 9 | pub node_id: Option, 10 | pub player_id: i32, 11 | } 12 | 13 | impl Message for SelectNode { 14 | type Result = Result<()>; 15 | } 16 | 17 | #[async_trait] 18 | impl Handler for GameActor { 19 | async fn handle( 20 | &mut self, 21 | _: &mut Context, 22 | SelectNode { node_id, player_id }: SelectNode, 23 | ) -> Result<()> { 24 | let game_id = self.game_id; 25 | 26 | if self.started() { 27 | return Err(Error::GameStarted); 28 | } 29 | 30 | self 31 | .db 32 | .exec(move |conn| crate::game::db::select_node(conn, game_id, player_id, node_id)) 33 | .await?; 34 | 35 | self.selected_node_id = node_id; 36 | 37 | let frame = proto::flo_connect::PacketGameSelectNode { game_id, node_id }.encode_as_frame()?; 38 | self 39 | .player_reg 40 | .broadcast(self.players.clone(), frame) 41 | .await?; 42 | 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/node/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod controller; 3 | mod echo; 4 | mod env; 5 | mod game; 6 | mod metrics; 7 | mod state; 8 | mod version; 9 | 10 | mod constants; 11 | pub mod error; 12 | mod observer; 13 | mod scheduler_fairness; 14 | 15 | use error::Result; 16 | 17 | use flo_event::*; 18 | 19 | use self::client::serve_client; 20 | use self::echo::serve_echo; 21 | use self::metrics::serve_metrics; 22 | use crate::state::GlobalState; 23 | use state::event::{handle_global_events, FloNodeEventContext, GlobalEvent}; 24 | 25 | pub async fn serve() -> Result<()> { 26 | let (event_sender, event_receiver) = GlobalEvent::channel(30); 27 | let state = GlobalState::new(event_sender).into_ref(); 28 | let mut ctrl = controller::ControllerServer::new(state.clone()); 29 | let ctrl_handle = ctrl.handle(); 30 | 31 | scheduler_fairness::start_monitoring(); 32 | 33 | tokio::try_join!( 34 | ctrl.serve(), 35 | serve_client(state.clone()), 36 | serve_metrics(), 37 | serve_echo(), 38 | handle_global_events( 39 | FloNodeEventContext { 40 | state, 41 | ctrl: ctrl_handle, 42 | }, 43 | event_receiver 44 | ) 45 | ) 46 | .map(|_| ()) 47 | } 48 | -------------------------------------------------------------------------------- /crates/client/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod controller; 2 | pub mod error; 3 | mod game; 4 | mod lan; 5 | mod message; 6 | mod node; 7 | pub mod observer; 8 | mod ping; 9 | pub mod platform; 10 | pub mod tcphealth; 11 | mod version; 12 | use tokio::sync::Notify; 13 | pub use version::FLO_VERSION; 14 | 15 | use std::{path::PathBuf, sync::Arc}; 16 | 17 | #[derive(Debug, Default, Clone)] 18 | pub struct StartConfig { 19 | pub token: Option, 20 | pub installation_path: Option, 21 | pub user_data_path: Option, 22 | pub controller_host: Option, 23 | pub stats_host: Option, 24 | pub version: Option, 25 | pub ptr: Option, 26 | pub save_replay: bool, //Default value is false 27 | pub user_battlenet_client_id: Option, 28 | pub lobby_countdown_notify: Option>, // Keep this name for backward compatibility 29 | pub lobby_countdown_start_notify: Option>, 30 | pub lobby_loaded_notify: Option>, 31 | } 32 | 33 | pub use crate::message::embed::{start_embed, FloEmbedClient, FloEmbedClientHandle}; 34 | pub use message::messages; 35 | 36 | #[cfg(feature = "ws")] 37 | pub use crate::message::ws::{start_ws, FloWsClient}; 38 | -------------------------------------------------------------------------------- /crates/lan/src/mdns/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, Result}; 2 | 3 | pub mod publisher; 4 | pub mod search; 5 | 6 | pub(crate) fn get_reg_type(game_version: &str) -> Result { 7 | let segments = game_version.split(".").collect::>(); 8 | if segments.len() != 4 { 9 | return Err(Error::InvalidVersionString(game_version.to_string())); 10 | } 11 | let major = segments[0] 12 | .parse::() 13 | .map_err(|_| Error::InvalidVersionString(game_version.to_string()))?; 14 | let major_offset = major - 1; 15 | let minor = segments[1] 16 | .parse::() 17 | .map_err(|_| Error::InvalidVersionString(game_version.to_string()))?; 18 | let num = format!("10{major_offset}{minor:02}") 19 | .parse::() 20 | .map_err(|_| Error::InvalidVersionString(game_version.to_string()))?; 21 | 22 | Ok(format!("_blizzard._udp,_w3xp{:x}", num)) 23 | } 24 | 25 | #[test] 26 | fn test_get_reg_type() { 27 | assert_eq!( 28 | get_reg_type("1.33.0.00000").unwrap(), 29 | "_blizzard._udp,_w3xp2731" 30 | ); 31 | assert_eq!( 32 | get_reg_type("1.34.0.00000").unwrap(), 33 | "_blizzard._udp,_w3xp2732" 34 | ); 35 | assert_eq!( 36 | get_reg_type("2.0.0.00000").unwrap(), 37 | "_blizzard._udp,_w3xp2774" 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /crates/controller/src/player/state/conn.rs: -------------------------------------------------------------------------------- 1 | use super::PlayerRegistry; 2 | use crate::client::PlayerSender; 3 | use crate::player::state::PlayerState; 4 | use flo_state::{async_trait, Context, Handler, Message}; 5 | 6 | pub struct Connect { 7 | pub game_id: Option, 8 | pub sender: PlayerSender, 9 | } 10 | 11 | impl Message for Connect { 12 | type Result = (); 13 | } 14 | 15 | #[async_trait] 16 | impl Handler for PlayerRegistry { 17 | async fn handle(&mut self, _: &mut Context, message: Connect) { 18 | let player_id = message.sender.player_id(); 19 | let removed = self.registry.insert( 20 | player_id, 21 | PlayerState::new(player_id, message.game_id, message.sender), 22 | ); 23 | if let Some(state) = removed { 24 | state.shutdown().await; 25 | } 26 | } 27 | } 28 | 29 | pub struct Disconnect { 30 | pub player_id: i32, 31 | } 32 | 33 | impl Message for Disconnect { 34 | type Result = (); 35 | } 36 | 37 | #[async_trait] 38 | impl Handler for PlayerRegistry { 39 | async fn handle(&mut self, _: &mut Context, message: Disconnect) { 40 | let player_id = message.player_id; 41 | if let Some(state) = self.registry.remove(&player_id) { 42 | state.shutdown().await; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/node/src/scheduler_fairness.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use prometheus::{register_histogram, Histogram}; 3 | use std::time::{Duration, Instant}; 4 | use tokio::time::sleep; 5 | 6 | pub static SCHEDULER_SLEEP_DIVERGENCE: Lazy = Lazy::new(|| { 7 | register_histogram!( 8 | "flonode_scheduler_sleep_divergence_seconds", 9 | "Divergence between expected and actual sleep time, measuring scheduler fairness", 10 | vec![ 11 | 0.0, 0.0001, 0.00025, 0.0005, 0.001, 0.0025, 0.005, 0.0075, 0.01, 0.015, 0.02, 0.025, 0.0375, 0.05, 0.075, 12 | 0.1, 0.25, 0.5, 1.0 13 | ] 14 | ) 15 | .unwrap() 16 | }); 17 | 18 | const TARGET_SLEEP_DURATION: Duration = Duration::from_millis(5); 19 | 20 | async fn monitor_scheduler_fairness() { 21 | loop { 22 | let start = Instant::now(); 23 | sleep(TARGET_SLEEP_DURATION).await; 24 | let elapsed = start.elapsed(); 25 | 26 | // Calculate divergence: actual sleep time minus expected sleep time 27 | let divergence = elapsed.saturating_sub(TARGET_SLEEP_DURATION); 28 | let divergence_secs = divergence.as_secs_f64(); 29 | 30 | SCHEDULER_SLEEP_DIVERGENCE.observe(divergence_secs); 31 | } 32 | } 33 | 34 | pub fn start_monitoring() { 35 | tokio::spawn(monitor_scheduler_fairness()); 36 | } 37 | -------------------------------------------------------------------------------- /crates/observer-edge/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("game not ready: {0}")] 6 | GameNotReady(String), 7 | #[error("game not found: {0}")] 8 | GameNotFound(i32), 9 | #[error("invalid game id: {0}")] 10 | InvalidGameId(i32), 11 | #[error("unexpected game records: {expected} << {range:?} {len}")] 12 | UnexpectedGameRecords { 13 | expected: u32, 14 | range: [u32; 2], 15 | len: usize, 16 | }, 17 | #[error("game version unknown")] 18 | GameVersionUnknown, 19 | #[error("peer lagged: {0} events dropped")] 20 | ObserverPeerLagged(u64), 21 | #[error("controller service: {0}")] 22 | ControllerService(tonic::Status), 23 | #[error("kinesis: {0}")] 24 | Kinesis(#[from] flo_kinesis::error::Error), 25 | #[error("w3gs: {0}")] 26 | W3GS(#[from] flo_w3gs::error::Error), 27 | #[error("io: {0}")] 28 | Io(#[from] std::io::Error), 29 | #[error("actor: {0}")] 30 | Actor(#[from] flo_state::error::Error), 31 | #[error("proto: {0}")] 32 | Proto(#[from] s2_grpc_utils::result::Error), 33 | #[error("net: {0}")] 34 | Net(#[from] flo_net::error::Error), 35 | #[error("observer archiver: {0}")] 36 | ObserverArchiver(#[from] flo_observer_archiver::error::Error), 37 | } 38 | 39 | pub type Result = std::result::Result; 40 | -------------------------------------------------------------------------------- /migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /crates/constants/src/lib.rs: -------------------------------------------------------------------------------- 1 | use crate::version::Version; 2 | pub mod version; 3 | 4 | pub const CONTROLLER_HOST: &str = "service.w3flo.com"; 5 | pub const STATS_HOST: &str = "stats.w3flo.com"; 6 | pub const CONTROLLER_GRPC_PORT: u16 = 3549; 7 | pub const CONTROLLER_SOCKET_PORT: u16 = 3550; 8 | pub const CLIENT_WS_PORT: u16 = 3551; 9 | pub const CLIENT_ORIGINS: &[&str] = &[ 10 | "http://localhost:3000", 11 | "https://w3flo.com", 12 | "https://asia.w3flo.com", 13 | "https://w3champions.com", 14 | ]; 15 | pub const NODE_PORT: u16 = 3552; 16 | pub const NODE_ECHO_PORT: u16 = 3552; 17 | pub const NODE_ECHO_PORT_OFFSET: u16 = NODE_ECHO_PORT - NODE_PORT; 18 | pub const NODE_CONTROLLER_PORT: u16 = 3553; 19 | pub const NODE_CONTROLLER_PORT_OFFSET: u16 = NODE_CONTROLLER_PORT - NODE_ECHO_PORT; 20 | pub const NODE_CLIENT_PORT: u16 = 3554; 21 | pub const NODE_CLIENT_PORT_OFFSET: u16 = NODE_CLIENT_PORT - NODE_ECHO_PORT; 22 | pub const NODE_HTTP_PORT: u16 = 3555; 23 | pub const NODE_HTTP_PORT_OFFSET: u16 = NODE_HTTP_PORT - NODE_ECHO_PORT; 24 | pub const MIN_FLO_VERSION: version::Version = Version { 25 | major: 0, 26 | minor: 9, 27 | patch: 2, 28 | }; 29 | pub const OBSERVER_GRPC_PORT: u16 = 3556; 30 | pub const OBSERVER_SOCKET_PORT: u16 = 3557; 31 | pub const OBSERVER_GRAPHQL_PORT: u16 = 3558; 32 | pub const OBSERVER_FAST_FORWARDING_SPEED: f64 = 3.; 33 | -------------------------------------------------------------------------------- /crates/w3gs/src/protocol/leave.rs: -------------------------------------------------------------------------------- 1 | use flo_util::{BinDecode, BinEncode}; 2 | 3 | use crate::protocol::constants::PacketTypeId; 4 | use crate::protocol::packet::PacketPayload; 5 | 6 | pub use crate::protocol::constants::LeaveReason; 7 | 8 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 9 | pub struct LeaveReq(LeaveReason); 10 | 11 | impl LeaveReq { 12 | pub fn new(reason: LeaveReason) -> Self { 13 | Self(reason) 14 | } 15 | 16 | pub fn reason(&self) -> LeaveReason { 17 | self.0 18 | } 19 | } 20 | 21 | impl PacketPayload for LeaveReq { 22 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::LeaveReq; 23 | } 24 | 25 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 26 | pub struct LeaveAck; 27 | 28 | impl PacketPayload for LeaveAck { 29 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::LeaveAck; 30 | } 31 | 32 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 33 | pub struct PlayerLeft { 34 | pub player_id: u8, 35 | pub reason: LeaveReason, 36 | } 37 | 38 | impl PacketPayload for PlayerLeft { 39 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::PlayerLeft; 40 | } 41 | 42 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 43 | pub struct PlayerKicked { 44 | pub reason: LeaveReason, 45 | } 46 | 47 | impl PacketPayload for PlayerKicked { 48 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::PlayerKicked; 49 | } 50 | -------------------------------------------------------------------------------- /binaries/flo-worker-ui/src/log.rs: -------------------------------------------------------------------------------- 1 | use tracing_subscriber::filter::LevelFilter; 2 | use tracing_subscriber::EnvFilter; 3 | 4 | #[cfg(debug_assertions)] 5 | pub fn init(debug: bool) { 6 | let filter = EnvFilter::from_default_env() 7 | // Set the base level when not matched by other directives to WARN. 8 | .add_directive(if debug { 9 | LevelFilter::DEBUG.into() 10 | } else { 11 | LevelFilter::WARN.into() 12 | }); 13 | 14 | tracing_subscriber::fmt().with_env_filter(filter).init(); 15 | } 16 | 17 | #[cfg(not(debug_assertions))] 18 | pub fn init(debug: bool) { 19 | use once_cell::sync::OnceCell; 20 | use tracing_appender::non_blocking::WorkerGuard; 21 | static INSTANCE: OnceCell = OnceCell::new(); 22 | 23 | let file_appender = tracing_appender::rolling::daily("flo-logs", "flo.log"); 24 | let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); 25 | 26 | INSTANCE.set(guard).unwrap(); 27 | 28 | let filter = EnvFilter::from_default_env() 29 | // Set the base level when not matched by other directives to WARN. 30 | .add_directive(if debug { 31 | LevelFilter::DEBUG.into() 32 | } else { 33 | LevelFilter::WARN.into() 34 | }); 35 | 36 | tracing_subscriber::fmt() 37 | .with_env_filter(filter) 38 | .with_writer(non_blocking) 39 | .with_ansi(false) 40 | .init(); 41 | } 42 | -------------------------------------------------------------------------------- /binaries/flo-worker/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | fn main() { 3 | use std::env::var; 4 | use std::fs; 5 | use std::path::Path; 6 | 7 | let src = include_str!("./resource.rc"); 8 | let path = Path::new(&var("OUT_DIR").unwrap()).join("resource.rc"); 9 | let version = var("CARGO_PKG_VERSION").unwrap(); 10 | 11 | let res = format!( 12 | r##"{src} 13 | 1 VERSIONINFO 14 | FILEVERSION {version2},0 15 | PRODUCTVERSION {version2},0 16 | {{ 17 | BLOCK "StringFileInfo" 18 | {{ 19 | BLOCK "040904b0" 20 | {{ 21 | VALUE "CompanyName", "Ke Xu" 22 | VALUE "FileDescription", "Flo Worker" 23 | VALUE "FileVersion", "{version}" 24 | VALUE "InternalName", "flo-worker" 25 | VALUE "LegalCopyright", "Flo 2020-2021. All rights reserved." 26 | VALUE "ProductName", "Flo Worker" 27 | VALUE "OriginalFilename", "flo-worker.exe" 28 | VALUE "ProductVersion", "{version}" 29 | VALUE "CompanyShortName", "Ke Xu" 30 | VALUE "ProductShortName", "Flo Worker" 31 | }} 32 | }} 33 | BLOCK "VarFileInfo" 34 | {{ 35 | VALUE "Translation", 0x409, 1200 36 | }} 37 | }} 38 | "##, 39 | src = src, 40 | version = version, 41 | version2 = version.replace(".", ",") 42 | ); 43 | 44 | fs::write(&path, &res).unwrap(); 45 | 46 | embed_resource::compile(path); 47 | } 48 | 49 | #[cfg(not(windows))] 50 | fn main() {} 51 | -------------------------------------------------------------------------------- /crates/client/src/controller/stream_test.rs: -------------------------------------------------------------------------------- 1 | use crate::controller::stream::*; 2 | use crate::controller::{ControllerClient, SendWs}; 3 | use crate::error::Result; 4 | use crate::node::UpdateNodes; 5 | use flo_state::mock::Mock; 6 | use std::time::Duration; 7 | use tokio::time::sleep; 8 | 9 | #[tokio::test] 10 | async fn test_controller_stream() { 11 | dotenv::dotenv().unwrap(); 12 | flo_log_subscriber::init_env_override("DEBUG"); 13 | let _token = flo_controller::player::token::create_player_token(1).unwrap(); 14 | 15 | async fn mock_handle_event(msg: ControllerEvent) { 16 | tracing::debug!("mock: ControllerStreamEvent: {:?}", msg); 17 | } 18 | 19 | async fn mock_handle_send_ws(msg: SendWs) { 20 | tracing::debug!("mock: SendWs: {:?}", msg.message); 21 | } 22 | 23 | async fn mock_handle_update_nodes(msg: UpdateNodes) -> Result<()> { 24 | tracing::debug!("mock: UpdateNodes: {:?}", msg); 25 | Ok(()) 26 | } 27 | 28 | let mut parent = Mock::::builder() 29 | .handle(mock_handle_event) 30 | .handle(mock_handle_send_ws) 31 | .handle(mock_handle_update_nodes) 32 | .build(); 33 | 34 | //TODO: ControllerStream takes 6 arguments, platform and nodes Addr 35 | //let s = ControllerStream::new(parent.addr(), 1, "127.0.0.1", token).start(); 36 | 37 | sleep(Duration::from_secs(1)).await; 38 | 39 | parent.shutdown().await.unwrap(); 40 | } 41 | -------------------------------------------------------------------------------- /crates/w3gs/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::protocol::constants::PacketTypeId; 4 | 5 | #[derive(Error, Debug)] 6 | pub enum Error { 7 | #[error("stream closed unexpectedly")] 8 | StreamClosed, 9 | #[error("Game stream timed out")] 10 | GameStreamTimeout, 11 | #[error("IPv6 is not supported")] 12 | Ipv6NotSupported, 13 | #[error("payload size overflow")] 14 | PayloadSizeOverflow, 15 | #[error("invalid packet length: {0}")] 16 | InvalidPacketLength(u16), 17 | #[error("invalid payload length: {0}")] 18 | InvalidPayloadLength(usize), 19 | #[error("invalid state: no header")] 20 | InvalidStateNoHeader, 21 | #[error("an interior nul byte was found")] 22 | InvalidStringNulByte(#[from] std::ffi::NulError), 23 | #[error("io: {0}")] 24 | Io(#[from] std::io::Error), 25 | #[error("unexpected bytes after payload: {0}")] 26 | ExtraPayloadBytes(usize), 27 | #[error("packet type id mismatch: expected `{expected:?}`, found `{found:?}`")] 28 | PacketTypeIdMismatch { 29 | expected: PacketTypeId, 30 | found: PacketTypeId, 31 | }, 32 | #[error("invalid checksum")] 33 | InvalidChecksum, 34 | #[error("bin decode: {0}")] 35 | BinDecode(#[from] flo_util::binary::BinDecodeError), 36 | #[error("protobuf decode: {0}")] 37 | ProtoBufDecode(#[from] prost::DecodeError), 38 | } 39 | 40 | pub type Result = std::result::Result; 41 | -------------------------------------------------------------------------------- /crates/net/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod codec; 2 | mod common; 3 | mod version; 4 | 5 | pub mod error; 6 | #[macro_use] 7 | pub mod packet; 8 | 9 | pub mod constants; 10 | pub mod listener; 11 | pub mod ping; 12 | pub mod stream; 13 | pub mod time; 14 | pub mod w3gs; 15 | 16 | pub mod proto { 17 | pub mod flo_common { 18 | #[allow(unused)] 19 | use serde::{Deserialize, Serialize}; 20 | include!(concat!(env!("OUT_DIR"), "/flo_common.rs")); 21 | } 22 | 23 | pub mod flo_connect { 24 | #[allow(unused)] 25 | use serde::{Deserialize, Serialize}; 26 | 27 | pub use super::flo_common::{Computer, Race, SlotClientStatus, SlotSettings, SlotStatus}; 28 | pub use super::flo_node::PacketClientUpdateSlotClientStatus; 29 | 30 | include!(concat!(env!("OUT_DIR"), "/flo_connect.rs")); 31 | } 32 | 33 | pub mod flo_node { 34 | #[allow(unused)] 35 | use serde::{Deserialize, Serialize}; 36 | 37 | pub use super::flo_common::{Computer, Race, SlotClientStatus, SlotSettings, SlotStatus}; 38 | 39 | include!(concat!(env!("OUT_DIR"), "/flo_node.rs")); 40 | } 41 | 42 | pub mod flo_observer { 43 | #[allow(unused)] 44 | use serde::{Deserialize, Serialize}; 45 | 46 | pub use super::flo_common::{SlotSettings, Version}; 47 | 48 | include!(concat!(env!("OUT_DIR"), "/flo_observer.rs")); 49 | } 50 | } 51 | 52 | pub mod connect; 53 | pub mod node; 54 | pub mod observer; 55 | -------------------------------------------------------------------------------- /local-init/init-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | while ! pg_isready -h $POSTGRES_HOST -p $POSTGRES_PORT; do 4 | echo "Waiting for postgres" 5 | sleep 0.5 6 | done 7 | 8 | /usr/local/cargo/bin/diesel --config-file /usr/local/flo/diesel.toml migration run 9 | 10 | # Now create the default API client, players and node 11 | PGPASSWORD=$POSTGRES_PASSWORD psql -h $POSTGRES_HOST -p $POSTGRES_PORT -d $POSTGRES_DB -U $POSTGRES_USER < = OnceCell::new(); 7 | 8 | fn get_db_handle() -> Result<&'static sled::Db> { 9 | if let Some(existing_handle) = SLED_DB.get() { 10 | Ok(existing_handle) 11 | } else { 12 | let sled = sled::open(SLED)?; 13 | SLED_DB.set(sled).map_err(|_| anyhow!("Failed to store db handle"))?; 14 | get_db_handle() 15 | } 16 | } 17 | 18 | pub fn read(target: &str) -> Result> { 19 | let sled = get_db_handle()?; 20 | if let Some(val) = sled.get(target)? { 21 | Ok(Some( 22 | String::from_utf8(val.to_vec())? 23 | )) 24 | } else { 25 | Ok(None) 26 | } 27 | } 28 | 29 | pub fn blacklisted() -> Result { 30 | let sled = get_db_handle()?; 31 | let mut result = vec![]; 32 | for key in sled.iter().keys() { 33 | if let Ok(k) = key { 34 | if let Ok(kk) = String::from_utf8(k.to_vec()) { 35 | result.push(kk); 36 | } 37 | } 38 | } 39 | Ok(result.join(", ")) 40 | } 41 | 42 | pub fn blacklist(target: &str, reason: &str) -> Result<()> { 43 | let sled = get_db_handle()?; 44 | sled.insert(target, reason)?; 45 | Ok(()) 46 | } 47 | 48 | pub fn unblacklist(target: &str) -> Result<()> { 49 | let sled = get_db_handle()?; 50 | sled.remove(target)?; 51 | Ok(()) 52 | } 53 | -------------------------------------------------------------------------------- /crates/client/src/observer/source/memory.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use flo_net::w3gs::W3GSPacketTypeId; 3 | use flo_observer::record::GameRecordData; 4 | use flo_w3gs::action::IncomingAction; 5 | use futures::Stream; 6 | use std::{ 7 | collections::VecDeque, 8 | pin::Pin, 9 | task::{Context, Poll}, 10 | }; 11 | 12 | pub struct MemorySource { 13 | records: VecDeque, 14 | } 15 | 16 | impl MemorySource { 17 | pub fn new(i: impl IntoIterator) -> Self { 18 | Self { 19 | records: i 20 | .into_iter() 21 | .collect(), 22 | } 23 | } 24 | 25 | pub fn remaining_millis(&self) -> u64 { 26 | let mut time = 0; 27 | for record in &self.records { 28 | if let GameRecordData::W3GS(pkt) = record { 29 | match pkt.type_id() { 30 | W3GSPacketTypeId::IncomingAction | W3GSPacketTypeId::IncomingAction2 => { 31 | let time_increment_ms = 32 | IncomingAction::peek_time_increment_ms(pkt.payload.as_ref()).ok().unwrap_or_default(); 33 | time += time_increment_ms as u64; 34 | }, 35 | _ => {} 36 | } 37 | } 38 | } 39 | time 40 | } 41 | } 42 | 43 | impl Stream for MemorySource { 44 | type Item = Result; 45 | 46 | fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { 47 | Poll::Ready(self.records.pop_front().map(Ok)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /crates/net/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | use crate::packet::PacketTypeId; 4 | use crate::w3gs::ParseW3GSPacketError; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum Error { 8 | #[error("payload too large")] 9 | PayloadTooLarge, 10 | #[error("payload too small")] 11 | PayloadTooSmall, 12 | #[error("stream timed out")] 13 | StreamTimeout, 14 | #[error("stream closed")] 15 | StreamClosed, 16 | #[error("unexpected packet type: expected {expected:?}, got {got:?}")] 17 | UnexpectedPacketType { 18 | expected: PacketTypeId, 19 | got: PacketTypeId, 20 | }, 21 | #[error("unexpected packet type: {got:?}")] 22 | UnexpectedPacketTypeId { got: PacketTypeId }, 23 | #[error("packet field not present")] 24 | PacketFieldNotPresent, 25 | #[error("task cancelled unexpectedly")] 26 | Cancelled, 27 | #[error("invalid W3GS frame")] 28 | ReadW3GSFrame(ParseW3GSPacketError), 29 | #[error("io: {0}")] 30 | Io(#[from] std::io::Error), 31 | #[error("decode: {0}")] 32 | Decode(#[from] flo_util::binary::BinDecodeError), 33 | #[error("protobuf decode: {0}")] 34 | ProtoBufDecode(#[from] prost::DecodeError), 35 | #[error("protobuf encode: {0}")] 36 | ProtoBufEncode(#[from] prost::EncodeError), 37 | } 38 | 39 | impl Error { 40 | pub fn unexpected_packet_type_id(got: PacketTypeId) -> Self { 41 | Self::UnexpectedPacketTypeId { got } 42 | } 43 | } 44 | 45 | pub type Result = std::result::Result; 46 | -------------------------------------------------------------------------------- /crates/lan/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("invalid version string: {0}")] 6 | InvalidVersionString(String), 7 | #[error("invalid game info: {0}")] 8 | InvalidGameInfo(&'static str), 9 | #[error("bonjour register: {0}")] 10 | BonjourRegister(std::io::Error), 11 | #[error("bonjour update: {0}")] 12 | BonjourUpdate(String), 13 | #[error("get hostname: {0}")] 14 | GetHostName(std::io::Error), 15 | #[error("couldn't find game info record in the replay file")] 16 | ReplayNoGameInfoRecord, 17 | #[error("the game info record in the replay file is invalid")] 18 | ReplayInvalidGameInfoRecord, 19 | #[error("string contains null byte")] 20 | NullByteInString, 21 | #[error("bin decode: {0}")] 22 | BinDecode(#[from] flo_util::binary::BinDecodeError), 23 | #[error("w3gs: {0}")] 24 | W3GS(#[from] flo_w3gs::error::Error), 25 | #[error("platform: {0}")] 26 | Platform(#[from] flo_platform::error::Error), 27 | #[error("protobuf encode: {0}")] 28 | ProtoBufEncode(#[from] prost::EncodeError), 29 | #[error("protobuf decode: {0}")] 30 | ProtoBufDecode(#[from] prost::DecodeError), 31 | #[error("base64 decode: {0}")] 32 | Base64Decode(#[from] base64::DecodeError), 33 | #[error("replay: {0}")] 34 | Replay(#[from] flo_w3replay::error::Error), 35 | #[error("io: {0}")] 36 | Io(#[from] std::io::Error), 37 | } 38 | 39 | pub type Result = std::result::Result; 40 | -------------------------------------------------------------------------------- /crates/observer-edge/src/broadcast.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | 3 | use tokio::sync::broadcast; 4 | use tokio_stream::{wrappers::BroadcastStream, Stream, StreamExt}; 5 | 6 | pub struct BroadcastSender { 7 | tx: broadcast::Sender, 8 | } 9 | 10 | impl BroadcastSender 11 | where 12 | E: Clone, 13 | { 14 | pub fn channel() -> (Self, BroadcastReceiver) { 15 | let (tx, rx) = broadcast::channel(16); 16 | (BroadcastSender { tx }, BroadcastReceiver { rx }) 17 | } 18 | 19 | pub fn send(&self, event: E) -> bool { 20 | self.tx.send(event).is_ok() 21 | } 22 | 23 | pub fn subscribe(&self) -> BroadcastReceiver { 24 | BroadcastReceiver { 25 | rx: self.tx.subscribe(), 26 | } 27 | } 28 | 29 | pub fn is_closed(&self) -> bool { 30 | self.tx.receiver_count() == 0 31 | } 32 | } 33 | 34 | pub struct BroadcastReceiver { 35 | rx: broadcast::Receiver, 36 | } 37 | 38 | impl Deref for BroadcastReceiver { 39 | type Target = broadcast::Receiver; 40 | fn deref(&self) -> &Self::Target { 41 | &self.rx 42 | } 43 | } 44 | 45 | impl DerefMut for BroadcastReceiver { 46 | fn deref_mut(&mut self) -> &mut Self::Target { 47 | &mut self.rx 48 | } 49 | } 50 | 51 | impl BroadcastReceiver { 52 | pub fn into_stream(self) -> impl Stream 53 | where 54 | E: Clone + Send + 'static, 55 | { 56 | BroadcastStream::new(self.rx).filter_map(|item| item.ok()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/node/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-node" 3 | version = "0.7.4" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-types = { path = "../types" } 9 | flo-util = { path = "../util" } 10 | flo-w3gs = { path = "../w3gs" } 11 | flo-net = { path = "../net" } 12 | flo-constants = { path = "../constants" } 13 | flo-event = { path = "../event" } 14 | flo-log = { path = "../log" } 15 | flo-task = { path = "../task" } 16 | flo-observer = { path = "../observer" } 17 | flo-state = "1" 18 | 19 | thiserror = "1.0" 20 | bytes = "1.2.1" 21 | futures = "0.3.24" 22 | tokio = { workspace = true, features = ["time", "sync", "macros", "net"] } 23 | tokio-stream = { workspace = true, features = ["time", "net"] } 24 | tokio-util = { workspace = true } 25 | tracing = "0.1" 26 | tracing-futures = "0.2" 27 | parking_lot = "0.11" 28 | s2-grpc-utils = "0.2" 29 | uuid = { version = "0.8", features = ["v4"] } 30 | hyper = { version = "1.0", features = ["server", "http2"] } 31 | hyper-util = { version = "0.1.1", features = ["server-auto", "server", "tokio"] } 32 | prometheus = "0.9" 33 | dashmap = "3.11" 34 | smallvec = "1.10" 35 | slab = "0.4" 36 | once_cell = "1.15" 37 | rusoto_core = "0.48.0" 38 | rusoto_kinesis = "0.48.0" 39 | backoff = "0.3" 40 | lru = "0.7" 41 | http-body-util = "0.1.0" 42 | 43 | [build-dependencies] 44 | flo-constants = { path = "../constants" } 45 | 46 | [dev-dependencies] 47 | rand = { version = "0.8", features = ["min_const_gen"] } 48 | -------------------------------------------------------------------------------- /crates/controller/src/state/actor_map.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use flo_state::{async_trait, Actor, Addr, Handler, Message}; 3 | 4 | use std::marker::PhantomData; 5 | 6 | pub struct GetActorEntry(K, PhantomData); 7 | 8 | impl GetActorEntry { 9 | pub fn key(&self) -> &K { 10 | &self.0 11 | } 12 | } 13 | 14 | impl Message for GetActorEntry 15 | where 16 | S: Actor, 17 | K: Send + 'static, 18 | { 19 | type Result = Option>; 20 | } 21 | 22 | #[async_trait] 23 | impl ActorMapExt for Addr 24 | where 25 | Parent: Actor + Handler>, 26 | Entry: Actor, 27 | K: Send + 'static, 28 | { 29 | async fn send_to(&self, key: K, message: M) -> Result 30 | where 31 | M: Message>, 32 | R: Send + 'static, 33 | Entry: Handler, 34 | { 35 | let addr = match self.send(GetActorEntry(key, PhantomData)).await { 36 | Ok(Some(v)) => v, 37 | Ok(None) => return Err(Error::ActorNotFound), 38 | Err(err) => return Err(err.into()), 39 | }; 40 | 41 | addr 42 | .send(message) 43 | .await 44 | .map_err(Error::from) 45 | .and_then(std::convert::identity) 46 | } 47 | } 48 | 49 | #[async_trait] 50 | pub trait ActorMapExt { 51 | async fn send_to(&self, key: K, message: M) -> Result 52 | where 53 | M: Message>, 54 | R: Send + 'static, 55 | S: Handler; 56 | } 57 | -------------------------------------------------------------------------------- /crates/observer-edge/src/env.rs: -------------------------------------------------------------------------------- 1 | use flo_observer::record::ObserverRecordSource; 2 | use once_cell::sync::Lazy; 3 | use std::env; 4 | 5 | #[derive(Debug)] 6 | pub struct Env { 7 | pub controller_url: String, 8 | pub controller_secret: String, 9 | pub record_source: ObserverRecordSource, 10 | pub record_backscan_secs: u64, 11 | pub jwt_secret_base64: String, 12 | pub aws_s3_region: Option, 13 | pub aws_s3_bucket: Option, 14 | pub aws_access_key_id: Option, 15 | pub aws_secret_access_key: Option, 16 | pub admin_secret: Option, 17 | } 18 | 19 | pub static ENV: Lazy = Lazy::new(|| { 20 | Env { 21 | controller_url: env::var("CONTROLLER_URL").expect("env CONTROLLER_URL"), 22 | controller_secret: env::var("CONTROLLER_SECRET").expect("env CONTROLLER_SECRET"), 23 | record_source: std::env::var("OBSERVER_CONSUMER_SOURCE") 24 | .ok() 25 | .and_then(|v| v.parse().ok()) 26 | .unwrap_or(ObserverRecordSource::Test), 27 | jwt_secret_base64: env::var("JWT_SECRET_BASE64").expect("env JWT_SECRET_BASE64"), 28 | record_backscan_secs: std::env::var("OBSERVER_BACKSCAN_SECS") 29 | .ok() 30 | .and_then(|v| v.parse().ok()) 31 | .unwrap_or(3600), 32 | aws_s3_region: env::var("AWS_S3_REGION").ok(), 33 | aws_s3_bucket: env::var("AWS_S3_BUCKET").ok(), 34 | aws_access_key_id: env::var("AWS_ACCESS_KEY_ID").ok(), 35 | aws_secret_access_key: env::var("AWS_SECRET_ACCESS_KEY").ok(), 36 | admin_secret: env::var("ADMIN_SECRET").ok(), 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /binaries/flo-worker-ui/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | 3 | mod core; 4 | mod flo; 5 | mod gui; 6 | mod log; 7 | 8 | use structopt::StructOpt; 9 | use tracing::instrument; 10 | 11 | use crate::core::*; 12 | 13 | #[instrument] 14 | pub fn main() { 15 | #[cfg(windows)] 16 | unsafe { 17 | winapi::um::processthreadsapi::SetPriorityClass( 18 | winapi::um::processthreadsapi::GetCurrentProcess(), 19 | winapi::um::winbase::ABOVE_NORMAL_PRIORITY_CLASS, 20 | ); 21 | } 22 | 23 | let mut opt = Default::default(); 24 | 25 | if let Ok(json_file) = std::fs::File::open(JSON_CONF_FNAME) { 26 | if let Ok(json_conf) = serde_json::from_reader::<_, Opt>(json_file) { 27 | opt = json_conf; 28 | } 29 | } 30 | 31 | //TODO: recode that part, not fully sure how 32 | let opt_from_args = Opt::from_args(); 33 | if opt_from_args.debug { 34 | opt.debug = true; 35 | } 36 | if let Some(token) = opt_from_args.token { 37 | opt.token = Some(token); 38 | } 39 | if let Some(installation_path) = opt_from_args.installation_path { 40 | opt.installation_path = Some(installation_path); 41 | } 42 | if let Some(user_data_path) = opt_from_args.user_data_path { 43 | opt.user_data_path = Some(user_data_path); 44 | } 45 | if let Some(controller_host) = opt_from_args.controller_host { 46 | opt.controller_host = Some(controller_host); 47 | } 48 | opt.ptr = opt_from_args.ptr; 49 | 50 | log::init(opt.debug); 51 | 52 | tracing::warn!("Running GUI"); 53 | 54 | gui::run(opt); 55 | } 56 | -------------------------------------------------------------------------------- /crates/controller/examples/node_request.rs: -------------------------------------------------------------------------------- 1 | use flo_controller::error::{Error, Result, TaskCancelledExt}; 2 | use flo_controller::node::messages::NodePlayerLeave; 3 | use flo_controller::node::{NodeConnActor, NodeConnConfig}; 4 | use flo_state::{mock::Mock, Actor}; 5 | use futures::TryFutureExt; 6 | use tokio_stream::StreamExt; 7 | 8 | #[tokio::main] 9 | async fn main() -> Result<()> { 10 | dotenv::dotenv().ok(); 11 | flo_log_subscriber::init_env_override("flo_controller=debug"); 12 | 13 | let game_registry_mock = Mock::builder().build(); 14 | 15 | let actor = NodeConnActor::new( 16 | NodeConnConfig { 17 | id: 0, 18 | addr: "127.0.0.1".to_string(), 19 | secret: "".to_string(), 20 | internal_address: None, 21 | }, 22 | game_registry_mock.addr(), 23 | ) 24 | .start(); 25 | 26 | let tasks: futures::stream::FuturesUnordered<_> = (0..1000) 27 | .into_iter() 28 | .map(|id| { 29 | let addr = actor.addr(); 30 | async move { 31 | let reply = addr 32 | .send(NodePlayerLeave { 33 | game_id: id, 34 | player_id: 0, 35 | }) 36 | .await??; 37 | 38 | let reply = reply.await.or_cancelled()?; 39 | 40 | tracing::debug!(id, "done: {:?}", reply); 41 | 42 | Ok::<_, Error>(reply) 43 | } 44 | .map_err(move |err| { 45 | tracing::error!(id, "error: {:?}", err); 46 | err 47 | }) 48 | }) 49 | .collect(); 50 | 51 | let results = tasks.collect::>().await; 52 | results.into_iter().collect::>>()?; 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /crates/net/src/node/mod.rs: -------------------------------------------------------------------------------- 1 | pub use crate::proto::flo_node::*; 2 | 3 | packet_type!(ControllerConnect, PacketControllerConnect); 4 | packet_type!(ControllerConnectAccept, PacketControllerConnectAccept); 5 | packet_type!(ControllerConnectReject, PacketControllerConnectReject); 6 | packet_type!(ControllerUpdateSlotStatus, PacketControllerUpdateSlotStatus); 7 | packet_type!( 8 | ControllerUpdateSlotStatusAccept, 9 | PacketControllerUpdateSlotStatusAccept 10 | ); 11 | packet_type!( 12 | ControllerUpdateSlotStatusReject, 13 | PacketControllerUpdateSlotStatusReject 14 | ); 15 | packet_type!(ControllerCreateGame, PacketControllerCreateGame); 16 | packet_type!(ControllerCreateGameAccept, PacketControllerCreateGameAccept); 17 | packet_type!(ControllerCreateGameReject, PacketControllerCreateGameReject); 18 | packet_type!(ControllerQueryGameStatus, PacketControllerQueryGameStatus); 19 | packet_type!(ClientConnect, PacketClientConnect); 20 | packet_type!(ClientConnectAccept, PacketClientConnectAccept); 21 | packet_type!(ClientConnectReject, PacketClientConnectReject); 22 | packet_type!(ClientHealthCheckResponse, PacketClientHealthCheckResponse); 23 | packet_type!( 24 | ClientUpdateSlotClientStatusRequest, 25 | PacketClientUpdateSlotClientStatusRequest 26 | ); 27 | packet_type!( 28 | ClientUpdateSlotClientStatus, 29 | PacketClientUpdateSlotClientStatus 30 | ); 31 | packet_type!( 32 | ClientUpdateSlotClientStatusReject, 33 | PacketClientUpdateSlotClientStatusReject 34 | ); 35 | packet_type!(NodeGameStatusUpdate, PacketNodeGameStatusUpdate); 36 | packet_type!(NodeGameStatusUpdateBulk, PacketNodeGameStatusUpdateBulk); 37 | -------------------------------------------------------------------------------- /crates/node/src/constants.rs: -------------------------------------------------------------------------------- 1 | use flo_observer::record::ObserverRecordSource; 2 | use once_cell::sync::Lazy; 3 | use std::time::Duration; 4 | 5 | pub const PEER_CHANNEL_SIZE: usize = 250; 6 | pub const CONTROLLER_SENDER_BUF_SIZE: usize = 10; 7 | pub const GAME_DISPATCH_BUF_SIZE: usize = 256; 8 | pub const GAME_PLAYER_LAGGING_THRESHOLD_MS: u32 = 3000; 9 | pub const GAME_PLAYER_MAX_ACK_QUEUE: usize = 300; 10 | pub static GAME_DEFAULT_STEP_MS: Lazy = Lazy::new(|| { 11 | std::env::var("FLO_GAME_STEP_MS") 12 | .ok() 13 | .and_then(|v| v.parse().ok()) 14 | .unwrap_or(30) 15 | }); 16 | pub const GAME_PING_INTERVAL: Duration = Duration::from_secs(1); 17 | pub const GAME_PING_TIMEOUT: Duration = Duration::from_secs(5); 18 | pub const GAME_CLOCK_MAX_PAUSE: Duration = Duration::from_secs(60 - 3); 19 | 20 | #[cfg(not(debug_assertions))] 21 | pub const GAME_DELAY_RANGE: [Duration; 2] = [Duration::from_millis(25), Duration::from_millis(100)]; 22 | #[cfg(debug_assertions)] 23 | pub const GAME_DELAY_RANGE: [Duration; 2] = 24 | [Duration::from_millis(25), Duration::from_millis(60 * 1000)]; 25 | 26 | pub const OBS_FLUSH_INTERVAL: Duration = Duration::from_secs(1); 27 | pub const OBS_CHANNEL_SIZE: usize = 10000; 28 | pub const OBS_MAX_CHUNK_SIZE: usize = 512 * 1024; 29 | pub static OBS_SOURCE: Lazy = Lazy::new(|| { 30 | std::env::var("OBSERVER_SOURCE") 31 | .ok() 32 | .and_then(|v| v.parse().ok()) 33 | .unwrap_or(ObserverRecordSource::Test) 34 | }); 35 | 36 | pub const RTT_STATS_REPORT_DELAY: Duration = std::time::Duration::from_secs(5); 37 | pub const RTT_STATS_REPORT_INTERVAL: Duration = std::time::Duration::from_secs(15); 38 | -------------------------------------------------------------------------------- /crates/controller/src/player/state/ping.rs: -------------------------------------------------------------------------------- 1 | use super::PlayerRegistry; 2 | 3 | use flo_state::{async_trait, Context, Handler, Message}; 4 | use flo_types::ping::PingStats; 5 | use std::collections::BTreeMap; 6 | 7 | #[derive(Debug)] 8 | pub struct UpdatePing { 9 | pub player_id: i32, 10 | pub ping_map: BTreeMap, 11 | } 12 | 13 | impl Message for UpdatePing { 14 | type Result = (); 15 | } 16 | 17 | #[async_trait] 18 | impl Handler for PlayerRegistry { 19 | async fn handle( 20 | &mut self, 21 | _: &mut Context, 22 | UpdatePing { 23 | player_id, 24 | ping_map, 25 | }: UpdatePing, 26 | ) { 27 | if let Some(state) = self.registry.get_mut(&player_id) { 28 | state.ping_map = ping_map; 29 | } 30 | } 31 | } 32 | 33 | pub struct GetPlayersPingSnapshot { 34 | pub players: Vec, 35 | } 36 | 37 | pub struct NodePlayersPingSnapshot { 38 | pub map: BTreeMap>, 39 | } 40 | 41 | impl Message for GetPlayersPingSnapshot { 42 | type Result = NodePlayersPingSnapshot; 43 | } 44 | 45 | #[async_trait] 46 | impl Handler for PlayerRegistry { 47 | async fn handle( 48 | &mut self, 49 | _: &mut Context, 50 | GetPlayersPingSnapshot { players }: GetPlayersPingSnapshot, 51 | ) -> ::Result { 52 | let mut map = BTreeMap::new(); 53 | for player_id in players { 54 | if let Some(stats) = self.registry.get(&player_id).map(|p| p.ping_map.clone()) { 55 | map.insert(player_id, stats.into_iter().collect()); 56 | } 57 | } 58 | NodePlayersPingSnapshot { map } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /binaries/flo-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-cli" 3 | version = "0.1.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-log-subscriber = { path = "../../crates/log-subscriber" } 9 | flo-controller = { path = "../../crates/controller" } 10 | flo-observer-edge = { path = "../../crates/observer-edge" } 11 | flo-observer-archiver = { path = "../../crates/observer-archiver" } 12 | flo-net = { path = "../../crates/net" } 13 | flo-lan = { path = "../../crates/lan" } 14 | flo-grpc = { path = "../../deps/flo-grpc" } 15 | flo-w3storage = { path = "../../crates/w3storage" } 16 | flo-w3map = { path = "../../crates/w3map" } 17 | flo-w3gs = { path = "../../crates/w3gs" } 18 | flo-client = { path = "../../crates/client", features = ["worker"] } 19 | flo-debug = { path = "../../crates/debug" } 20 | flo-observer = { path = "../../crates/observer" } 21 | flo-observer-fs = { path = "../../crates/observer-fs" } 22 | flo-kinesis = { path = "../../crates/kinesis" } 23 | flo-w3replay = { path = "../../crates/w3replay" } 24 | flo-util = { path = "../../crates/util" } 25 | flo-types = { path = "../../crates/types" } 26 | flo-replay = { path = "../../crates/replay" } 27 | 28 | anyhow = "1" 29 | tonic = "0.6" 30 | dotenv = "0.15" 31 | clap = "2.34" 32 | structopt = "0.3" 33 | tracing = "0.1" 34 | tracing-futures = "0.2" 35 | tokio = { workspace = true, features = ["macros", "signal"] } 36 | tokio-stream = { workspace = true } 37 | rand = "0.8" 38 | hex = "0.4" 39 | async-tungstenite = { version = "0.16.1", features = ["tokio-runtime"] } 40 | futures = "0.3.24" 41 | serde_json = "1" 42 | once_cell = "1.15" 43 | bytes = "1.2.1" 44 | s2-grpc-utils = "0.2" 45 | -------------------------------------------------------------------------------- /crates/controller/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-controller" 3 | version = "0.2.0" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | flo-w3gs = { path = "../w3gs" } 9 | flo-grpc = { path = "../../deps/flo-grpc" } 10 | flo-net = { path = "../net" } 11 | flo-constants = { path = "../constants" } 12 | flo-log = { path = "../log" } 13 | flo-task = { path = "../task" } 14 | flo-state = "1" 15 | flo-types = { path = "../types" } 16 | flo-otel = { path = "../otel" } 17 | 18 | thiserror = "1.0" 19 | serde = { version = "1", features = ["derive"] } 20 | chrono = { version = "0.4", features = ["serde"] } 21 | bs-diesel-utils = "0.1" 22 | s2-grpc-utils = "0.2" 23 | diesel = { version = "1.4", features = [ 24 | "postgres", 25 | "chrono", 26 | "32-column-tables", 27 | "serde_json", 28 | "uuid", 29 | "r2d2", 30 | "numeric", 31 | "chrono", 32 | ] } 33 | diesel_migrations = "1.4" 34 | serde_json = "1" 35 | tonic = "0.6" 36 | jsonwebtoken = "7.2" 37 | futures = "0.3.24" 38 | tokio = { workspace = true, features = ["time", "sync", "macros"] } 39 | tokio-stream = { workspace = true, features = ["time"] } 40 | tracing = "0.1" 41 | tracing-futures = "0.2" 42 | parking_lot = "0.11" 43 | dashmap = "3.11" 44 | prometheus = "0.9" 45 | backoff = { version = "0.3" } 46 | rand = "0.8" 47 | arc-swap = "1.5" 48 | anyhow = "1.0" 49 | once_cell = "1.15" 50 | tower = "0.4" 51 | tower-http = { version = "0.4", features = ["trace"] } 52 | http = "0.2.12" 53 | dns-lookup = "2.0.4" 54 | 55 | [dev-dependencies] 56 | dotenv = "0.15" 57 | flo-log-subscriber = { path = "../log-subscriber" } 58 | 59 | [build-dependencies] 60 | flo-constants = { path = "../constants" } 61 | -------------------------------------------------------------------------------- /crates/observer/src/token.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | use jsonwebtoken::errors::ErrorKind; 3 | use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 4 | use once_cell::sync::Lazy; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::error::*; 8 | 9 | // 15mins 10 | const TOKEN_EXPIRATION_SECS: i64 = 60 * 15; 11 | const TOKEN_SUB: &str = "flo-observer"; 12 | 13 | static JWT_SECRET_BASE64: Lazy = 14 | Lazy::new(|| std::env::var("JWT_SECRET_BASE64").expect("env JWT_SECRET_BASE64")); 15 | 16 | #[derive(Debug, Serialize, Deserialize)] 17 | pub struct ObserverToken { 18 | pub sub: String, 19 | pub game_id: i32, 20 | pub delay_secs: Option, 21 | pub exp: usize, 22 | } 23 | 24 | pub fn create_observer_token(game_id: i32, delay_secs: Option) -> Result { 25 | static ENCODING_KEY: Lazy = Lazy::new(|| { 26 | EncodingKey::from_base64_secret(&JWT_SECRET_BASE64).expect("DecodingKey::from_base64_secret") 27 | }); 28 | 29 | let exp = Utc::now().timestamp() + TOKEN_EXPIRATION_SECS; 30 | let claims = ObserverToken { 31 | sub: TOKEN_SUB.to_string(), 32 | game_id, 33 | delay_secs, 34 | exp: exp as usize, 35 | }; 36 | encode(&Header::default(), &claims, &ENCODING_KEY).map_err(Into::into) 37 | } 38 | 39 | pub fn validate_observer_token(token: &str) -> Result { 40 | let decoding_key = DecodingKey::from_base64_secret(&JWT_SECRET_BASE64)?; 41 | decode(token, &decoding_key, &Validation::default()) 42 | .map(|data| data.claims) 43 | .map_err(|e| match e.kind() { 44 | ErrorKind::ExpiredSignature => Error::ObserverTokenExpired, 45 | _ => e.into(), 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /binaries/flo-worker-ui/src/flo/mod.rs: -------------------------------------------------------------------------------- 1 | use super::Opt; 2 | 3 | use anyhow::Result; 4 | use flo_client::StartConfig; 5 | use tokio::runtime::Runtime; 6 | use tokio::sync::Mutex; 7 | 8 | use once_cell::sync::Lazy; 9 | 10 | static RT: Lazy> = Lazy::new(|| Mutex::new(Runtime::new().unwrap())); 11 | 12 | async fn run_flo() -> Result { 13 | let rt = RT.lock().await; 14 | let client = rt.block_on(flo_client::start_ws(Default::default()))?; 15 | let port = client.port(); 16 | rt.spawn(client.serve()); 17 | Ok(port) 18 | } 19 | 20 | async fn run_flo_worker(opt: Opt) -> Result { 21 | let rt = RT.lock().await; 22 | let client = rt.block_on(flo_client::start_ws(StartConfig { 23 | token: opt.token, 24 | installation_path: opt.installation_path, 25 | user_data_path: opt.user_data_path, 26 | controller_host: opt.controller_host.clone(), 27 | ptr: opt.ptr, 28 | ..Default::default() 29 | }))?; 30 | let port = client.port(); 31 | rt.spawn(client.serve()); 32 | Ok(port) 33 | } 34 | 35 | pub async fn perform_run_flo(opt: Opt) -> (bool, String) { 36 | let res = if opt.use_flo_web { 37 | run_flo() 38 | .await 39 | .map_err(|err| anyhow::format_err!("Start flo failed: {:?}", err)) 40 | } else { 41 | run_flo_worker(opt) 42 | .await 43 | .map_err(|err| anyhow::format_err!("Start flo worker failed: {:?}", err)) 44 | }; 45 | 46 | match res { 47 | Ok(port) => { 48 | tracing::info!("running on port: {}", port); 49 | (true, port.to_string()) 50 | } 51 | Err(err) => { 52 | tracing::error!("failed to run flo clinet: {:?}", err); 53 | (false, err.to_string()) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/controller/src/player/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod conn; 2 | pub mod ping; 3 | pub mod sender; 4 | 5 | use crate::client::PlayerSender; 6 | use crate::error::Error; 7 | use crate::state::Data; 8 | use flo_state::{async_trait, Actor, RegistryRef, Service}; 9 | use flo_types::ping::PingStats; 10 | 11 | use crate::player::state::sender::PlayerFrames; 12 | use std::collections::BTreeMap; 13 | 14 | #[derive(Debug)] 15 | pub struct PlayerRegistry { 16 | registry: BTreeMap, 17 | } 18 | 19 | impl PlayerRegistry { 20 | pub fn new() -> Self { 21 | Self { 22 | registry: Default::default(), 23 | } 24 | } 25 | } 26 | 27 | impl Actor for PlayerRegistry {} 28 | 29 | #[async_trait] 30 | impl Service for PlayerRegistry { 31 | type Error = Error; 32 | 33 | async fn create(_registry: &mut RegistryRef) -> Result { 34 | Ok(PlayerRegistry::new()) 35 | } 36 | } 37 | 38 | #[derive(Debug)] 39 | pub struct PlayerState { 40 | pub player_id: i32, 41 | pub ping_map: BTreeMap, 42 | pub game_id: Option, 43 | pub sender: PlayerSender, 44 | } 45 | 46 | impl PlayerState { 47 | fn new(player_id: i32, game_id: Option, sender: PlayerSender) -> PlayerState { 48 | Self { 49 | player_id, 50 | game_id, 51 | ping_map: Default::default(), 52 | sender, 53 | } 54 | } 55 | 56 | fn try_send_frames(&mut self, frames: PlayerFrames) -> bool { 57 | for frame in frames { 58 | if !self.sender.try_send(frame) { 59 | return false; 60 | } 61 | } 62 | true 63 | } 64 | 65 | async fn shutdown(mut self) { 66 | self.sender.disconnect_multi().await; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/release-docker.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - "develop" 5 | 6 | name: Release Docker 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | image: [controller, node, stats] 15 | 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v2 19 | with: 20 | submodules: 'recursive' 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v1 24 | - name: Set up Docker Buildx 25 | uses: docker/setup-buildx-action@v1 26 | - name: Login to DockerHub 27 | uses: docker/login-action@v1 28 | with: 29 | username: fluxxu 30 | password: ${{ secrets.DOCKER_ACCESS_TOKEN }} 31 | - name: Login to Private Registry 32 | uses: docker/login-action@v1 33 | with: 34 | registry: docker-registry.w3champions.com 35 | username: test #${{ secrets.PRIVATE_REGISTRY_USERNAME }} 36 | password: test #${{ secrets.PRIVATE_REGISTRY_PASSWORD }} 37 | 38 | - name: Build and push - flo-${{ matrix.image }} 39 | uses: docker/build-push-action@v2 40 | with: 41 | push: true 42 | tags: fluxxu/flo-${{ matrix.image }}:${{ github.sha }},fluxxu/flo-${{ matrix.image }}:latest,docker-registry.w3champions.com/flo-${{ matrix.image }}:${{ github.sha }},docker-registry.w3champions.com/flo-${{ matrix.image }}:latest 43 | file: './build/${{ matrix.image }}.Dockerfile' 44 | cache-from: type=registry,ref=fluxxu/flo-${{ matrix.image }}:buildcache 45 | cache-to: type=registry,ref=fluxxu/flo-${{ matrix.image }}:buildcache,mode=max -------------------------------------------------------------------------------- /crates/w3c/src/types/w3c.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize)] 4 | pub struct Season { 5 | pub id: u32, 6 | } 7 | 8 | #[derive(Deserialize)] 9 | pub struct PlayerId { 10 | pub name: String, 11 | pub battleTag: String 12 | } 13 | 14 | #[derive(Deserialize)] 15 | pub struct W3CPlayer { 16 | pub playerIds: Vec, 17 | pub name: String, 18 | pub id: String, 19 | pub mmr: u32, 20 | pub gateWay: u32, 21 | pub gameMode: u32, 22 | pub season: u32, 23 | pub wins: u32, 24 | pub losses: u32, 25 | pub games: u32, 26 | pub winrate: f64 27 | } 28 | 29 | #[derive(Deserialize)] 30 | pub struct Search { 31 | pub gateway: u32, 32 | pub id: String, 33 | pub league: u32, 34 | pub rankNumber: u32, 35 | pub rankingPoints: u32, 36 | pub playerId: String, 37 | pub player: W3CPlayer, 38 | pub gameMode: u32, 39 | pub season: u32 40 | } 41 | 42 | #[derive(Deserialize)] 43 | pub struct RankingPointsProgress { 44 | pub rankingPoints: i32, 45 | pub mmr: i32 46 | } 47 | 48 | #[derive(Deserialize)] 49 | pub struct GMStats { 50 | pub race: Option, 51 | pub division: u32, 52 | pub gameMode: u32, 53 | pub games: u32, 54 | pub gateWay: u32, 55 | pub id: String, 56 | pub leagueId: u32, 57 | pub leagueOrder: u32, 58 | pub losses: u32, 59 | pub mmr: u32, 60 | pub playerIds: Vec, 61 | pub rank: u32, 62 | pub rankingPoints: u32, 63 | pub rankingPointsProgress: RankingPointsProgress, 64 | pub season: u32, 65 | pub winrate: f64, 66 | pub wins: u32 67 | } 68 | 69 | #[derive(Deserialize)] 70 | pub struct Stats { 71 | pub race: u32, 72 | pub gateWay: u32, 73 | pub id: String, 74 | pub wins: u32, 75 | pub losses: u32, 76 | pub games: u32, 77 | pub winrate: f64, 78 | } 79 | -------------------------------------------------------------------------------- /crates/node/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::game::{AckError, SlotClientStatus}; 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | pub enum Error { 6 | #[error("cancelled")] 7 | Cancelled, 8 | #[error("game exists")] 9 | GameExists, 10 | #[error("game desync: {0:?}")] 11 | GameDesync(#[from] AckError), 12 | #[error("game has no player")] 13 | NoPlayer, 14 | #[error("player busy: {0}")] 15 | PlayerBusy(i32), 16 | #[error("player not found in game")] 17 | PlayerNotFoundInGame, 18 | #[error("player connection exists")] 19 | PlayerConnectionExists, 20 | #[error("player channel broken")] 21 | PlayerChannelBroken, 22 | #[error("player already left")] 23 | PlayerAlreadyLeft, 24 | #[error("invalid player slot client status: {0:?}")] 25 | InvalidPlayerSlotClientStatus(SlotClientStatus), 26 | #[error("invalid slot id")] 27 | InvalidSlotId, 28 | #[error("invalid secret")] 29 | InvalidSecret, 30 | #[error("invalid token")] 31 | InvalidToken, 32 | #[error("invalid client status transition: {0:?} => {1:?}")] 33 | InvalidClientStatusTransition(SlotClientStatus, SlotClientStatus), 34 | #[error("observer put record: {0}")] 35 | ObsPutRecord(#[from] rusoto_core::RusotoError), 36 | #[error("tokio io: {0}")] 37 | Tokio(#[from] tokio::io::Error), 38 | #[error("operation timeout")] 39 | Timeout(#[from] tokio::time::error::Elapsed), 40 | #[error("w3gs: {0}")] 41 | W3GS(#[from] flo_w3gs::error::Error), 42 | #[error("net: {0}")] 43 | Net(#[from] flo_net::error::Error), 44 | #[error("proto: {0}")] 45 | Proto(#[from] s2_grpc_utils::result::Error), 46 | #[error("http: {0}")] 47 | Http(#[from] hyper::Error), 48 | } 49 | 50 | pub type Result = std::result::Result; 51 | -------------------------------------------------------------------------------- /binaries/flo-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | mod client; 4 | mod env; 5 | mod game; 6 | mod grpc; 7 | mod kinesis; 8 | mod lan; 9 | mod map; 10 | mod observer; 11 | mod replay; 12 | mod server; 13 | 14 | pub use anyhow::Result; 15 | 16 | #[derive(Debug, StructOpt)] 17 | enum Opt { 18 | Client { 19 | player_id: i32, 20 | #[structopt(subcommand)] 21 | cmd: client::Command, 22 | }, 23 | Server { 24 | #[structopt(subcommand)] 25 | cmd: server::Command, 26 | }, 27 | Lan { 28 | #[structopt(subcommand)] 29 | cmd: lan::Command, 30 | }, 31 | Observer { 32 | #[structopt(subcommand)] 33 | cmd: observer::Command, 34 | }, 35 | Kinesis { 36 | #[structopt(subcommand)] 37 | cmd: kinesis::Command, 38 | }, 39 | Replay { 40 | #[structopt(subcommand)] 41 | cmd: replay::Command, 42 | }, 43 | Map { 44 | #[structopt(subcommand)] 45 | cmd: map::Command, 46 | }, 47 | } 48 | 49 | #[tokio::main] 50 | async fn main() -> Result<()> { 51 | dotenv::dotenv().ok(); 52 | // flo_log_subscriber::init_env_override("debug,h2=error,async_dnssd=error"); 53 | flo_log_subscriber::init(); 54 | 55 | let opt = Opt::from_args(); 56 | 57 | match opt { 58 | Opt::Client { player_id, cmd } => { 59 | cmd.run(player_id).await?; 60 | } 61 | Opt::Server { cmd } => { 62 | cmd.run().await?; 63 | } 64 | Opt::Lan { cmd } => { 65 | cmd.run().await?; 66 | } 67 | Opt::Observer { cmd } => { 68 | cmd.run().await?; 69 | } 70 | Opt::Kinesis { cmd } => { 71 | cmd.run().await?; 72 | } 73 | Opt::Replay { cmd } => { 74 | cmd.run().await?; 75 | } 76 | Opt::Map { cmd } => { 77 | cmd.run().await?; 78 | } 79 | } 80 | 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /crates/util/src/dword_string.rs: -------------------------------------------------------------------------------- 1 | use crate::{BinDecode, BinEncode}; 2 | use std::fmt; 3 | 4 | #[derive(Clone, Copy, BinEncode, BinDecode)] 5 | #[bin(mod_path = "crate::binary")] 6 | pub struct DwordString { 7 | bytes: [u8; 4], 8 | } 9 | 10 | impl DwordString { 11 | pub fn new(bstr: &[u8; 4]) -> Self { 12 | DwordString { 13 | bytes: [bstr[3], bstr[2], bstr[1], bstr[0]], 14 | } 15 | } 16 | 17 | pub fn as_bytes(&self) -> &[u8; 4] { 18 | &self.bytes 19 | } 20 | 21 | pub fn from_bytes(bytes: [u8; 4]) -> Self { 22 | DwordString { bytes } 23 | } 24 | 25 | pub fn to_string(&self) -> String { 26 | self 27 | .bytes 28 | .iter() 29 | .rev() 30 | .cloned() 31 | .filter_map(|byte| { 32 | if byte != 0 { 33 | Some(char::from(byte)) 34 | } else { 35 | None 36 | } 37 | }) 38 | .collect() 39 | } 40 | } 41 | 42 | impl fmt::Display for DwordString { 43 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 44 | write!(f, "'{}'", self.to_string()) 45 | } 46 | } 47 | 48 | impl fmt::Debug for DwordString { 49 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 50 | write!(f, "'{}'", self.to_string()) 51 | } 52 | } 53 | 54 | impl<'a> PartialEq<&'a [u8; 4]> for DwordString { 55 | fn eq(&self, other: &&'a [u8; 4]) -> bool { 56 | self.bytes[0] == other[3] 57 | && self.bytes[1] == other[2] 58 | && self.bytes[2] == other[1] 59 | && self.bytes[3] == other[0] 60 | } 61 | } 62 | 63 | #[test] 64 | fn test_dword_string() { 65 | assert_eq!(DwordString::new(b"W3XP").as_bytes(), &[80_u8, 88, 51, 87]); 66 | assert_eq!( 67 | DwordString::from_bytes([80_u8, 88, 51, 87]).to_string(), 68 | "W3XP" 69 | ); 70 | assert_eq!(DwordString::new(b"W3XP"), b"W3XP"); 71 | } 72 | -------------------------------------------------------------------------------- /crates/controller/src/client/sender.rs: -------------------------------------------------------------------------------- 1 | use flo_net::packet::*; 2 | use flo_net::proto::flo_connect::*; 3 | use std::time::Duration; 4 | use tokio::sync::mpsc::{channel, Receiver, Sender}; 5 | 6 | use crate::error::*; 7 | 8 | pub type PlayerReceiver = Receiver; 9 | pub enum PlayerSenderMessage { 10 | Frame(Frame), 11 | Disconnect(ClientDisconnectReason), 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct PlayerSender { 16 | player_id: i32, 17 | sender: Sender, 18 | } 19 | 20 | impl PlayerSender { 21 | pub fn new(player_id: i32) -> (Self, PlayerReceiver) { 22 | let (sender, receiver) = channel(8); 23 | (PlayerSender { player_id, sender }, receiver) 24 | } 25 | 26 | pub fn player_id(&self) -> i32 { 27 | self.player_id 28 | } 29 | 30 | pub async fn disconnect_multi(&mut self) { 31 | self.disconnect(ClientDisconnectReason::Multi).await; 32 | } 33 | 34 | #[tracing::instrument] 35 | async fn disconnect(&mut self, reason: ClientDisconnectReason) { 36 | self 37 | .sender 38 | .send_timeout( 39 | PlayerSenderMessage::Disconnect(reason), 40 | Duration::from_secs(3), 41 | ) 42 | .await 43 | .ok(); 44 | } 45 | 46 | pub fn try_send(&mut self, frame: Frame) -> bool { 47 | self 48 | .sender 49 | .try_send(PlayerSenderMessage::Frame(frame)) 50 | .is_ok() 51 | } 52 | 53 | pub async fn send_frame(&mut self, frame: Frame) -> Result<()> { 54 | self 55 | .sender 56 | .send(PlayerSenderMessage::Frame(frame)) 57 | .await 58 | .map_err(|_| Error::PlayerStreamClosed)?; 59 | Ok(()) 60 | } 61 | 62 | pub async fn send(&mut self, packet: T) -> Result<()> { 63 | self.send_frame(packet.encode_as_frame()?).await?; 64 | Ok(()) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/controller/src/map/db.rs: -------------------------------------------------------------------------------- 1 | use diesel::prelude::*; 2 | use s2_grpc_utils::S2ProtoUnpack; 3 | use serde::Deserialize; 4 | 5 | use crate::db::DbConn; 6 | use crate::error::*; 7 | use crate::schema::map_checksum; 8 | 9 | pub fn search_checksum(conn: &DbConn, sha1: String) -> Result> { 10 | use map_checksum::dsl; 11 | let value = map_checksum::table 12 | .filter(dsl::sha1.eq(sha1)) 13 | .select(dsl::checksum) 14 | .first::>(conn) 15 | .optional()? 16 | .and_then(|bytes| { 17 | if bytes.len() == 4 { 18 | let mut b = [0_u8; 4]; 19 | b.copy_from_slice(&bytes[0..4]); 20 | Some(u32::from_le_bytes(b)) 21 | } else { 22 | None 23 | } 24 | }); 25 | Ok(value) 26 | } 27 | 28 | #[derive(Debug, Deserialize, S2ProtoUnpack)] 29 | #[s2_grpc(message_type = "flo_grpc::game::MapChecksumImportItem")] 30 | pub struct ImportItem { 31 | pub sha1: String, 32 | pub checksum: u32, 33 | } 34 | 35 | pub fn import(conn: &DbConn, mut items: Vec) -> Result { 36 | use diesel::pg::upsert::excluded; 37 | use map_checksum::dsl; 38 | 39 | items.sort_by_cached_key(|i| i.sha1.clone()); 40 | items.dedup_by(|a, b| a.sha1 == b.sha1); 41 | 42 | let inserts: Vec<_> = items 43 | .iter() 44 | .map(|item| Insert { 45 | sha1: item.sha1.as_ref(), 46 | checksum: item.checksum.to_le_bytes().to_vec(), 47 | }) 48 | .collect(); 49 | 50 | diesel::insert_into(map_checksum::table) 51 | .values(inserts) 52 | .on_conflict(dsl::sha1) 53 | .do_update() 54 | .set(dsl::checksum.eq(excluded(dsl::checksum))) 55 | .execute(conn) 56 | .map_err(Into::into) 57 | } 58 | 59 | #[derive(Debug, Insertable)] 60 | #[table_name = "map_checksum"] 61 | struct Insert<'a> { 62 | sha1: &'a str, 63 | checksum: Vec, 64 | } 65 | -------------------------------------------------------------------------------- /migrations/2020-07-11-023655_initial/up.sql: -------------------------------------------------------------------------------- 1 | create table player ( 2 | id serial not null primary key, 3 | name text not null, 4 | source integer not null, 5 | source_id text not null, 6 | source_state jsonb, 7 | realm text, 8 | created_at timestamp with time zone default now() not null, 9 | updated_at timestamp with time zone default now() not null, 10 | unique(source, source_id) 11 | ); 12 | SELECT diesel_manage_updated_at('player'); 13 | 14 | create index player_source_id on player(source, source_id); 15 | 16 | create table game ( 17 | id serial not null primary key, 18 | name text not null, 19 | map_name text not null, 20 | status integer not null default 0, 21 | node jsonb, 22 | is_private boolean not null, 23 | secret integer, 24 | is_live boolean not null, 25 | max_players integer not null, 26 | created_by integer references player(id), 27 | started_at timestamp with time zone, 28 | ended_at timestamp with time zone, 29 | meta jsonb not null, 30 | created_at timestamp with time zone default now() not null, 31 | updated_at timestamp with time zone default now() not null 32 | ); 33 | SELECT diesel_manage_updated_at('game'); 34 | 35 | create index game_status on game(status); 36 | 37 | create table node ( 38 | id serial not null primary key, 39 | name text not null, 40 | location text not null, 41 | secret text not null, 42 | ip_addr text not null, 43 | created_at timestamp with time zone default now() not null, 44 | updated_at timestamp with time zone default now() not null 45 | ); 46 | SELECT diesel_manage_updated_at('node'); 47 | 48 | create table api_client ( 49 | id serial not null primary key, 50 | name text not null, 51 | secret_key text not null, 52 | created_at timestamp with time zone default now() not null 53 | ); -------------------------------------------------------------------------------- /crates/platform/src/windows_bindings.rs: -------------------------------------------------------------------------------- 1 | use winapi::shared::minwindef::*; 2 | use winapi::um::winnt::*; 3 | 4 | #[allow(unused)] 5 | extern "C" { 6 | pub fn get_last_error() -> u32; 7 | pub fn get_version(file_name: LPCWSTR, out: *mut DWORD) -> bool; 8 | pub fn get_process_path_by_window_title( 9 | title: LPCWSTR, 10 | buffer: *mut u16, 11 | buffer_len: u32, 12 | ) -> GetProcessPathByWindowTitleResult; 13 | } 14 | 15 | #[allow(unused)] 16 | #[repr(u32)] 17 | #[derive(Debug, Copy, Clone, PartialEq)] 18 | pub enum GetProcessPathByWindowTitleResult { 19 | Ok = 0, 20 | WindowNotFound = 1, 21 | GetWindowThreadProcessId = 2, 22 | OpenProcess = 3, 23 | GetModuleFileNameExW = 4, 24 | } 25 | 26 | #[test] 27 | fn test_get_version() { 28 | use std::os::windows::ffi::OsStrExt; 29 | use std::path::PathBuf; 30 | let mut path: Vec = 31 | PathBuf::from(r#"C:\Program Files (x86)\Warcraft III\_retail_\x86_64\Warcraft III.exe"#) 32 | .as_os_str() 33 | .encode_wide() 34 | .collect(); 35 | path.push(0); 36 | let mut out: [u32; 4] = [0; 4]; 37 | let v = unsafe { get_version(path.as_ptr() as *const _, out.as_mut_ptr()) }; 38 | dbg!(v, out); 39 | } 40 | 41 | #[test] 42 | fn test_get_process_path_by_window_title() { 43 | use std::ffi::OsString; 44 | use std::os::windows::ffi::OsStrExt; 45 | use widestring::U16CString; 46 | let mut out = Vec::::with_capacity(256); 47 | out.resize(256, 0); 48 | let title = OsString::from("Warcraft III".to_string()); 49 | let mut title: Vec = title.encode_wide().collect(); 50 | title.push(0); 51 | let r = 52 | unsafe { get_process_path_by_window_title(title.as_ptr() as *const _, out.as_mut_ptr(), 256) }; 53 | dbg!(r); 54 | 55 | let path = unsafe { U16CString::from_ptr_str(out.as_ptr()) }.to_os_string(); 56 | 57 | dbg!(std::path::PathBuf::from(path)); 58 | } 59 | -------------------------------------------------------------------------------- /binaries/flo-cli/src/replay.rs: -------------------------------------------------------------------------------- 1 | use flo_w3replay::replay::ReplayDecoder; 2 | use std::path::PathBuf; 3 | use structopt::StructOpt; 4 | 5 | use crate::Result; 6 | 7 | #[derive(Debug, StructOpt)] 8 | pub enum Command { 9 | DumpHeader { path: PathBuf }, 10 | DumpGameInfo { path: PathBuf }, 11 | DumpSlotInfo { path: PathBuf }, 12 | } 13 | 14 | impl Command { 15 | pub async fn run(&self) -> Result<()> { 16 | match *self { 17 | Command::DumpHeader { ref path } => { 18 | let d = ReplayDecoder::new(std::fs::File::open(path)?)?; 19 | println!("{:#?}", d.header()) 20 | } 21 | Command::DumpGameInfo { ref path } => { 22 | let d = ReplayDecoder::new(std::fs::File::open(path)?)?; 23 | for r in d.into_records() { 24 | let r = r?; 25 | match r { 26 | flo_w3replay::Record::GameInfo(game) => { 27 | println!("{:#?}", game) 28 | } 29 | _ => {} 30 | } 31 | } 32 | } 33 | Command::DumpSlotInfo { ref path } => { 34 | let d = ReplayDecoder::new(std::fs::File::open(path)?)?; 35 | for r in d.into_records() { 36 | let r = r?; 37 | match r { 38 | flo_w3replay::Record::SlotInfo(slot) => { 39 | // println!("{:#?}", slot); 40 | println!("#\tCOLOR\tTEAM\tSTATUS\tTYPE"); 41 | for (i, slot) in slot.slots().into_iter().enumerate() { 42 | println!( 43 | "{}\t{}\t{}\t{:?}\t{}", 44 | i, 45 | slot.color, 46 | slot.team, 47 | slot.slot_status, 48 | if slot.computer { "COMPUTER" } else { "PLAYER" } 49 | ); 50 | } 51 | } 52 | _ => {} 53 | } 54 | } 55 | } 56 | } 57 | Ok(()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/controller/src/game/token.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | use jsonwebtoken::errors::ErrorKind; 3 | use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 4 | use once_cell::sync::Lazy; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::error::*; 8 | 9 | const TOKEN_EXPIRATION_SECS: i64 = 15 * 60; 10 | const TOKEN_SUB: &str = "flo"; 11 | 12 | #[derive(Debug, Serialize, Deserialize)] 13 | pub struct JoinToken { 14 | pub sub: String, 15 | pub game_id: i32, 16 | pub exp: usize, 17 | } 18 | 19 | pub fn create_join_token(game_id: i32) -> Result { 20 | static ENCODING_KEY: Lazy = Lazy::new(|| { 21 | EncodingKey::from_base64_secret(&crate::config::JWT_SECRET_BASE64) 22 | .expect("DecodingKey::from_base64_secret") 23 | }); 24 | 25 | let exp = Utc::now().timestamp() + TOKEN_EXPIRATION_SECS; 26 | let claims = JoinToken { 27 | sub: TOKEN_SUB.to_string(), 28 | game_id, 29 | exp: exp as usize, 30 | }; 31 | encode(&Header::default(), &claims, &ENCODING_KEY).map_err(Into::into) 32 | } 33 | 34 | pub fn validate_join_token(token: &str) -> Result { 35 | let decoding_key = DecodingKey::from_base64_secret(&crate::config::JWT_SECRET_BASE64)?; 36 | decode(token, &decoding_key, &Validation::default()) 37 | .map(|data| data.claims) 38 | .map_err(|e| match e.kind() { 39 | ErrorKind::ExpiredSignature => Error::JoinTokenExpired, 40 | _ => e.into(), 41 | }) 42 | } 43 | 44 | #[test] 45 | fn test_join_token() { 46 | // This JWT_SECRET is not an actual secret used anywhere. But we need to set one to create a player token. 47 | std::env::set_var( 48 | "JWT_SECRET_BASE64", 49 | "dd7bb476554a8f3980ff95dd1c6da1665fd8b77f42e71b558402f5a3aecec98e", 50 | ); 51 | let token = create_join_token(100).unwrap(); 52 | let token = validate_join_token(&token).unwrap(); 53 | dbg!(token); 54 | } 55 | -------------------------------------------------------------------------------- /crates/net/src/proto/observer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package flo_observer; 3 | 4 | import "proto/common.proto"; 5 | import "google/protobuf/wrappers.proto"; 6 | 7 | message PacketObserverConnect { 8 | flo_common.Version version = 1; 9 | string token = 2; 10 | } 11 | 12 | message PacketObserverConnectAccept { 13 | flo_common.Version version = 1; 14 | GameInfo game = 2; 15 | google.protobuf.Int64Value delay_secs = 3; 16 | } 17 | 18 | message PacketObserverConnectReject { 19 | ObserverConnectRejectReason reason = 1; 20 | google.protobuf.Int64Value delay_ends_at = 2; 21 | } 22 | 23 | message PacketObserverPasswordRequest { 24 | // Request password from client 25 | } 26 | 27 | message PacketObserverPasswordResponse { 28 | string password_sha256 = 1; 29 | } 30 | 31 | enum ObserverConnectRejectReason { 32 | ObserverConnectRejectReasonUnknown = 0; 33 | ObserverConnectRejectReasonObserverVersionTooOld = 1; 34 | ObserverConnectRejectReasonInvalidToken = 2; 35 | ObserverConnectRejectReasonGameNotFound = 3; 36 | ObserverConnectRejectReasonGameNotReady = 4; 37 | ObserverConnectRejectReasonDelayNotOver = 5; 38 | ObserverConnectRejectReasonIncorrectPassword = 6; 39 | ObserverConnectRejectReasonPasswordTimeout = 7; 40 | ObserverConnectRejectReasonPasswordResponseError = 8; 41 | } 42 | 43 | message GameInfo { 44 | int32 id = 1; 45 | string name = 2; 46 | Map map = 3; 47 | repeated Slot slots = 4; 48 | int32 random_seed = 5; 49 | string game_version = 6; 50 | int64 start_time_millis = 7; 51 | optional string flo_tv_password_sha256 = 8; 52 | } 53 | 54 | message Map { 55 | bytes sha1 = 1; 56 | uint32 checksum = 2; 57 | string path = 3; 58 | bool twelve_p = 4; 59 | } 60 | 61 | message Slot { 62 | PlayerInfo player = 1; 63 | flo_common.SlotSettings settings = 2; 64 | } 65 | 66 | message PlayerInfo { 67 | int32 id = 1; 68 | string name = 2; 69 | } 70 | -------------------------------------------------------------------------------- /crates/types/src/health.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::fmt; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize, Default)] 5 | pub struct TcpHealthStats { 6 | pub min: Option, // Min RTT in ms 7 | pub max: Option, // Max RTT in ms 8 | pub avg: Option, // Average RTT in ms 9 | pub stddev: Option, // Standard deviation of RTT 10 | pub current: Option, // Most recent RTT in ms 11 | pub success_rate: f32, // Success rate (0.0 - 1.0) 12 | pub last_check: Option, // Timestamp of last check attempt 13 | pub failure_reason: Option, // Reason for last failure 14 | } 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub enum TcpHealthFailureReason { 18 | Unknown, 19 | ConnectionRefused, 20 | Timeout, 21 | InvalidResponse, 22 | EchoMismatch, 23 | UnexpectedPacket, 24 | IoError(String), 25 | } 26 | 27 | impl Default for TcpHealthFailureReason { 28 | fn default() -> Self { 29 | Self::Timeout 30 | } 31 | } 32 | 33 | impl fmt::Display for TcpHealthFailureReason { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | match self { 36 | TcpHealthFailureReason::Unknown => write!(f, "Unknown"), 37 | TcpHealthFailureReason::ConnectionRefused => write!(f, "ConnectionRefused"), 38 | TcpHealthFailureReason::Timeout => write!(f, "Timeout"), 39 | TcpHealthFailureReason::InvalidResponse => write!(f, "InvalidResponse"), 40 | TcpHealthFailureReason::EchoMismatch => write!(f, "EchoMismatch"), 41 | TcpHealthFailureReason::UnexpectedPacket => write!(f, "UnexpectedPacket"), 42 | TcpHealthFailureReason::IoError(msg) => write!(f, "IoError({})", msg), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /crates/w3gs/src/protocol/ping.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use flo_util::binary::*; 4 | use flo_util::{BinDecode, BinEncode}; 5 | 6 | use crate::protocol::constants::PacketTypeId; 7 | use crate::protocol::packet::PacketPayload; 8 | 9 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 10 | pub struct PingFromHost(Ping); 11 | 12 | impl PingFromHost { 13 | pub fn with_payload(payload: u32) -> Self { 14 | Self(Ping { payload }) 15 | } 16 | 17 | pub fn with_payload_since(since: Instant) -> Self { 18 | Self(Ping::payload_since(since)) 19 | } 20 | } 21 | 22 | impl PacketPayload for PingFromHost { 23 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::PingFromHost; 24 | } 25 | 26 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 27 | pub struct PongToHost(Ping); 28 | 29 | impl PongToHost { 30 | pub fn payload(&self) -> u32 { 31 | self.0.payload 32 | } 33 | 34 | pub fn elapsed_millis(&self, since: Instant) -> u32 { 35 | let d = Instant::now().saturating_duration_since(since); 36 | (d.as_millis() as u32).saturating_sub(self.0.payload) 37 | } 38 | } 39 | 40 | impl PacketPayload for PongToHost { 41 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::PongToHost; 42 | } 43 | 44 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 45 | pub struct Ping { 46 | pub payload: u32, 47 | } 48 | 49 | impl Ping { 50 | pub fn payload_since(since: Instant) -> Ping { 51 | let d = Instant::now().saturating_duration_since(since); 52 | Self { 53 | payload: d.as_millis() as u32, 54 | } 55 | } 56 | } 57 | 58 | #[test] 59 | fn test_ping_from_host() { 60 | crate::packet::test_simple_payload_type( 61 | "ping_from_host.bin", 62 | &PingFromHost(Ping { payload: 95750587 }), 63 | ) 64 | } 65 | 66 | #[test] 67 | fn test_pong_to_host() { 68 | crate::packet::test_simple_payload_type( 69 | "pong_to_host.bin", 70 | &PongToHost(Ping { payload: 95750587 }), 71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /crates/net/src/listener.rs: -------------------------------------------------------------------------------- 1 | use futures::ready; 2 | 3 | use futures::stream::Stream; 4 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; 5 | use std::pin::Pin; 6 | use std::task::{Context, Poll}; 7 | use tokio::net::TcpListener; 8 | 9 | use crate::error::*; 10 | 11 | use crate::stream::FloStream; 12 | 13 | #[derive(Debug)] 14 | pub struct FloListener { 15 | listener: TcpListener, 16 | local_addr: SocketAddr, 17 | } 18 | 19 | impl FloListener { 20 | pub async fn bind_v4(port: u16) -> Result { 21 | let listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)).await?; 22 | let local_addr = listener.local_addr()?; 23 | Ok(FloListener { 24 | listener, 25 | local_addr, 26 | }) 27 | } 28 | 29 | pub fn incoming(&mut self) -> Incoming { 30 | Incoming::new(&mut self.listener) 31 | } 32 | 33 | pub fn local_addr(&self) -> &SocketAddr { 34 | &self.local_addr 35 | } 36 | 37 | pub fn port(&self) -> u16 { 38 | self.local_addr.port() 39 | } 40 | } 41 | 42 | pub struct Incoming<'a> { 43 | inner: &'a mut TcpListener, 44 | } 45 | 46 | impl Incoming<'_> { 47 | pub(crate) fn new(listener: &mut TcpListener) -> Incoming<'_> { 48 | Incoming { inner: listener } 49 | } 50 | 51 | pub fn poll_accept(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 52 | let (socket, _addr) = ready!(self.inner.poll_accept(cx))?; 53 | 54 | socket.set_nodelay(true).ok(); 55 | 56 | //TODO: not supported atm by tokio 57 | //socket.set_keepalive(None).ok(); 58 | 59 | let stream = FloStream::new(socket); 60 | 61 | Poll::Ready(Ok(stream)) 62 | } 63 | } 64 | 65 | impl Stream for Incoming<'_> { 66 | type Item = Result; 67 | 68 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 69 | let stream = ready!(self.poll_accept(cx))?; 70 | Poll::Ready(Some(Ok(stream))) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /crates/controller/src/player/token.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | use jsonwebtoken::errors::ErrorKind; 3 | use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 4 | use once_cell::sync::Lazy; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use crate::error::*; 8 | 9 | // 1 month 10 | const TOKEN_EXPIRATION_SECS: i64 = 3600 * 24 * 30; 11 | const TOKEN_SUB: &str = "flo"; 12 | 13 | #[derive(Debug, Serialize, Deserialize)] 14 | pub struct PlayerToken { 15 | pub sub: String, 16 | pub player_id: i32, 17 | pub exp: usize, 18 | } 19 | 20 | pub fn create_player_token(player_id: i32) -> Result { 21 | static ENCODING_KEY: Lazy = Lazy::new(|| { 22 | EncodingKey::from_base64_secret(&crate::config::JWT_SECRET_BASE64) 23 | .expect("DecodingKey::from_base64_secret") 24 | }); 25 | 26 | let exp = Utc::now().timestamp() + TOKEN_EXPIRATION_SECS; 27 | let claims = PlayerToken { 28 | sub: TOKEN_SUB.to_string(), 29 | player_id, 30 | exp: exp as usize, 31 | }; 32 | encode(&Header::default(), &claims, &ENCODING_KEY).map_err(Into::into) 33 | } 34 | 35 | pub fn validate_player_token(token: &str) -> Result { 36 | let decoding_key = DecodingKey::from_base64_secret(&crate::config::JWT_SECRET_BASE64)?; 37 | decode(token, &decoding_key, &Validation::default()) 38 | .map(|data| data.claims) 39 | .map_err(|e| match e.kind() { 40 | ErrorKind::ExpiredSignature => Error::PlayerTokenExpired, 41 | _ => e.into(), 42 | }) 43 | } 44 | 45 | #[test] 46 | fn test_player_token() { 47 | // This JWT_SECRET is not an actual secret used anywhere. But we need to set one to create a player token. 48 | std::env::set_var( 49 | "JWT_SECRET_BASE64", 50 | "dd7bb476554a8f3980ff95dd1c6da1665fd8b77f42e71b558402f5a3aecec98e", 51 | ); 52 | let token = create_player_token(100).unwrap(); 53 | let token = validate_player_token(&token).unwrap(); 54 | dbg!(token); 55 | } 56 | -------------------------------------------------------------------------------- /crates/net/src/connect/packets.rs: -------------------------------------------------------------------------------- 1 | pub use crate::proto::flo_connect::*; 2 | 3 | packet_type!(ConnectController, PacketClientConnect); 4 | packet_type!(ConnectControllerAccept, PacketClientConnectAccept); 5 | packet_type!(ConnectControllerReject, PacketClientConnectReject); 6 | packet_type!(LobbyDisconnect, PacketClientDisconnect); 7 | packet_type!(GameInfo, PacketGameInfo); 8 | packet_type!(GamePlayerEnter, PacketGamePlayerEnter); 9 | packet_type!(GamePlayerLeave, PacketGamePlayerLeave); 10 | packet_type!(GameSlotUpdate, PacketGameSlotUpdate); 11 | packet_type!(GameSlotUpdateRequest, PacketGameSlotUpdateRequest); 12 | packet_type!(PlayerSessionUpdate, PacketPlayerSessionUpdate); 13 | packet_type!(ListNodesRequest, PacketListNodesRequest); 14 | packet_type!(ListNodes, PacketListNodes); 15 | packet_type!(GameSelectNodeRequest, PacketGameSelectNodeRequest); 16 | packet_type!(GameSelectNode, PacketGameSelectNode); 17 | packet_type!(PlayerPingMapUpdateRequest, PacketPlayerPingMapUpdateRequest); 18 | packet_type!(PlayerPingMapUpdate, PacketPlayerPingMapUpdate); 19 | packet_type!( 20 | GamePlayerPingMapSnapshotRequest, 21 | PacketGamePlayerPingMapSnapshotRequest 22 | ); 23 | packet_type!(GamePlayerPingMapSnapshot, PacketGamePlayerPingMapSnapshot); 24 | packet_type!(GamePlayerToken, PacketGamePlayerToken); 25 | packet_type!(GameStartRequest, PacketGameStartRequest); 26 | packet_type!(GameStarting, PacketGameStarting); 27 | packet_type!(GameStartReject, PacketGameStartReject); 28 | packet_type!( 29 | GameStartPlayerClientInfoRequest, 30 | PacketGameStartPlayerClientInfoRequest 31 | ); 32 | packet_type!(GameSlotClientStatusUpdate, PacketGameSlotClientStatusUpdate); 33 | packet_type!(AddNode, PacketAddNode); 34 | packet_type!(RemoveNode, PacketRemoveNode); 35 | packet_type!(PlayerMuteListUpdate, PacketPlayerMuteListUpdate); 36 | packet_type!(PlayerMuteAddRequest, PacketPlayerMuteAddRequest); 37 | packet_type!(PlayerMuteRemoveRequest, PacketPlayerMuteRemoveRequest); 38 | -------------------------------------------------------------------------------- /crates/task/src/spawn_scope.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::watch::{channel, Receiver, Sender}; 2 | 3 | /// RAII guard used to notify child tasks that the parent has been dropped. 4 | #[derive(Debug)] 5 | pub struct SpawnScope { 6 | tx: Option>, 7 | rx: Receiver<()>, 8 | } 9 | 10 | impl SpawnScope { 11 | pub fn new() -> Self { 12 | let (tx, rx) = channel(()); 13 | Self { tx: Some(tx), rx } 14 | } 15 | 16 | pub fn handle(&self) -> SpawnScopeHandle { 17 | let rx = self.rx.clone(); 18 | SpawnScopeHandle(rx) 19 | } 20 | 21 | pub fn close(&mut self) { 22 | self.tx.take(); 23 | } 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct SpawnScopeHandle(Receiver<()>); 28 | 29 | impl Clone for SpawnScopeHandle { 30 | fn clone(&self) -> Self { 31 | let rx = self.0.clone(); 32 | SpawnScopeHandle(rx) 33 | } 34 | } 35 | 36 | impl SpawnScopeHandle { 37 | pub async fn left(&mut self) { 38 | while self.0.changed().await.is_ok() {} 39 | } 40 | } 41 | 42 | #[tokio::test] 43 | async fn test_initial_value() { 44 | use std::future::Future; 45 | use std::time::Duration; 46 | use tokio::time::sleep; 47 | let scope = SpawnScope::new(); 48 | 49 | fn get_task(mut scope: SpawnScopeHandle) -> impl Future { 50 | async move { 51 | let mut n = 0; 52 | loop { 53 | tokio::select! { 54 | _ = scope.left() => { 55 | return n 56 | } 57 | _ = sleep(Duration::from_millis(50)) => { 58 | n = n + 1 59 | } 60 | } 61 | } 62 | } 63 | } 64 | 65 | let t1 = tokio::spawn(get_task(scope.handle())); 66 | let t2 = tokio::spawn(get_task(scope.handle())); 67 | let t3 = tokio::spawn(get_task(scope.handle())); 68 | 69 | sleep(Duration::from_millis(100)).await; 70 | drop(scope); 71 | 72 | let (v1, v2, v3) = tokio::try_join!(t1, t2, t3).unwrap(); 73 | assert!(v1 > 0); 74 | assert!(v2 > 0); 75 | assert!(v3 > 0); 76 | } 77 | -------------------------------------------------------------------------------- /crates/controller/src/map/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod db; 2 | 3 | use s2_grpc_utils::result::Error as ProtoError; 4 | use s2_grpc_utils::{S2ProtoPack, S2ProtoUnpack}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Debug, Serialize, Deserialize, S2ProtoPack, S2ProtoUnpack, Clone)] 8 | #[s2_grpc(message_type = "flo_grpc::game::Wc3Map")] 9 | pub struct Wc3Map { 10 | pub sha1: MapSha1, 11 | pub checksum: u32, 12 | pub name: String, 13 | pub description: String, 14 | pub author: String, 15 | pub path: String, 16 | pub width: u32, 17 | pub height: u32, 18 | pub players: Vec, 19 | pub forces: Vec, 20 | #[serde(default)] 21 | pub twelve_p: bool, 22 | } 23 | 24 | #[derive(Debug, Serialize, Deserialize, Clone)] 25 | #[serde(transparent)] 26 | pub struct MapSha1(pub [u8; 20]); 27 | 28 | impl MapSha1 { 29 | pub fn to_vec(&self) -> Vec { 30 | self.0.to_vec() 31 | } 32 | } 33 | 34 | impl S2ProtoUnpack> for MapSha1 { 35 | fn unpack(value: Vec) -> Result { 36 | let mut bytes = [0_u8; 20]; 37 | if value.len() >= 20 { 38 | bytes.clone_from_slice(&value[0..20]); 39 | } else { 40 | (&mut bytes[0..(value.len())]).clone_from_slice(&value[0..(value.len())]); 41 | } 42 | Ok(MapSha1(bytes)) 43 | } 44 | } 45 | 46 | impl S2ProtoPack> for MapSha1 { 47 | fn pack(self) -> Result, ProtoError> { 48 | Ok(self.0.to_vec()) 49 | } 50 | } 51 | 52 | #[derive(Debug, Serialize, Deserialize, S2ProtoPack, S2ProtoUnpack, Clone)] 53 | #[s2_grpc(message_type = "flo_grpc::game::MapPlayer")] 54 | pub struct MapPlayer { 55 | pub name: String, 56 | pub r#type: u32, 57 | pub race: u32, 58 | pub flags: u32, 59 | } 60 | 61 | #[derive(Debug, Serialize, Deserialize, S2ProtoPack, S2ProtoUnpack, Clone)] 62 | #[s2_grpc(message_type = "flo_grpc::game::MapForce")] 63 | pub struct MapForce { 64 | pub name: String, 65 | pub flags: u32, 66 | pub player_set: u32, 67 | } 68 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | jobs: 5 | - job: server 6 | pool: 7 | vmImage: "ubuntu-24.04" 8 | steps: 9 | - script: | 10 | curl https://sh.rustup.rs/ -sSf | sh -s -- --default-toolchain stable -y 11 | displayName: 'install rust' 12 | 13 | # - script: | 14 | # cargo build -p flo-controller-service --release 15 | # displayName: 'build flo controller' 16 | 17 | # - script: | 18 | # cargo build -p flo-node-service --release 19 | # displayName: 'build flo node' 20 | 21 | # - script: | 22 | # cargo build -p flo-observer-service --release 23 | # displayName: 'build flo observer' 24 | 25 | # - script: | 26 | # mkdir -p ./build/release && 27 | # cp ./target/release/flo-controller-service ./build/release/flo-controller-service && 28 | # cp ./target/release/flo-node-service ./build/release/flo-node-service && 29 | # cp ./target/release/flo-observer-service ./build/release/flo-observer-service 30 | # displayName: 'move files for docker' 31 | 32 | # - task: Docker@2 33 | # inputs: 34 | # containerRegistry: 'Fluxxu Docker Hub' 35 | # repository: 'fluxxu/flo-controller' 36 | # command: 'buildAndPush' 37 | # Dockerfile: './build/controller.Dockerfile' 38 | # buildContext: "./build" 39 | # tags: | 40 | # $(Build.BuildID) 41 | # latest 42 | # - task: Docker@2 43 | # inputs: 44 | # containerRegistry: 'Fluxxu Docker Hub' 45 | # repository: 'fluxxu/flo-node' 46 | # command: 'buildAndPush' 47 | # Dockerfile: './build/node.Dockerfile' 48 | # buildContext: "./build" 49 | # tags: | 50 | # $(Build.BuildID) 51 | # latest 52 | # - task: Docker@2 53 | # inputs: 54 | # containerRegistry: 'Fluxxu Docker Hub' 55 | # repository: 'fluxxu/flo-observer' 56 | # command: 'buildAndPush' 57 | # Dockerfile: './build/observer.Dockerfile' 58 | # buildContext: "./build" 59 | # tags: | 60 | # $(Build.BuildID) 61 | # latest 62 | -------------------------------------------------------------------------------- /binaries/flo-worker-ui/src/gui/style.rs: -------------------------------------------------------------------------------- 1 | use iced::{button, container, Background, Color}; 2 | 3 | pub struct DefaultStyle(); 4 | impl container::StyleSheet for DefaultStyle { 5 | fn style(&self) -> container::Style { 6 | container::Style { 7 | background: Some(Background::Color(Color::from_rgb(0.1, 0.1, 0.1))), 8 | text_color: Some(Color::from_rgb(0.9, 0.9, 0.9)), 9 | ..container::Style::default() 10 | } 11 | } 12 | } 13 | 14 | pub struct DefaultButton(); 15 | impl button::StyleSheet for DefaultButton { 16 | fn active(&self) -> button::Style { 17 | button::Style { 18 | text_color: Color::from_rgb(0.8, 0.8, 0.8), 19 | border_radius: 2.0, 20 | ..button::Style::default() 21 | } 22 | } 23 | fn hovered(&self) -> button::Style { 24 | button::Style { 25 | background: Some(Background::Color(Color::from_rgb(0.3, 0.3, 0.3))), 26 | text_color: Color::from_rgb(0.9, 0.9, 0.9), 27 | ..self.active() 28 | } 29 | } 30 | fn disabled(&self) -> button::Style { 31 | button::Style { 32 | text_color: Color::from_rgba(0.75, 0.75, 0.75, 0.25), 33 | ..self.active() 34 | } 35 | } 36 | } 37 | 38 | pub struct DefaultBoxedButton(); 39 | impl button::StyleSheet for DefaultBoxedButton { 40 | fn active(&self) -> button::Style { 41 | button::Style { 42 | border_color: Color::from_rgba(0.25, 0.25, 0.25, 0.6), 43 | background: Some(Background::Color(Color::from_rgb(0.1, 0.1, 0.1))), 44 | border_width: 1.0, 45 | border_radius: 5.0, 46 | text_color: Color::from_rgb(0.8, 0.8, 0.8), 47 | ..button::Style::default() 48 | } 49 | } 50 | 51 | fn hovered(&self) -> button::Style { 52 | button::Style { 53 | background: Some(Background::Color(Color::from_rgb(0.3, 0.3, 0.3))), 54 | text_color: Color::from_rgb(0.9, 0.9, 0.9), 55 | ..self.active() 56 | } 57 | } 58 | 59 | fn disabled(&self) -> button::Style { 60 | button::Style { 61 | text_color: Color::from_rgba(0.75, 0.75, 0.75, 0.25), 62 | ..self.active() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /crates/controller/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | mod actor_map; 2 | 3 | use bs_diesel_utils::{Executor, ExecutorRef}; 4 | use flo_state::{Addr, Message, Registry}; 5 | 6 | use std::sync::Arc; 7 | 8 | use crate::error::*; 9 | use crate::game::state::GameRegistry; 10 | 11 | use crate::node::NodeRegistry; 12 | use crate::player::state::PlayerRegistry; 13 | 14 | use crate::config::ConfigStorage; 15 | use crate::player::state::sender::PlayerRegistryHandle; 16 | pub use actor_map::{ActorMapExt, GetActorEntry}; 17 | 18 | #[derive(Debug)] 19 | pub struct Data { 20 | pub db: ExecutorRef, 21 | } 22 | 23 | pub struct ControllerState { 24 | pub db: ExecutorRef, 25 | pub registry: Registry, 26 | pub nodes: Addr, 27 | pub games: Addr, 28 | pub players: Addr, 29 | pub player_packet_sender: PlayerRegistryHandle, 30 | pub config: Addr, 31 | } 32 | 33 | pub type ControllerStateRef = Arc; 34 | 35 | impl ControllerState { 36 | pub async fn init() -> Result { 37 | let db = Executor::env().into_ref(); 38 | 39 | #[cfg(not(debug_assertions))] 40 | { 41 | db.exec(|conn| crate::migration::run(conn)).await?; 42 | } 43 | 44 | let registry = Registry::with_data(Data { db: db.clone() }); 45 | 46 | let nodes = registry.resolve().await?; 47 | let games = registry.resolve().await?; 48 | let players = registry.resolve().await?; 49 | let config = registry.resolve().await?; 50 | 51 | Ok(ControllerState { 52 | db, 53 | registry, 54 | nodes, 55 | games, 56 | players: players.clone(), 57 | player_packet_sender: PlayerRegistryHandle::from(players), 58 | config, 59 | }) 60 | } 61 | 62 | pub async fn reload(&self) -> Result<()> { 63 | self.config.send(Reload).await??; 64 | self.nodes.send(Reload).await??; 65 | Ok(()) 66 | } 67 | 68 | pub fn into_ref(self) -> Arc { 69 | Arc::new(self) 70 | } 71 | } 72 | 73 | pub struct Reload; 74 | 75 | impl Message for Reload { 76 | type Result = Result<()>; 77 | } 78 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "crates/codegen", 5 | "crates/platform", 6 | "crates/util", 7 | "crates/w3gs", 8 | "crates/w3map", 9 | "crates/w3c", 10 | "crates/blp", 11 | "crates/lan", 12 | "crates/log", 13 | "crates/log-subscriber", 14 | "crates/config", 15 | "crates/net", 16 | "crates/w3storage", 17 | "crates/w3replay", 18 | "crates/constants", 19 | "crates/event", 20 | "crates/task", 21 | "crates/types", 22 | "crates/debug", 23 | "crates/observer", 24 | "crates/observer-fs", 25 | "crates/observer-archiver", 26 | "crates/kinesis", 27 | "crates/replay", 28 | "crates/otel", 29 | "crates/state", 30 | 31 | "crates/controller", 32 | "crates/node", 33 | "crates/client", 34 | "crates/observer-edge", 35 | 36 | "binaries/flo", 37 | "binaries/flo-cli", 38 | "binaries/flo-controller-service", 39 | "binaries/flo-node-service", 40 | "binaries/flo-worker", 41 | "binaries/flo-worker-ui", 42 | "binaries/flo-ping", 43 | "binaries/flo-stats-service", 44 | 45 | "deps/flo-grpc" 46 | ] 47 | 48 | [workspace.dependencies] 49 | tokio = { version = "1.47.1" } 50 | tokio-stream = { version = "0.1.17" } 51 | tokio-util = { version = "0.7" } 52 | 53 | [patch.crates-io] 54 | #stormlib = { path = "../stormlib-rs/crates/stormlib" } 55 | #stormlib-sys = { path = "../stormlib-rs/crates/stormlib-sys" } 56 | stormlib = { git = "https://github.com/wc3tools/stormlib-rs.git" } 57 | stormlib-sys = { git = "https://github.com/wc3tools/stormlib-rs.git" } 58 | casclib = { git = "https://github.com/w3champions/casclib-rs.git" } 59 | casclib-sys = { git = "https://github.com/w3champions/casclib-rs.git" } 60 | #s2-grpc-utils = { path = "../s2-grpc-utils" } 61 | s2-grpc-utils = { git = "https://github.com/Ventmere/s2-grpc-utils.git", branch = "0.2" } 62 | flo-state = { path = "crates/state" } 63 | bs-diesel-utils = { git = "https://github.com/BSpaceinc/bs-diesel-utils.git" } 64 | async-dnssd = { git = "https://github.com/stbuehler/rust-async-dnssd.git" } 65 | 66 | [profile.release-with-debug] 67 | inherits = "release" 68 | debug = true 69 | -------------------------------------------------------------------------------- /crates/observer-edge/src/controller.rs: -------------------------------------------------------------------------------- 1 | use flo_grpc::{ 2 | Channel, 3 | controller::flo_controller_client::FloControllerClient, 4 | }; 5 | use s2_grpc_utils::S2ProtoUnpack; 6 | use tonic::{service::Interceptor, metadata::{MetadataValue, Ascii}, codegen::InterceptedService}; 7 | use crate::error::{Result, Error}; 8 | use crate::game::Game; 9 | 10 | type Client = FloControllerClient>; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct Controller { 14 | client: Client, 15 | } 16 | 17 | impl Controller { 18 | pub fn from_env() -> Self { 19 | let chan = Channel::from_static(crate::env::ENV.controller_url.as_str()); 20 | let secret = crate::env::ENV.controller_secret.parse().unwrap(); 21 | Self { 22 | client: FloControllerClient::with_interceptor(chan.connect_lazy(), WithSecretInterceptor { 23 | secret, 24 | }), 25 | } 26 | } 27 | 28 | pub async fn fetch_game(&self, game_id: i32) -> Result { 29 | use flo_grpc::controller::GetGameRequest; 30 | let res = self.client.clone().get_game(GetGameRequest { 31 | game_id 32 | }).await; 33 | match res { 34 | Ok(res) => Ok(Game::unpack(res.into_inner().game)?), 35 | Err(status) => { 36 | if status.code() == tonic::Code::InvalidArgument { 37 | Err(Error::InvalidGameId(game_id)) 38 | } else { 39 | Err(Error::ControllerService(status)) 40 | } 41 | }, 42 | } 43 | } 44 | } 45 | 46 | #[derive(Clone)] 47 | pub struct WithSecretInterceptor { 48 | secret: MetadataValue, 49 | } 50 | 51 | impl Interceptor for WithSecretInterceptor { 52 | fn call(&mut self, mut request: tonic::Request<()>) -> Result, tonic::Status> { 53 | request 54 | .metadata_mut() 55 | .insert("x-flo-secret", self.secret.clone()); 56 | Ok(request) 57 | } 58 | } 59 | 60 | #[tokio::test] 61 | async fn test_ctrlr_client() -> anyhow::Result<()> { 62 | dotenv::dotenv().unwrap(); 63 | let client = Controller::from_env(); 64 | let game = client.fetch_game(2048816).await?; 65 | dbg!(game); 66 | Ok(()) 67 | } 68 | -------------------------------------------------------------------------------- /crates/platform/src/path/windows.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use winapi::shared::guiddef::GUID; 3 | use winapi::um::winnt::WCHAR; 4 | 5 | fn get_known_folder_path(id: GUID) -> Option { 6 | use std::ptr; 7 | use widestring::UCString; 8 | use winapi::shared::winerror::S_OK; 9 | use winapi::um::combaseapi::CoTaskMemFree; 10 | use winapi::um::shlobj::SHGetKnownFolderPath; 11 | 12 | let mut pwstr: *mut WCHAR = ptr::null_mut(); 13 | 14 | unsafe { 15 | if S_OK 16 | != SHGetKnownFolderPath( 17 | &id as *const _, 18 | 0, 19 | ptr::null_mut(), 20 | &mut pwstr as *mut *mut WCHAR, 21 | ) 22 | { 23 | return None; 24 | } 25 | }; 26 | 27 | if pwstr.is_null() { 28 | return None; 29 | } 30 | 31 | let wstring = unsafe { UCString::from_ptr_str(pwstr) }; 32 | let path = PathBuf::from(wstring.to_os_string()); 33 | 34 | unsafe { 35 | CoTaskMemFree(pwstr.cast()); 36 | } 37 | 38 | Some(path) 39 | } 40 | 41 | pub fn detect_user_data_path(ptr: bool) -> Option { 42 | let mut path = get_known_folder_path(winapi::um::knownfolders::FOLDERID_Documents)?; 43 | path.push(if ptr {"Warcraft III Public Test"} else {"Warcraft III"}); 44 | if std::fs::metadata(&path).is_ok() { 45 | Some(path) 46 | } else { 47 | None 48 | } 49 | } 50 | 51 | pub fn detect_installation_path() -> Option { 52 | let try_list = vec![ 53 | { 54 | let mut path = get_known_folder_path(winapi::um::knownfolders::FOLDERID_ProgramFilesX86)?; 55 | path.push("Warcraft III"); 56 | path 57 | }, 58 | { 59 | let mut path = get_known_folder_path(winapi::um::knownfolders::FOLDERID_ProgramFilesX64)?; 60 | path.push("Warcraft III"); 61 | path 62 | }, 63 | ]; 64 | 65 | for path in try_list { 66 | let full = path.join("Warcraft III Launcher.exe"); 67 | if std::fs::metadata(full).is_ok() { 68 | return Some(path); 69 | } 70 | } 71 | 72 | None 73 | } 74 | 75 | #[test] 76 | fn test_windows() { 77 | assert!(dbg!(detect_user_data_path(false)).is_some()); 78 | assert!(dbg!(detect_installation_path()).is_some()); 79 | } 80 | -------------------------------------------------------------------------------- /crates/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flo-client" 3 | version = "0.18.3" 4 | authors = ["Flux Xu "] 5 | edition = "2018" 6 | 7 | [features] 8 | default = [] 9 | ws = ["async-tungstenite"] 10 | worker = ["ws"] 11 | blacklist = ["flo-w3c/blacklist"] 12 | 13 | [dependencies] 14 | flo-constants = { path = "../constants" } 15 | flo-types = { path = "../types" } 16 | flo-log = { path = "../log" } 17 | flo-lan = { path = "../lan" } 18 | flo-net = { path = "../net" } 19 | flo-config = { path = "../config" } 20 | flo-platform = { path = "../platform" } 21 | flo-w3storage = { path = "../w3storage" } 22 | flo-w3map = { path = "../w3map", features = ["w3storage"] } 23 | flo-w3gs = { path = "../w3gs" } 24 | flo-util = { path = "../util" } 25 | flo-task = { path = "../task" } 26 | flo-w3c = { path = "../w3c" } 27 | flo-state = "1" 28 | flo-observer = { path = "../observer" } 29 | flo-observer-fs = { path = "../observer-fs" } 30 | flo-replay = { path = "../replay" } 31 | 32 | s2-grpc-utils = "0.2" 33 | tokio = { workspace = true, features = [ 34 | "time", 35 | "net", 36 | "macros", 37 | "sync", 38 | "rt", 39 | "rt-multi-thread", 40 | ] } 41 | tokio-stream = { workspace = true, features = ["time", "net"] } 42 | tokio-util = { workspace = true, features = ["time"] } 43 | async-tungstenite = { version = "0.16.1", features = [ 44 | "tokio-runtime", 45 | ], optional = true } 46 | tracing = "0.1" 47 | tracing-futures = "0.2" 48 | thiserror = "1.0" 49 | anyhow = "1.0" 50 | parking_lot = "0.11" 51 | http = "0.2" 52 | futures = "0.3.24" 53 | serde = { version = "1", features = ["derive", "rc"] } 54 | serde_json = "1.0" 55 | base64 = "0.12" 56 | lazy_static = "1.4" 57 | hash-ids = "0.2" 58 | rand = "0.8" 59 | backoff = "0.3" 60 | bytes = "1.2.1" 61 | chrono = "^0.4.26" 62 | 63 | [target.'cfg(windows)'.dependencies] 64 | winapi = { version = "0.3", features = ["timeapi"] } 65 | 66 | [dev-dependencies] 67 | dotenv = "0.15" 68 | flo-log-subscriber = { path = "../log-subscriber" } 69 | flo-controller = { path = "../controller" } 70 | flo-grpc = { path = "../../deps/flo-grpc" } 71 | tonic = "0.6" 72 | 73 | [build-dependencies] 74 | flo-constants = { path = "../constants" } 75 | -------------------------------------------------------------------------------- /crates/w3gs/src/protocol/map.rs: -------------------------------------------------------------------------------- 1 | use flo_util::binary::*; 2 | use flo_util::{BinDecode, BinEncode}; 3 | 4 | use crate::protocol::constants::PacketTypeId; 5 | use crate::protocol::game::GameSettings; 6 | use crate::protocol::packet::PacketPayload; 7 | 8 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 9 | pub struct MapCheck { 10 | #[bin(eq = 0x01)] 11 | _unknown_1: u32, 12 | pub file_path: CString, 13 | pub file_size: u32, 14 | pub file_crc: u32, 15 | pub map_xoro: u32, 16 | pub sha1: [u8; 20], 17 | } 18 | 19 | impl MapCheck { 20 | pub fn new(file_size: u32, file_crc: u32, game_settings: &GameSettings) -> Self { 21 | Self { 22 | _unknown_1: 0x01, 23 | file_path: game_settings.map_path.clone(), 24 | file_size, 25 | file_crc, 26 | map_xoro: game_settings.map_checksum, 27 | sha1: game_settings.map_sha1, 28 | } 29 | } 30 | } 31 | 32 | impl PacketPayload for MapCheck { 33 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::MapCheck; 34 | } 35 | 36 | #[derive(Debug, BinDecode, BinEncode, PartialEq)] 37 | pub struct MapSize { 38 | #[bin(eq = 0x01)] 39 | _unknown_1: u32, 40 | pub size_flag: u8, 41 | pub map_size: u32, 42 | } 43 | 44 | impl MapSize { 45 | pub fn new(map_size: u32) -> Self { 46 | Self { 47 | _unknown_1: 1, 48 | size_flag: 1, 49 | map_size, 50 | } 51 | } 52 | } 53 | 54 | impl PacketPayload for MapSize { 55 | const PACKET_TYPE_ID: PacketTypeId = PacketTypeId::MapSize; 56 | } 57 | 58 | #[test] 59 | fn test_map_check() { 60 | crate::packet::test_simple_payload_type( 61 | "map_check.bin", 62 | &MapCheck { 63 | _unknown_1: 1, 64 | file_path: CString::new("Maps/(2)bootybay.w3m").unwrap(), 65 | file_size: 127172, 66 | file_crc: 1444344839, 67 | map_xoro: 2039165270, 68 | sha1: [ 69 | 201, 228, 110, 214, 86, 255, 142, 141, 140, 96, 141, 57, 3, 110, 63, 27, 250, 11, 28, 194, 70 | ], 71 | }, 72 | ) 73 | } 74 | 75 | #[test] 76 | fn test_map_size() { 77 | crate::packet::test_simple_payload_type( 78 | "map_size.bin", 79 | &MapSize { 80 | _unknown_1: 1, 81 | size_flag: 1, 82 | map_size: 127172, 83 | }, 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /crates/util/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum Error { 5 | #[error("parse error: {0}")] 6 | Parse(String), 7 | } 8 | 9 | pub type Result = std::result::Result; 10 | 11 | #[derive(Error, Debug)] 12 | pub enum BinDecodeError { 13 | #[error("{context}not enough data")] 14 | Incomplete { context: BinDecodeErrorContext }, 15 | #[error("{context}{message}")] 16 | Failure { 17 | message: String, 18 | context: BinDecodeErrorContext, 19 | }, 20 | } 21 | 22 | impl BinDecodeError { 23 | #[inline] 24 | pub fn incomplete() -> Self { 25 | BinDecodeError::Incomplete { 26 | context: BinDecodeErrorContext::new(), 27 | } 28 | } 29 | 30 | #[inline] 31 | pub fn failure(msg: T) -> Self 32 | where 33 | T: std::fmt::Display, 34 | { 35 | BinDecodeError::Failure { 36 | message: msg.to_string(), 37 | context: BinDecodeErrorContext::new(), 38 | } 39 | } 40 | 41 | pub fn context(self, ctx: T) -> Self { 42 | match self { 43 | BinDecodeError::Incomplete { mut context } => { 44 | context.insert(ctx); 45 | BinDecodeError::Incomplete { context } 46 | } 47 | BinDecodeError::Failure { 48 | message, 49 | mut context, 50 | } => { 51 | context.insert(ctx); 52 | BinDecodeError::Failure { message, context } 53 | } 54 | } 55 | } 56 | 57 | #[inline] 58 | pub fn is_incomplete(&self) -> bool { 59 | match *self { 60 | BinDecodeError::Incomplete { .. } => true, 61 | _ => false, 62 | } 63 | } 64 | } 65 | 66 | #[derive(Debug)] 67 | pub struct BinDecodeErrorContext(Vec); 68 | 69 | impl BinDecodeErrorContext { 70 | fn new() -> Self { 71 | BinDecodeErrorContext(vec![]) 72 | } 73 | 74 | fn insert(&mut self, ctx: T) { 75 | self.0.push(ctx.to_string()) 76 | } 77 | } 78 | 79 | impl std::fmt::Display for BinDecodeErrorContext { 80 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 81 | if self.0.is_empty() { 82 | return Ok(()); 83 | } 84 | 85 | for ctx in self.0.iter().rev() { 86 | write!(f, "{}: ", ctx)? 87 | } 88 | 89 | Ok(()) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /crates/controller/src/game/state/slot.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::game::db::UpdateSlotSettings; 3 | use crate::game::state::GameActor; 4 | use crate::game::{Slot, SlotSettings}; 5 | use diesel::prelude::*; 6 | use flo_net::packet::FloPacket; 7 | use flo_net::proto; 8 | use flo_state::{async_trait, Context, Handler, Message}; 9 | use s2_grpc_utils::S2ProtoPack; 10 | 11 | pub struct UpdateSlot { 12 | pub player_id: i32, 13 | pub slot_index: i32, 14 | pub settings: SlotSettings, 15 | } 16 | 17 | impl Message for UpdateSlot { 18 | type Result = Result>; 19 | } 20 | 21 | #[async_trait] 22 | impl Handler for GameActor { 23 | async fn handle( 24 | &mut self, 25 | _: &mut Context, 26 | UpdateSlot { 27 | player_id, 28 | slot_index, 29 | settings, 30 | }: UpdateSlot, 31 | ) -> Result> { 32 | let game_id = self.game_id; 33 | 34 | let UpdateSlotSettings { 35 | slots, 36 | updated_indexes, 37 | } = self 38 | .db 39 | .exec(move |conn| { 40 | conn.transaction(|| { 41 | let info = crate::game::db::get_slot_owner_info(conn, game_id, slot_index)?; 42 | if !info.is_slot_owner(player_id) { 43 | return Err(Error::GameSlotUpdateDenied); 44 | } 45 | crate::game::db::update_slot_settings(conn, game_id, slot_index, settings) 46 | }) 47 | }) 48 | .await?; 49 | 50 | let mut frames_slot_update = Vec::with_capacity(updated_indexes.len()); 51 | 52 | for index in updated_indexes { 53 | let slot = &slots[index as usize]; 54 | let settings: proto::flo_connect::SlotSettings = slot.settings.clone().pack()?; 55 | let frame = proto::flo_connect::PacketGameSlotUpdate { 56 | game_id, 57 | slot_index: index, 58 | slot_settings: settings.into(), 59 | player: slot.player.clone().map(|p| p.pack()).transpose()?, 60 | } 61 | .encode_as_frame()?; 62 | frames_slot_update.push(frame); 63 | } 64 | 65 | let players = slots 66 | .iter() 67 | .filter_map(|s| s.player.as_ref().map(|p| p.id)) 68 | .collect(); 69 | self 70 | .player_reg 71 | .broadcast(players, frames_slot_update) 72 | .await?; 73 | 74 | Ok(slots) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /crates/w3gs/src/net/codec.rs: -------------------------------------------------------------------------------- 1 | use flo_util::binary::*; 2 | use tokio_util::codec::{Decoder, Encoder}; 3 | 4 | use crate::error::Error; 5 | use crate::protocol::packet::{Header, Packet}; 6 | 7 | #[derive(Debug)] 8 | pub struct W3GSCodec { 9 | decode_state: DecoderState, 10 | } 11 | 12 | impl W3GSCodec { 13 | pub fn new() -> Self { 14 | Self { 15 | decode_state: DecoderState::DecodingHeader, 16 | } 17 | } 18 | } 19 | 20 | impl Decoder for W3GSCodec { 21 | type Item = Packet; 22 | type Error = Error; 23 | 24 | fn decode(&mut self, src: &mut BytesMut) -> Result, Self::Error> { 25 | match self.decode_state { 26 | DecoderState::DecodingHeader => { 27 | if src.remaining() >= Header::MIN_SIZE { 28 | let header = Header::decode(src)?; 29 | let payload_len = header.get_payload_len()?; 30 | if src.remaining() >= payload_len { 31 | // payload received 32 | let packet = Packet::decode(header, src)?; 33 | Ok(Some(packet)) 34 | } else { 35 | // wait payload 36 | src.reserve(payload_len); 37 | self.decode_state = DecoderState::DecodingPayload { 38 | header: Some(header), 39 | payload_len, 40 | }; 41 | Ok(None) 42 | } 43 | } else { 44 | // wait header 45 | Ok(None) 46 | } 47 | } 48 | DecoderState::DecodingPayload { 49 | ref mut header, 50 | payload_len, 51 | } => { 52 | if src.remaining() >= payload_len { 53 | let packet = Packet::decode( 54 | header.take().ok_or_else(|| Error::InvalidStateNoHeader)?, 55 | src, 56 | )?; 57 | self.decode_state = DecoderState::DecodingHeader; 58 | Ok(Some(packet)) 59 | } else { 60 | Ok(None) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | #[derive(Debug)] 68 | enum DecoderState { 69 | DecodingHeader, 70 | DecodingPayload { 71 | header: Option
, 72 | payload_len: usize, 73 | }, 74 | } 75 | 76 | impl Encoder for W3GSCodec { 77 | type Error = Error; 78 | 79 | fn encode(&mut self, item: Packet, dst: &mut BytesMut) -> Result<(), Self::Error> { 80 | item.encode(dst); 81 | Ok(()) 82 | } 83 | } 84 | --------------------------------------------------------------------------------