1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Add pit spawn points #34

This commit is contained in:
NG (Graham)
2025-09-13 17:36:35 -04:00
parent 82cc2dcf1b
commit c85d4bbc38
11 changed files with 2186 additions and 260 deletions

View File

@@ -14543,6 +14543,10 @@
{ {
"message": "Stop Killing Games", "message": "Stop Killing Games",
"duration": 20 "duration": 20
},
{
"message": "Also check out RC15",
"duration": 20
} }
] ]
} }

View File

@@ -376,7 +376,7 @@ fn default_rotation() -> GameEventSequence {
auto_heal: true, auto_heal: true,
}, },
multiplayer: GameEvent { multiplayer: GameEvent {
map: GameMap::Neptune3, map: GameMap::Earth1,
visibility: GameVisibility::Good, visibility: GameVisibility::Good,
mode: GameType::Pit, mode: GameType::Pit,
auto_heal: true, auto_heal: true,

View File

@@ -385,21 +385,30 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
fn maps(&self) -> std::collections::HashMap<super::GameMap, super::MapConfig> { fn maps(&self) -> std::collections::HashMap<super::GameMap, super::MapConfig> {
self.battle.maps.map.iter().map(|(map, conf)| { self.battle.maps.map.iter().map(|(map, conf)| {
let mut spawns = std::collections::HashMap::<u8, Vec<super::Point>>::with_capacity(2); // usually 2 teams let mut spawns = std::collections::HashMap::<u8, Vec<super::Point>>::with_capacity(2); // usually 2 teams
let mut pit_spawns = Vec::default();
for point in conf.spawn_points.iter() { for point in conf.spawn_points.iter() {
if let Some(list) = spawns.get_mut(&point.team) { if let Some(point_team) = &point.team {
list.push(super::Point { if let Some(list) = spawns.get_mut(point_team) {
x: point.x, list.push(super::Point {
y: point.y, x: point.x,
z: point.z, 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 { } else {
let mut list = Vec::with_capacity(10); // usually 10 spawn points (suddent death has the most) pit_spawns.push(super::Point {
list.push(super::Point {
x: point.x, x: point.x,
y: point.y, y: point.y,
z: point.z, z: point.z,
}); });
spawns.insert(point.team, list);
} }
} }
let bases = conf.bases.iter().map(|base| (base.team, (super::Sphere { let bases = conf.bases.iter().map(|base| (base.team, (super::Sphere {
@@ -425,6 +434,7 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
}; };
let map_conf = super::MapConfig { let map_conf = super::MapConfig {
spawns, spawns,
pit_spawns,
bases, bases,
capture_points, capture_points,
equalizer, equalizer,

View File

@@ -360,6 +360,7 @@ pub struct Sphere {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MapConfig { pub struct MapConfig {
pub spawns: std::collections::HashMap<u8, Vec<Point>>, // team -> points pub spawns: std::collections::HashMap<u8, Vec<Point>>, // team -> points
pub pit_spawns: Vec<Point>,
pub bases: std::collections::HashMap<u8, (Sphere, f32)>, // team -> (base, capture speed) pub bases: std::collections::HashMap<u8, (Sphere, f32)>, // team -> (base, capture speed)
pub capture_points: Vec<(Sphere, f32)>, // (capture point, capture speed) pub capture_points: Vec<(Sphere, f32)>, // (capture point, capture speed)
pub equalizer: Point, pub equalizer: Point,

File diff suppressed because it is too large Load Diff

View File

@@ -245,6 +245,7 @@ mod _broadcast_impls {
impl Broadcastable for rlnl::events::sync::GetEqualizer {} impl Broadcastable for rlnl::events::sync::GetEqualizer {}
impl Broadcastable for rlnl::events::sync::FusionShieldState {} impl Broadcastable for rlnl::events::sync::FusionShieldState {}
impl Broadcastable for rlnl::events::sync::EqualizerNotification {} impl Broadcastable for rlnl::events::sync::EqualizerNotification {}
impl Broadcastable for rlnl::events::sync::SpawnPoint {}
impl Broadcastable for rlnl::events::GameTime {} impl Broadcastable for rlnl::events::GameTime {}
impl Broadcastable for rlnl::events::ingame::TeamBaseState {} impl Broadcastable for rlnl::events::ingame::TeamBaseState {}
impl Broadcastable for rlnl::events::ingame::GameEnd {} impl Broadcastable for rlnl::events::ingame::GameEnd {}

View File

@@ -66,6 +66,7 @@ impl GameMatches {
log::warn!("No configuration found for map {}, game {} may not work correctly", game_info.map, guid); log::warn!("No configuration found for map {}, game {} may not work correctly", game_info.map, guid);
oj_rc_core::persist::config::MapConfig { oj_rc_core::persist::config::MapConfig {
spawns: std::collections::HashMap::default(), spawns: std::collections::HashMap::default(),
pit_spawns: Vec::default(),
bases: std::collections::HashMap::default(), bases: std::collections::HashMap::default(),
capture_points: Vec::default(), capture_points: Vec::default(),
equalizer: oj_rc_core::persist::config::Point { equalizer: oj_rc_core::persist::config::Point {

View File

@@ -1073,6 +1073,8 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
&connection.connection) &connection.connection)
.await?; .await?;
let spawn_data_already_handled = extra_packets.iter().any(|packet| matches!(packet.event, rlnl::event_code::NetworkEvent::FreeSpawnPoint));
for packet in extra_packets { for packet in extra_packets {
sender.send_data( sender.send_data(
&*packet.data, &*packet.data,
@@ -1096,66 +1098,68 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
literustlib::packet::Property::ReliableOrdered, literustlib::packet::Property::ReliableOrdered,
&connection.connection) &connection.connection)
.await?; .await?;
if map.spawns.is_empty() { if !spawn_data_already_handled {
// fallback if map.spawns.is_empty() {
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;
}
}
// fallback // fallback
log::warn!("No spawn point found for player {} on team {}, using bad fallback", player.player_id, team); for i in 0..num_players {
sender.send_data( sender.send_data(
&rlnl::events::sync::SpawnPoint { &rlnl::events::sync::SpawnPoint {
pos: rlnl::types::PosQuatPair { 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)), 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 }, 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,
rlnl::event_code::NetworkEvent::FreeSpawnPoint, &connection.connection)
literustlib::packet::Property::ReliableOrdered, .await?;
&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?;
}
} }
} }

View File

@@ -1,3 +1,5 @@
use rand::Rng;
use crate::matches::CustomGameLogic; use crate::matches::CustomGameLogic;
struct WinTracker { struct WinTracker {
@@ -146,10 +148,11 @@ pub struct PitLogic {
win_tracking: WinTracker, win_tracking: WinTracker,
settings: std::sync::Arc<oj_rc_core::persist::config::PitSettings>, settings: std::sync::Arc<oj_rc_core::persist::config::PitSettings>,
timer_task: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>, timer_task: tokio::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
initial_spawns: Vec<(u8, oj_rc_core::persist::config::Point)>, // (player_id, spawn_point)
} }
impl PitLogic { 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<oj_rc_core::persist::config::PitSettings>) -> 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<oj_rc_core::persist::config::PitSettings>) -> Self {
PitLogic { PitLogic {
respawn_full_heal_duration: config.respawn_full_heal_duration, respawn_full_heal_duration: config.respawn_full_heal_duration,
respawn_heal_duration: config.respawn_heal_duration, respawn_heal_duration: config.respawn_heal_duration,
@@ -157,6 +160,7 @@ impl PitLogic {
win_tracking: WinTracker::new(), win_tracking: WinTracker::new(),
timer_task: tokio::sync::Mutex::new(None), timer_task: tokio::sync::Mutex::new(None),
settings: pit_settings, 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; self.do_leader_update(generic, killer, victim).await;
} }
fn choose_spawn_point(map_config: &oj_rc_core::persist::config::MapConfig, player_id: u8) -> (Option<usize>, 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<crate::matches::RlnlPacket> {
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<Self>, player_id: u8) { async fn do_respawn_tasks(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, player_id: u8) {
log::info!("Handling respawn player {} in game {}", player_id, generic.game_guid()); 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); let respawn_time = std::time::Duration::from_secs(self.settings.respawn_time_seconds);
@@ -248,25 +315,7 @@ impl PitLogic {
&respawn_payload, &respawn_payload,
true true
).await; ).await;
// FIXME use full map spawns instead of team base spawns let spawn_point = Self::choose_spawn_point(&generic.map_config, player_id).1;
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 connections = generic.users.read().await.values().map(|player_info| player_info.connection.clone()).collect(); 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)); 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<Self>, _player: &crate::matches::generic::UserConnection) -> bool { async fn on_player_end(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _player: &crate::matches::generic::UserConnection) -> bool {
// TODO handle win condition when only one player remains
true true
} }
@@ -341,7 +391,8 @@ impl CustomGameLogic for PitLogic {
} }
async fn extra_sync_events(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _player: &crate::matches::generic::UserConnection) -> Vec<crate::matches::RlnlPacket> { async fn extra_sync_events(&self, _generic: &crate::matches::GenericGamemodeEngine<Self>, _player: &crate::matches::generic::UserConnection) -> Vec<crate::matches::RlnlPacket> {
vec![ let mut initial_spawn_packets = self.initial_spawns_to_packets();
initial_spawn_packets.push(
crate::matches::RlnlPacket { crate::matches::RlnlPacket {
event: rlnl::event_code::NetworkEvent::GameModeSettings, event: rlnl::event_code::NetworkEvent::GameModeSettings,
property: literustlib::packet::Property::ReliableOrdered, property: literustlib::packet::Property::ReliableOrdered,
@@ -350,7 +401,8 @@ impl CustomGameLogic for PitLogic {
respawn_full_heal_duration: self.respawn_full_heal_duration, respawn_full_heal_duration: self.respawn_full_heal_duration,
}), }),
}, },
] );
initial_spawn_packets
} }
async fn on_countdown_start(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, game_start: chrono::DateTime<chrono::Utc>) -> bool { async fn on_countdown_start(&self, generic: &crate::matches::GenericGamemodeEngine<Self>, game_start: chrono::DateTime<chrono::Utc>) -> bool {

View File

@@ -193,6 +193,7 @@ LOGIN_MESSAGES = [
"Warning, live without warning\nI say, warning, live without warning\nWithout, all right", "Warning, live without warning\nI say, warning, live without warning\nWithout, all right",
"Spitzer Damn!", "Spitzer Damn!",
"Stop Killing Games", "Stop Killing Games",
"Also check out RC15",
] ]
def guess_category(name: str, sprite: str) -> str: def guess_category(name: str, sprite: str) -> str:

84
utils/generate_sub_spawns.py Executable file
View File

@@ -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)