diff --git a/assets/robocraft/config.json b/assets/robocraft/config.json index 56f7d98..3061bc7 100644 --- a/assets/robocraft/config.json +++ b/assets/robocraft/config.json @@ -14543,6 +14543,10 @@ { "message": "Stop Killing Games", "duration": 20 + }, + { + "message": "Also check out RC15", + "duration": 20 } ] } diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index 53b2fcc..e76d7b7 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -376,7 +376,7 @@ fn default_rotation() -> GameEventSequence { auto_heal: true, }, multiplayer: GameEvent { - map: GameMap::Neptune3, + map: GameMap::Earth1, visibility: GameVisibility::Good, mode: GameType::Pit, auto_heal: true, diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 10606a4..febdb6a 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -385,21 +385,30 @@ impl super::ConfigProvider for CubeConfig { fn maps(&self) -> std::collections::HashMap { self.battle.maps.map.iter().map(|(map, conf)| { let mut spawns = std::collections::HashMap::>::with_capacity(2); // usually 2 teams + let mut pit_spawns = Vec::default(); for point in conf.spawn_points.iter() { - if let Some(list) = spawns.get_mut(&point.team) { - list.push(super::Point { - x: point.x, - y: point.y, - z: point.z, - }); + if let Some(point_team) = &point.team { + if let Some(list) = spawns.get_mut(point_team) { + list.push(super::Point { + x: point.x, + y: point.y, + z: point.z, + }); + } else { + let mut list = Vec::with_capacity(10); // usually 10 spawn points (suddent death has the most) + list.push(super::Point { + x: point.x, + y: point.y, + z: point.z, + }); + spawns.insert(*point_team, list); + } } else { - let mut list = Vec::with_capacity(10); // usually 10 spawn points (suddent death has the most) - list.push(super::Point { + pit_spawns.push(super::Point { x: point.x, y: point.y, z: point.z, }); - spawns.insert(point.team, list); } } let bases = conf.bases.iter().map(|base| (base.team, (super::Sphere { @@ -425,6 +434,7 @@ impl super::ConfigProvider for CubeConfig { }; let map_conf = super::MapConfig { spawns, + pit_spawns, bases, capture_points, equalizer, diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index 8eca126..03a43ce 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -360,6 +360,7 @@ pub struct Sphere { #[derive(Clone, Debug)] pub struct MapConfig { pub spawns: std::collections::HashMap>, // team -> points + pub pit_spawns: Vec, pub bases: std::collections::HashMap, // team -> (base, capture speed) pub capture_points: Vec<(Sphere, f32)>, // (capture point, capture speed) pub equalizer: Point, diff --git a/rc_core/src/persist/maps.rs b/rc_core/src/persist/maps.rs index b32b2da..52771a7 100644 --- a/rc_core/src/persist/maps.rs +++ b/rc_core/src/persist/maps.rs @@ -16,7 +16,7 @@ pub struct MapConfig { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct SpawnPoint { - pub team: u8, + pub team: Option, pub x: f32, pub y: f32, pub z: f32, @@ -134,126 +134,347 @@ pub(super) fn default_map() -> std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap std::collections::HashMap GenericGamemodeEngine { &connection.connection) .await?; + let spawn_data_already_handled = extra_packets.iter().any(|packet| matches!(packet.event, rlnl::event_code::NetworkEvent::FreeSpawnPoint)); + for packet in extra_packets { sender.send_data( &*packet.data, @@ -1096,66 +1098,68 @@ impl GenericGamemodeEngine { literustlib::packet::Property::ReliableOrdered, &connection.connection) .await?; - if map.spawns.is_empty() { - // fallback - for i in 0..num_players { - sender.send_data( - &rlnl::events::sync::SpawnPoint { - pos: rlnl::types::PosQuatPair { - pos: rlnl::types::CompressedVec3::from((10.0 * (i as f32), 100.0, 10.0 * (i as f32))), - rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, - }, - owner: i, - }, - rlnl::event_code::NetworkEvent::FreeSpawnPoint, - literustlib::packet::Property::ReliableOrdered, - &connection.connection) - .await?; - } - } else { - let mut last_spawn_point = std::collections::HashMap::with_capacity(2); // team -> last index - for player in players.iter() { - let team = player.team as u8; - if let Some(team_points) = map.spawns.get(&team) { - if !team_points.is_empty() { - let spawn_index = if let Some(last_spawn_i) = last_spawn_point.get_mut(&team) { - *last_spawn_i = (*last_spawn_i + 1) % team_points.len(); - *last_spawn_i - } else { - last_spawn_point.insert(team, 0usize); - 0 - }; - let spawn = &team_points[spawn_index]; - sender.send_data( - &rlnl::events::sync::SpawnPoint { - pos: rlnl::types::PosQuatPair { - pos: rlnl::types::CompressedVec3::from((spawn.x, spawn.y, spawn.z)), - rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, - }, - owner: player.player_id, - }, - rlnl::event_code::NetworkEvent::FreeSpawnPoint, - literustlib::packet::Property::ReliableOrdered, - &connection.connection) - .await?; - continue; - } - } + if !spawn_data_already_handled { + if map.spawns.is_empty() { // fallback - log::warn!("No spawn point found for player {} on team {}, using bad fallback", player.player_id, team); - sender.send_data( - &rlnl::events::sync::SpawnPoint { - pos: rlnl::types::PosQuatPair { - pos: rlnl::types::CompressedVec3::from((10.0 * (player.player_id as f32), 100.0, 10.0 * (team as f32) + 10.0)), - rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + for i in 0..num_players { + sender.send_data( + &rlnl::events::sync::SpawnPoint { + pos: rlnl::types::PosQuatPair { + pos: rlnl::types::CompressedVec3::from((10.0 * (i as f32), 100.0, 10.0 * (i as f32))), + rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + }, + owner: i, }, - owner: player.player_id, - }, - rlnl::event_code::NetworkEvent::FreeSpawnPoint, - literustlib::packet::Property::ReliableOrdered, - &connection.connection) - .await?; + rlnl::event_code::NetworkEvent::FreeSpawnPoint, + literustlib::packet::Property::ReliableOrdered, + &connection.connection) + .await?; + } + } else { + let mut last_spawn_point = std::collections::HashMap::with_capacity(2); // team -> last index + for player in players.iter() { + let team = player.team as u8; + if let Some(team_points) = map.spawns.get(&team) { + if !team_points.is_empty() { + let spawn_index = if let Some(last_spawn_i) = last_spawn_point.get_mut(&team) { + *last_spawn_i = (*last_spawn_i + 1) % team_points.len(); + *last_spawn_i + } else { + last_spawn_point.insert(team, 0usize); + 0 + }; + let spawn = &team_points[spawn_index]; + sender.send_data( + &rlnl::events::sync::SpawnPoint { + pos: rlnl::types::PosQuatPair { + pos: rlnl::types::CompressedVec3::from((spawn.x, spawn.y, spawn.z)), + rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + }, + owner: player.player_id, + }, + rlnl::event_code::NetworkEvent::FreeSpawnPoint, + literustlib::packet::Property::ReliableOrdered, + &connection.connection) + .await?; + continue; + } + } + // fallback + log::warn!("No spawn point found for player {} on team {}, using bad fallback", player.player_id, team); + sender.send_data( + &rlnl::events::sync::SpawnPoint { + pos: rlnl::types::PosQuatPair { + pos: rlnl::types::CompressedVec3::from((10.0 * (player.player_id as f32), 100.0, 10.0 * (team as f32) + 10.0)), + rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + }, + owner: player.player_id, + }, + rlnl::event_code::NetworkEvent::FreeSpawnPoint, + literustlib::packet::Property::ReliableOrdered, + &connection.connection) + .await?; + } } } diff --git a/rc_multiplayer/src/matches/modes/pit.rs b/rc_multiplayer/src/matches/modes/pit.rs index 6c009ee..f1ec1a1 100644 --- a/rc_multiplayer/src/matches/modes/pit.rs +++ b/rc_multiplayer/src/matches/modes/pit.rs @@ -1,3 +1,5 @@ +use rand::Rng; + use crate::matches::CustomGameLogic; struct WinTracker { @@ -146,10 +148,11 @@ pub struct PitLogic { win_tracking: WinTracker, settings: std::sync::Arc, timer_task: tokio::sync::Mutex>>, + initial_spawns: Vec<(u8, oj_rc_core::persist::config::Point)>, // (player_id, spawn_point) } impl PitLogic { - pub fn new(config: &oj_rc_core::data::game_mode::GameModeConfig, _map: &oj_rc_core::persist::config::MapConfig, players: &[oj_rc_core::persist::user::PlayerDescriptor], pit_settings: std::sync::Arc) -> Self { + pub fn new(config: &oj_rc_core::data::game_mode::GameModeConfig, map: &oj_rc_core::persist::config::MapConfig, players: &[oj_rc_core::persist::user::PlayerDescriptor], pit_settings: std::sync::Arc) -> Self { PitLogic { respawn_full_heal_duration: config.respawn_full_heal_duration, respawn_heal_duration: config.respawn_heal_duration, @@ -157,6 +160,7 @@ impl PitLogic { win_tracking: WinTracker::new(), timer_task: tokio::sync::Mutex::new(None), settings: pit_settings, + initial_spawns: Self::generate_first_spawns(map, players), } } @@ -230,6 +234,69 @@ impl PitLogic { self.do_leader_update(generic, killer, victim).await; } + fn choose_spawn_point(map_config: &oj_rc_core::persist::config::MapConfig, player_id: u8) -> (Option, oj_rc_core::persist::config::Point) { + if map_config.pit_spawns.is_empty() { + (None, oj_rc_core::persist::config::Point { + x: 10.0 * (player_id as f32), + y: 100.0, + z: 10.0, + }) + } else { + let spawn_index = { + rand::rng().random_range(0..map_config.pit_spawns.len()) + }; + (Some(spawn_index), map_config.pit_spawns[spawn_index].clone()) + } + } + + fn generate_first_spawns(map_config: &oj_rc_core::persist::config::MapConfig, players: &[oj_rc_core::persist::user::PlayerDescriptor]) -> Vec<(u8, oj_rc_core::persist::config::Point)> { + if map_config.pit_spawns.is_empty() { + log::warn!("Map is missing pit spawn points, spawn points will be (poorly) generated"); + } + let mut spawns = Vec::with_capacity(players.len()); + let mut seen_spawns = std::collections::HashSet::new(); + for player in players { + 'choose_loop: loop { + let (spawn_i, spawn_point) = Self::choose_spawn_point(map_config, player.player_id); + if let Some(spawn_i) = spawn_i { + if seen_spawns.contains(&spawn_i) { + continue; + } else { + seen_spawns.insert(spawn_i); + } + } + spawns.push((player.player_id, spawn_point)); + /*spawns.push(crate::matches::engine::RlnlPacket { + event: rlnl::event_code::NetworkEvent::FreeSpawnPoint, + property: literustlib::packet::Property::ReliableOrdered, + data: Box::new(rlnl::events::sync::SpawnPoint { + pos: rlnl::types::PosQuatPair { + pos: rlnl::types::CompressedVec3::from((spawn_point.x, spawn_point.y, spawn_point.z)), + rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + }, + owner: player_i as u8, + }) + });*/ + break 'choose_loop; + } + } + spawns + } + + fn initial_spawns_to_packets(&self) -> Vec { + self.initial_spawns.iter().map(|(player_id, spawn_point)| crate::matches::engine::RlnlPacket { + event: rlnl::event_code::NetworkEvent::FreeSpawnPoint, + property: literustlib::packet::Property::ReliableOrdered, + data: Box::new(rlnl::events::sync::SpawnPoint { + pos: rlnl::types::PosQuatPair { + pos: rlnl::types::CompressedVec3::from((spawn_point.x, spawn_point.y, spawn_point.z)), + rot: rlnl::types::CompressedQuat { x: 0, y: 0, z: 0 }, + }, + owner: *player_id, + }) + }).collect() + } + async fn do_respawn_tasks(&self, generic: &crate::matches::GenericGamemodeEngine, player_id: u8) { log::info!("Handling respawn player {} in game {}", player_id, generic.game_guid()); let respawn_time = std::time::Duration::from_secs(self.settings.respawn_time_seconds); @@ -248,25 +315,7 @@ impl PitLogic { &respawn_payload, true ).await; - // FIXME use full map spawns instead of team base spawns - let spawn_point = if let Some(team_spawns) = generic.map_config.spawns.get(&0) { - if team_spawns.is_empty() { - oj_rc_core::persist::config::Point { - x: 10.0 * (player_id as f32), - y: 100.0, - z: 10.0, - } - } else { - let index = (player_id as usize) % team_spawns.len(); - team_spawns[index].clone() - } - } else { - oj_rc_core::persist::config::Point { - x: 10.0 * (player_id as f32), - y: 100.0, - z: 10.0, - } - }; + let spawn_point = Self::choose_spawn_point(&generic.map_config, player_id).1; let connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); tokio::task::spawn(Self::respawn_player_after(respawn_timestamp, connections, spawn_point, player_id)); } @@ -301,6 +350,7 @@ impl CustomGameLogic for PitLogic { } async fn on_player_end(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> bool { + // TODO handle win condition when only one player remains true } @@ -341,7 +391,8 @@ impl CustomGameLogic for PitLogic { } async fn extra_sync_events(&self, _generic: &crate::matches::GenericGamemodeEngine, _player: &crate::matches::generic::UserConnection) -> Vec { - vec![ + let mut initial_spawn_packets = self.initial_spawns_to_packets(); + initial_spawn_packets.push( crate::matches::RlnlPacket { event: rlnl::event_code::NetworkEvent::GameModeSettings, property: literustlib::packet::Property::ReliableOrdered, @@ -350,7 +401,8 @@ impl CustomGameLogic for PitLogic { respawn_full_heal_duration: self.respawn_full_heal_duration, }), }, - ] + ); + initial_spawn_packets } async fn on_countdown_start(&self, generic: &crate::matches::GenericGamemodeEngine, game_start: chrono::DateTime) -> bool { diff --git a/utils/cube_gen.py b/utils/cube_gen.py index a0930bd..524a0e0 100755 --- a/utils/cube_gen.py +++ b/utils/cube_gen.py @@ -193,6 +193,7 @@ LOGIN_MESSAGES = [ "Warning, live without warning\nI say, warning, live without warning\nWithout, all right", "Spitzer Damn!", "Stop Killing Games", + "Also check out RC15", ] def guess_category(name: str, sprite: str) -> str: diff --git a/utils/generate_sub_spawns.py b/utils/generate_sub_spawns.py new file mode 100755 index 0000000..d14b76a --- /dev/null +++ b/utils/generate_sub_spawns.py @@ -0,0 +1,84 @@ +#!/bin/env python3 + +import json +import argparse +import requests +import urllib + +def print_transform(child_json): + local_x = child_json["m_LocalPosition"]["m_X"] + local_y = child_json["m_LocalPosition"]["m_Y"] + local_z = child_json["m_LocalPosition"]["m_Z"] + rot_w = child_json["m_LocalRotation"]["m_W"] + rot_x = child_json["m_LocalRotation"]["m_X"] + rot_y = child_json["m_LocalRotation"]["m_Y"] + rot_z = child_json["m_LocalRotation"]["m_Z"] + # valid Rust code (to be inserted into a vec![here]) + print(f"""SpawnPoint {{ + team: None, + x: {local_x:.3f}, + y: {local_y:.3f}, + z: {local_z:.3f}, +}}/*.with_rotation(num_quaternion::Quaternion {{ + x: {rot_x:.6f}, + y: {rot_y:.6f}, + z: {rot_z:.6f}, + w: {rot_w:.6f}, +}})*/,""") + +def gen_by_children(path: str, start: int, end: int): + path_json = json.loads(path) + url_path = str(path) + if "\"" in path: + url_path = urllib.parse.quote(path) + asset_resp = requests.get(f"http://127.0.0.1:38723/Assets/Json?Path={url_path}") + asset_json = asset_resp.json() + if end is None: + end = len(asset_json["m_Children"]) + for (i, child) in enumerate(asset_json["m_Children"][start:end]): + #print(i, child) + path_json["D"] = child["m_PathID"] + url_path = urllib.parse.quote(json.dumps(path_json)) + child_resp = requests.get(f"http://127.0.0.1:38723/Assets/Json?Path={url_path}") + child_json = child_resp.json() + print_transform(child_json) + +def gen_by_mono(path: str, start: int, end: int): + path_json = json.loads(path) + url_path = str(path) + if "\"" in path: + url_path = urllib.parse.quote(path) + asset_resp = requests.get(f"http://127.0.0.1:38723/Assets/Json?Path={url_path}") + asset_json = asset_resp.json() + points = asset_json["m_Structure"]["spawningPoints"] + if end is None: + end = len(points) + for (i, child) in enumerate(points): + #print(i, child) + path_json["D"] = child["m_PathID"] + url_path = urllib.parse.quote(json.dumps(path_json)) + gameobj_resp = requests.get(f"http://127.0.0.1:38723/Assets/Json?Path={url_path}") + gameobj_json = gameobj_resp.json() + path_json["D"] = gameobj_json["m_GameObject"]["m_PathID"] + url_path = urllib.parse.quote(json.dumps(path_json)) + point_resp = requests.get(f"http://127.0.0.1:38723/Assets/Json?Path={url_path}") + point_json = point_resp.json() + path_json["D"] = point_json["m_Components"][0]["m_Component"]["m_PathID"] + url_path = urllib.parse.quote(json.dumps(path_json)) + child_resp = requests.get(f"http://127.0.0.1:38723/Assets/Json?Path={url_path}") + child_json = child_resp.json() + print_transform(child_json) + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--start", type=int, default=0) + parser.add_argument("--end", type=int, default=None) + parser.add_argument("--mono", action='store_true') + parser.add_argument("initial_path") + args = parser.parse_args() + if args.mono: + # this works better + gen_by_mono(args.initial_path, args.start, args.end) + else: + # maybe for desperate measures + gen_by_children(args.initial_path, args.start, args.end)