Initial commit
commit
9102608b37
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
example.config.toml
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "spleef"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
once_cell = "1.17.1"
|
||||||
|
rand = "0.8.5"
|
||||||
|
serde = { version = "1.0.159", features = ["derive"] }
|
||||||
|
toml = "0.7.3"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = "0.3.16"
|
||||||
|
valence = { git = "https://github.com/valence-rs/valence" }
|
||||||
|
valence_protocol = { git = "https://github.com/valence-rs/valence" }
|
@ -0,0 +1,24 @@
|
|||||||
|
This is free and unencumbered software released into the public domain.
|
||||||
|
|
||||||
|
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||||
|
distribute this software, either in source code form or as a compiled
|
||||||
|
binary, for any purpose, commercial or non-commercial, and by any
|
||||||
|
means.
|
||||||
|
|
||||||
|
In jurisdictions that recognize copyright laws, the author or authors
|
||||||
|
of this software dedicate any and all copyright interest in the
|
||||||
|
software to the public domain. We make this dedication for the benefit
|
||||||
|
of the public at large and to the detriment of our heirs and
|
||||||
|
successors. We intend this dedication to be an overt act of
|
||||||
|
relinquishment in perpetuity of all present and future rights to this
|
||||||
|
software under copyright law.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||||
|
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
|
OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
For more information, please refer to <http://unlicense.org/>
|
@ -0,0 +1,9 @@
|
|||||||
|
# TODO
|
||||||
|
- Implement waiting room system [✔️]
|
||||||
|
- Chat [✔️]
|
||||||
|
- Make the game automatically start
|
||||||
|
- Make spectator mode work properly
|
||||||
|
- Maybe make the floors use a random block each round?
|
||||||
|
- Leaderboards using a database, probably Surreal
|
||||||
|
- Maybe some kind of events that can randomly happen? Could be a cool mechanic that other spleef servers (probably) don't have
|
||||||
|
- Eventually add world support for nicer looking arenas
|
@ -0,0 +1,11 @@
|
|||||||
|
no_auto_stop = true
|
||||||
|
|
||||||
|
spawn_y = 60
|
||||||
|
|
||||||
|
platforms = 2
|
||||||
|
platform_gap = 5
|
||||||
|
platform_size = 15
|
||||||
|
|
||||||
|
operators = [
|
||||||
|
"dc524fc6-bdba-4f55-9f7b-cefc824cc77e"
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
use valence::prelude::{event::ChatMessage, *};
|
||||||
|
|
||||||
|
pub fn handle_messages(
|
||||||
|
mut clients: Query<(&mut Client, &Username)>,
|
||||||
|
mut messages: EventReader<ChatMessage>,
|
||||||
|
) {
|
||||||
|
for message in messages.iter() {
|
||||||
|
let username = match clients.get_component::<Username>(message.client) {
|
||||||
|
Ok(username) => username.to_string(),
|
||||||
|
_ => "Unknown".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = format!("{}: {}", username, message.message);
|
||||||
|
|
||||||
|
for (mut client, _) in &mut clients {
|
||||||
|
client.send_message(message.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
use valence::prelude::*;
|
||||||
|
|
||||||
|
use crate::{GameState, CONFIG};
|
||||||
|
|
||||||
|
use super::Command;
|
||||||
|
|
||||||
|
pub fn run(
|
||||||
|
command: Command,
|
||||||
|
client: &mut Client,
|
||||||
|
uuid: Uuid,
|
||||||
|
next_state: &mut ResMut<NextState<GameState>>,
|
||||||
|
state: &Res<State<GameState>>
|
||||||
|
) {
|
||||||
|
if CONFIG.operators.contains(&uuid) {
|
||||||
|
if let Some(args) = command.args {
|
||||||
|
match args[0].as_str() {
|
||||||
|
"start" => next_state.set(GameState::Started),
|
||||||
|
"stop" => next_state.set(GameState::Stopped),
|
||||||
|
_ => client.send_message("Invalid game state!"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client.send_message(format!(
|
||||||
|
"The game is currently {}",
|
||||||
|
match state.0 {
|
||||||
|
GameState::Started => "started",
|
||||||
|
GameState::Stopped => "stopped",
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client.send_message("You do not have the permission to execute this command.");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
mod game;
|
||||||
|
|
||||||
|
use valence::prelude::{event::CommandExecution, *};
|
||||||
|
|
||||||
|
use crate::GameState;
|
||||||
|
|
||||||
|
pub struct Command {
|
||||||
|
pub name: String,
|
||||||
|
pub args: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command {
|
||||||
|
pub fn parse(command: String) -> Self {
|
||||||
|
let command = command
|
||||||
|
.split(" ")
|
||||||
|
.map(|e| e.to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
let name = command[0].clone();
|
||||||
|
let args = if command.len() > 1 {
|
||||||
|
Some(command.as_slice()[1..].to_vec())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Self { name, args }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command_executor(
|
||||||
|
mut clients: Query<&mut Client>,
|
||||||
|
uuids: Query<&UniqueId, With<Client>>,
|
||||||
|
mut events: EventReader<CommandExecution>,
|
||||||
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
|
state: Res<State<GameState>>
|
||||||
|
) {
|
||||||
|
for event in events.iter() {
|
||||||
|
let cmd = Command::parse(event.command.to_string());
|
||||||
|
let mut client = clients.get_mut(event.client).unwrap();
|
||||||
|
let uuid = uuids.get(event.client).unwrap();
|
||||||
|
|
||||||
|
match cmd.name.as_str() {
|
||||||
|
"game" => game::run(cmd, &mut client, uuid.0, &mut next_state, &state),
|
||||||
|
_ => client.send_message(format!("Command {} not found!", cmd.name)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use valence::uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
// USE ONLY FOR DEVELOPMENT PURPOSES
|
||||||
|
pub no_auto_stop: Option<bool>,
|
||||||
|
|
||||||
|
pub spawn_y: i32,
|
||||||
|
|
||||||
|
pub platforms: i32,
|
||||||
|
pub platform_gap: i32,
|
||||||
|
pub platform_size: i32,
|
||||||
|
|
||||||
|
pub operators: Vec<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static CONFIG: Lazy<Config> = Lazy::new(|| {
|
||||||
|
let config = fs::read_to_string("./config.toml").expect("Failed to read config.toml!");
|
||||||
|
|
||||||
|
toml::from_str(&config).unwrap()
|
||||||
|
});
|
@ -0,0 +1,94 @@
|
|||||||
|
use rand::prelude::*;
|
||||||
|
use valence::prelude::*;
|
||||||
|
|
||||||
|
use crate::{player::Alive, GameState, CONFIG};
|
||||||
|
|
||||||
|
pub fn start_game(
|
||||||
|
mut clients: Query<(&mut Client, &mut Position, &mut GameMode, &mut Alive)>,
|
||||||
|
mut instances: Query<&mut Instance>,
|
||||||
|
) {
|
||||||
|
tracing::debug!("Starting game!");
|
||||||
|
|
||||||
|
let mut instance = instances.single_mut();
|
||||||
|
|
||||||
|
for (mut client, mut pos, mut gamemode, mut alive) in &mut clients {
|
||||||
|
*gamemode = GameMode::Survival;
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
let x = rng.gen_range(0..=CONFIG.platform_size);
|
||||||
|
let z = rng.gen_range(0..=CONFIG.platform_size);
|
||||||
|
|
||||||
|
pos.set([x.into(), CONFIG.spawn_y as f64 + 1.0, z.into()]);
|
||||||
|
alive.0 = true;
|
||||||
|
|
||||||
|
// TODO: add colors
|
||||||
|
client.set_title("Game started!", "Have fun!", None);
|
||||||
|
}
|
||||||
|
|
||||||
|
for platform in -CONFIG.platforms..CONFIG.platforms * CONFIG.platform_gap {
|
||||||
|
if platform % CONFIG.platform_gap == 0 {
|
||||||
|
for z in -CONFIG.platform_size..CONFIG.platform_size {
|
||||||
|
for x in -CONFIG.platform_size..CONFIG.platform_size {
|
||||||
|
instance.set_block([x, CONFIG.spawn_y - platform, z], BlockState::SNOW_BLOCK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_game(
|
||||||
|
mut clients: Query<(
|
||||||
|
&mut Client,
|
||||||
|
&mut Position,
|
||||||
|
&mut GameMode,
|
||||||
|
&mut Alive,
|
||||||
|
&Username,
|
||||||
|
)>,
|
||||||
|
) {
|
||||||
|
tracing::debug!("Stopping game!");
|
||||||
|
|
||||||
|
let mut alive_players = vec![];
|
||||||
|
|
||||||
|
for (mut client, mut pos, mut gamemode, mut alive, username) in &mut clients {
|
||||||
|
if alive.0 {
|
||||||
|
alive_players.push(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
*gamemode = GameMode::Spectator;
|
||||||
|
pos.set([0.0, CONFIG.spawn_y as f64 + 10.0, 0.0]);
|
||||||
|
alive.0 = false;
|
||||||
|
|
||||||
|
// TODO: add colors
|
||||||
|
client.set_title(
|
||||||
|
"Game has ended!",
|
||||||
|
format!(
|
||||||
|
"{} wins!",
|
||||||
|
// if statement to not panic if somehow no one was alive when the game ended
|
||||||
|
if alive_players.len() >= 1 {
|
||||||
|
// let's assume there's only one alive player
|
||||||
|
alive_players[0].0.clone()
|
||||||
|
} else {
|
||||||
|
"Somehow, no one".to_string()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auto_stop(
|
||||||
|
clients: Query<&Alive, With<Client>>,
|
||||||
|
mut next_state: ResMut<NextState<GameState>>,
|
||||||
|
state: Res<State<GameState>>,
|
||||||
|
) {
|
||||||
|
if state.0 == GameState::Started {
|
||||||
|
let alive_clients = clients
|
||||||
|
.iter()
|
||||||
|
.filter(|alive| alive.0)
|
||||||
|
.collect::<Vec<&Alive>>();
|
||||||
|
|
||||||
|
if alive_clients.len() <= 1 {
|
||||||
|
next_state.set(GameState::Stopped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
mod chat;
|
||||||
|
mod commands;
|
||||||
|
mod config;
|
||||||
|
mod game;
|
||||||
|
mod player;
|
||||||
|
mod world;
|
||||||
|
|
||||||
|
use config::CONFIG;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use valence::bevy_ecs;
|
||||||
|
use valence::prelude::*;
|
||||||
|
|
||||||
|
#[derive(States, Default, Debug, Hash, PartialEq, Eq, Clone)]
|
||||||
|
pub enum GameState {
|
||||||
|
Started,
|
||||||
|
#[default]
|
||||||
|
Stopped,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
Lazy::force(&CONFIG);
|
||||||
|
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
.add_state::<GameState>()
|
||||||
|
.add_plugin(ServerPlugin::new(()))
|
||||||
|
.add_startup_system(world::setup)
|
||||||
|
.add_system(player::init_clients)
|
||||||
|
.add_systems(
|
||||||
|
(
|
||||||
|
default_event_handler,
|
||||||
|
player::break_block,
|
||||||
|
commands::command_executor,
|
||||||
|
chat::handle_messages,
|
||||||
|
)
|
||||||
|
.in_schedule(EventLoopSchedule),
|
||||||
|
)
|
||||||
|
.add_systems((
|
||||||
|
despawn_disconnected_clients,
|
||||||
|
player::death,
|
||||||
|
game::auto_stop.run_if(|| !CONFIG.no_auto_stop.unwrap_or(false)),
|
||||||
|
))
|
||||||
|
.add_system(game::start_game.in_schedule(OnEnter(GameState::Started)))
|
||||||
|
.add_system(game::stop_game.in_schedule(OnExit(GameState::Started)))
|
||||||
|
.add_systems(PlayerList::default_systems())
|
||||||
|
.run();
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
use valence::bevy_ecs;
|
||||||
|
use valence::{
|
||||||
|
entity::player::PlayerEntityBundle,
|
||||||
|
prelude::{event::StartDigging, *},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{GameState, CONFIG};
|
||||||
|
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct Alive(pub bool);
|
||||||
|
|
||||||
|
pub fn init_clients(
|
||||||
|
mut clients: Query<(Entity, &UniqueId, &mut GameMode), Added<Client>>,
|
||||||
|
instances: Query<Entity, With<Instance>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (entity, uuid, mut game_mode) in &mut clients {
|
||||||
|
*game_mode = GameMode::Spectator;
|
||||||
|
|
||||||
|
commands.entity(entity).insert((
|
||||||
|
Alive(false),
|
||||||
|
PlayerEntityBundle {
|
||||||
|
location: Location(instances.single()),
|
||||||
|
position: Position::new([0.0, CONFIG.spawn_y as f64 + 10.0, 0.0]),
|
||||||
|
uuid: *uuid,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn break_block(mut instances: Query<&mut Instance>, mut events: EventReader<StartDigging>) {
|
||||||
|
let mut instance = instances.single_mut();
|
||||||
|
|
||||||
|
for event in events.iter() {
|
||||||
|
instance.set_block(event.position, BlockState::AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn death(
|
||||||
|
mut clients: Query<(&mut Client, &mut Position, &mut GameMode, &mut Alive)>,
|
||||||
|
state: Res<State<GameState>>,
|
||||||
|
) {
|
||||||
|
for (mut client, mut pos, mut gamemode, mut alive) in &mut clients {
|
||||||
|
if pos.0.y <= 0.0 && state.0 == GameState::Started {
|
||||||
|
*gamemode = GameMode::Spectator;
|
||||||
|
pos.set([0.0, (CONFIG.spawn_y + 10).into(), 0.0]);
|
||||||
|
alive.0 = false;
|
||||||
|
|
||||||
|
client.send_message("You died!".italic().bold().color(Color::GRAY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
use valence::prelude::*;
|
||||||
|
|
||||||
|
pub fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
server: Res<Server>,
|
||||||
|
dimensions: Query<&DimensionType>,
|
||||||
|
biomes: Query<&Biome>,
|
||||||
|
) {
|
||||||
|
let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server);
|
||||||
|
|
||||||
|
for z in -5..5 {
|
||||||
|
for x in -5..5 {
|
||||||
|
instance.insert_chunk([x, z], Chunk::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commands.spawn(instance);
|
||||||
|
|
||||||
|
tracing::info!("Finished setting up world!");
|
||||||
|
}
|
Loading…
Reference in new issue