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

Get enemies spawning in singleplayer #23

This commit is contained in:
NG (Graham)
2025-05-30 23:03:02 -04:00
parent 1a7400d3d9
commit 50cc0cc3a9
19 changed files with 327 additions and 54 deletions

View File

@@ -60,3 +60,15 @@ pub enum ChatErrorCodes {
PasswordRequired = 16,
ChannelExpired = 17,
}
#[repr(i16)]
#[allow(dead_code)]
#[derive(Debug)]
pub enum SingleplayerErrorCode {
None = 0,
DatabaseError = 1,
UnexpectedError = 2,
WrongNumberOfAuthParams = 3,
MaintenanceMode = 4,
DuplicateLogin = 5,
}

View File

@@ -13,6 +13,7 @@ pub mod weapon_upgrade;
pub mod crf;
pub mod channel;
pub mod sanction;
pub mod robot_data;
pub mod error_codes;

View File

@@ -0,0 +1,23 @@
use polariton::operation::Typed;
pub struct PrebuiltRobotInfo {
//pub id: String,
pub name: String,
pub class: String,
pub category: String,
pub robot_data: Vec<u8>,
pub colour_data: Vec<u8>,
}
impl PrebuiltRobotInfo {
pub fn as_transmissible<C>(&self) -> Typed<C> {
Typed::HashMap(vec![
//(Typed::Str("[key]".into()), Typed::Str(self.id.clone().into())),
(Typed::Str("Name".into()), Typed::Str(self.name.clone().into())),
(Typed::Str("Class".into()), Typed::Str(self.class.clone().into())),
(Typed::Str("Category".into()), Typed::Str(self.category.clone().into())),
(Typed::Str("RobotData".into()), Typed::Bytes(self.robot_data.clone().into())),
(Typed::Str("ColourData".into()), Typed::Bytes(self.colour_data.clone().into())),
].into())
}
}

View File

@@ -9,7 +9,7 @@ pub struct BattleConfig {
#[serde(default = "default_game_modes")]
pub games: GameModes,
#[serde(default = "default_campaigns")]
pub singleplayer: super::Campaigns,
pub singleplayer: super::SingleplayerConfig,
#[serde(default = "default_rotation")]
pub rotation: GameEventSequence,
}
@@ -252,8 +252,8 @@ fn default_game_modes() -> GameModes {
}
}
fn default_campaigns() -> super::Campaigns {
super::Campaigns {
pub(super) fn default_campaigns() -> super::SingleplayerConfig {
super::SingleplayerConfig {
campaigns: vec![
super::Campaign {
id: "strCampaignModeBattle".to_owned(),
@@ -317,7 +317,24 @@ fn default_campaigns() -> super::Campaigns {
}
],
}
]
],
vehicles: vec![
super::PrefabVehicle {
name: Some("Config your singleplayer!".to_owned()),
username: "NGnius".to_owned(),
id: super::PrefabId::Database { garage: 1 }
},
super::PrefabVehicle {
name: Some("Config your singleplayer!".to_owned()),
username: "NGram".to_owned(),
id: super::PrefabId::Database { garage: 1 }
},
super::PrefabVehicle {
name: Some("Config your singleplayer!".to_owned()),
username: "NGniusness".to_owned(),
id: super::PrefabId::Database { garage: 1 }
},
],
}
}

View File

@@ -30,7 +30,7 @@ impl CubeConfig {
}
#[async_trait::async_trait]
impl <C: Clone> super::ConfigProvider<C> for CubeConfig {
impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
fn cube_list(&self) -> Typed<C> {
Typed::Dict(Dict {
key_ty: TypePrefix::Str,
@@ -303,4 +303,55 @@ impl <C: Clone> super::ConfigProvider<C> for CubeConfig {
started: chrono::Utc::now().timestamp(),
}
}
fn singleplayer_vehicles(&self) -> Vec<crate::persist::garage::PrefabVehicle> {
// FIXME don't use serializable types in traits
self.battle.singleplayer.vehicles.clone()
}
/*async fn prefab_vehicles(&self, user: &(dyn crate::persist::user::User<C> + Sync), factory: &crate::factory::Factory) -> Typed<C> {
let mut next_id = 0;
let mut id_map = Vec::with_capacity(self.battle.singleplayer.vehicles.len());
let mut debug_str_map = Vec::with_capacity(self.battle.singleplayer.vehicles.len());
for vehicle in self.battle.singleplayer.vehicles.clone().into_iter() {
let current_id = next_id;
let uuid_i64 = crate::persist::user::uuid_sanitize(crate::persist::user::i64_join((i32::MAX as u32, current_id)));
let uuid_str = crate::persist::user::i64_as_uuid_str(uuid_i64);
let prebuilt = match &vehicle.id {
crate::persist::PrefabId::Factory { factory: factory_id } => {
use rc_factory::VehicleFactoryAdapter;
let factory_vehicle = factory.vehicle(*factory_id).await
.expect("Failed to retrieve prefab vehicle from factory") // result
.expect("Prefab vehicle does not exist in factory"); // option
crate::data::robot_data::PrebuiltRobotInfo {
name: vehicle.name.unwrap_or(factory_vehicle.1.name),
class: vehicle.class,
category: "RE_robot_category0".to_owned(),
robot_data: factory_vehicle.0.cube_data,
colour_data: factory_vehicle.0.colour_data,
}
},
crate::persist::PrefabId::Database { garage } => {
let db_vehicle = user.garage_by_id(*garage).await
.expect("Prefab vehicle does not exist in main garage database");
crate::data::robot_data::PrebuiltRobotInfo {
name: vehicle.name.unwrap_or(db_vehicle.name.expect("Prefab vehicle name is required")),
class: vehicle.class,
category: "RE_robot_category0".to_owned(),
robot_data: db_vehicle.robot_data,
colour_data: db_vehicle.colour_data,
}
},
};
debug_str_map.push(format!("`{}` -> `{}`", uuid_str, prebuilt.name));
id_map.push((Typed::Str(uuid_str.into()), prebuilt.as_transmissible()));
next_id += 1;
}
log::debug!("Prefab mapping generated `<uuid>` -> `<name>`:\n\t{}", debug_str_map.join("\n\t"));
Typed::Dict(Dict {
key_ty: TypePrefix::Str, // str
val_ty: TypePrefix::HashMap, // hashmap
items: id_map,
})
}*/
}

View File

@@ -25,6 +25,8 @@ pub trait ConfigProvider<C: Clone> {
fn cubes(&self) -> &'_ std::collections::HashMap<String, crate::persist::Cube>;
fn chat_system_config(&self) -> ChatSystemConfig;
fn gamemode_events(&self) -> GameEventSequence;
// FIXME don't use serializable types in traits
fn singleplayer_vehicles(&self) -> Vec<crate::persist::garage::PrefabVehicle>;
}
pub struct CompleteCampaignProvider {

View File

@@ -176,3 +176,24 @@ impl std::convert::Into<crate::data::garage_bay::ControlOptions> for GarageContr
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PrefabVehicle {
pub name: Option<String>,
pub username: String,
#[serde(flatten)]
pub id: PrefabId,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum PrefabId {
Factory {
#[serde(alias="crf")]
factory: u32,
},
Database {
garage: u32,
},
// TODO File, Raw
}

View File

@@ -6,7 +6,7 @@ pub use cube_data::{Cube, ItemTier, ItemCategory, ItemType};
//pub use cube_data::{VisibilityMode, ItemType};
mod garage;
pub use garage::{GarageSlot, GarageControls, ControlType};
pub use garage::{GarageSlot, GarageControls, ControlType, PrefabVehicle, PrefabId};
mod movement;
pub use movement::{MovementCategoryData, MovementData};
@@ -21,7 +21,7 @@ mod combat;
pub use combat::BattleConfig;
mod singleplayer;
pub use singleplayer::{Campaigns, Campaign, CampaignDifficulty, CampaignCompletion, CampaignType, Wave, WaveRobot};
pub use singleplayer::{SingleplayerConfig, Campaign, CampaignDifficulty, CampaignCompletion, CampaignType, Wave, WaveRobot};
mod client_config;
pub use client_config::GameplaySettings;

View File

@@ -1,11 +1,13 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Campaigns {
pub struct SingleplayerConfig {
#[serde(default = "default_campaigns")]
pub campaigns: Vec<Campaign>,
pub vehicles: Vec<super::PrefabVehicle>,
}
impl Campaigns {
impl SingleplayerConfig {
pub fn into_campaign_params(self) -> crate::data::campaign::CampaignsGameParameters {
crate::data::campaign::CampaignsGameParameters { campaigns: self.campaigns.into_iter().map(|x| x.into_campaign_params()).collect() }
}
@@ -222,3 +224,7 @@ impl std::convert::Into<crate::data::campaign::CampaignType> for CampaignType {
}
}
}
fn default_campaigns() -> Vec<Campaign> {
super::combat::default_campaigns().campaigns
}

View File

@@ -5,6 +5,7 @@ use crate::persist::config::ConfigProvider;
pub struct AccountProvider {
cubes: std::sync::Arc<Vec<u32>>,
garage_upgrades: std::sync::Arc<crate::persist::config::GarageUpgrades>,
singleplayer_vehicles: std::sync::Arc<Vec<crate::persist::garage::PrefabVehicle>>,
auto_signups: bool,
secret: Vec<u8>,
db: std::sync::Arc<rc_database::Database>,
@@ -21,11 +22,41 @@ impl AccountProvider {
Ok(Self {
cubes: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::ids(conf)),
garage_upgrades: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::garage_upgrades(conf)),
singleplayer_vehicles: std::sync::Arc::new(<crate::persist::config::ConfigImpl as ConfigProvider<()>>::singleplayer_vehicles(conf)),
auto_signups: server_settings.auto_signup,
secret: std::fs::read(&token_path)?,
db: std::sync::Arc::new(db),
})
}
pub fn fake_user<C: Clone>(&self) -> Box<dyn super::User<C> + Send + Sync> {
Box::new(UserData {
token: super::UserToken { uuid: "fake user!".to_owned(), token: "".to_owned(), refresh_token: "".to_owned() },
account: rc_database::schema::user::Model {
id: 0,
creation_time: 0,
public_id: "".to_owned(),
display_name: "".to_owned(),
password: "".to_owned(),
email: "".to_owned(),
steam_id: None,
},
perms: rc_database::schema::permissions::Model {
id: 0,
user_id: 0,
moderator: true,
administrator: true,
developer: true,
royalty: false,
banned: false,
},
cubes: self.cubes.clone(),
garage_upgrades: self.garage_upgrades.clone(),
singleplayer_vehicles: self.singleplayer_vehicles.clone(),
extensions: Default::default(),
db: self.db.clone(),
})
}
}
#[async_trait::async_trait]
@@ -53,6 +84,7 @@ impl <C: Clone> super::UserProvider<C> for AccountProvider {
perms: user_perms,
cubes: self.cubes.clone(),
garage_upgrades: self.garage_upgrades.clone(),
singleplayer_vehicles: self.singleplayer_vehicles.clone(),
extensions: ext,
db: self.db.clone(),
}))
@@ -163,6 +195,7 @@ struct UserData {
perms: rc_database::schema::permissions::Model,
cubes: std::sync::Arc<Vec<u32>>,
garage_upgrades: std::sync::Arc<crate::persist::config::GarageUpgrades>,
singleplayer_vehicles: std::sync::Arc<Vec<crate::persist::garage::PrefabVehicle>>,
extensions: std::collections::HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync + 'static>>,
db: std::sync::Arc<rc_database::Database>,
}
@@ -225,6 +258,105 @@ impl UserData {
fn has_admin_or_better_perms(&self) -> bool {
self.perms.administrator | self.perms.developer
}
async fn resolve_some_singleplayer_vehicles(&self, factory: &dyn rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser) -> Result<Vec<crate::data::player_data::PlayerData>, i16> {
use rand::seq::IndexedRandom;
let mut enemies = Vec::with_capacity(5);
let mut next_id = 0;
let mut seen_usernames = std::collections::HashSet::<String>::new();
for _ in 0..5 {
let vehicle = self.singleplayer_vehicles.choose(&mut rand::rng())
.ok_or(crate::data::error_codes::SingleplayerErrorCode::UnexpectedError as i16)?;
let current_id = next_id;
let uuid_i64 = crate::persist::user::uuid_sanitize(crate::persist::user::i64_join((i32::MAX as u32, current_id)));
let uuid_str = crate::persist::user::i64_as_uuid_str(uuid_i64);
let username = if seen_usernames.contains(&vehicle.username) {
let mut username = vehicle.username.clone();
while seen_usernames.contains(&username) {
username = format!("{}{}", username, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].choose(&mut rand::rng()).unwrap());
}
username
} else {
vehicle.username.clone()
};
seen_usernames.insert(username.clone());
let enemy = match &vehicle.id {
crate::persist::PrefabId::Factory { factory: factory_id } => {
//use rc_factory::VehicleFactoryAdapter;
match factory.vehicle(*factory_id).await {
Ok(Some(factory_vehicle)) => {
let weapons_guess = weapon_order.guess_weapons(&mut std::io::Cursor::new(&factory_vehicle.0.cube_data));
let weapons_guess = vec![weapons_guess[0], 0, 0];
let weapon_ranks = weapons_guess.iter().map(|&x| (x, if x == 0 { 0 } else { 1 })).collect();
crate::data::player_data::PlayerData {
name: username.clone(),
display_name: username.clone(),
mastery: 1,
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| factory_vehicle.1.name.clone()),
robot_map: factory_vehicle.0.cube_data,
team: 1,
has_premium: false,
robot_uuid: uuid_str,
cpu: 420,
weapon_order: weapons_guess,
colour_map: factory_vehicle.0.colour_data,
is_ai: true,
spawn_effect: "Spawn".to_owned(),
death_effect: "Explosion".to_owned(),
player_rank: 1,
weapon_rank: weapon_ranks,
}
},
Ok(None) => {
log::error!("Prefab vehicle {} does not exist in factory", factory_id);
return Err(crate::data::error_codes::SingleplayerErrorCode::UnexpectedError as i16);
},
Err(e) => {
log::error!("Failed to retrieve prefab vehicle {} from factory: {}", factory_id, e);
return Err(crate::data::error_codes::SingleplayerErrorCode::DatabaseError as i16);
}
}
},
crate::persist::PrefabId::Database { garage } => {
match self.db.garage_by_id(*garage).await {
Ok(Some(db_vehicle)) => {
crate::data::player_data::PlayerData {
name: username.clone(),
display_name: username.clone(),
mastery: 1,
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| db_vehicle.name.clone()),
robot_map: db_vehicle.robot_data,
team: 1,
has_premium: false,
robot_uuid: uuid_str,
cpu: db_vehicle.total_robot_cpu as i32,
weapon_order: rc_database::schema::parse_int_csv(&db_vehicle.weapon_order).into_iter().map(|x| x as i32).collect::<Vec<_>>(),
colour_map: db_vehicle.colour_data,
is_ai: true,
spawn_effect: "Spawn".to_owned(),
death_effect: "Explosion".to_owned(),
player_rank: 1,
weapon_rank: rc_database::schema::parse_int_csv(&db_vehicle.weapon_order).into_iter().map(|x| (x as i32, if x == 0 { 0 } else { 1 })).collect(),
}
},
Ok(None) => {
log::error!("Prefab vehicle {} does not exist in main garage database", garage);
return Err(crate::data::error_codes::SingleplayerErrorCode::UnexpectedError as i16);
}
Err(e) => {
log::error!("Failed to retrieve prefab vehicle {} from main garage database: {}", garage, e);
return Err(crate::data::error_codes::SingleplayerErrorCode::DatabaseError as i16);
}
}
},
};
next_id += 1;
enemies.push(enemy);
}
Ok(enemies)
}
}
const INVALID_ROBOT_ERR: i16 = crate::data::error_codes::WebServicesError::InvalidRobot as i16; // 140
@@ -539,8 +671,9 @@ impl <C: Clone> super::User<C> for UserData {
super::since_windows_epoch(self.account.creation_time)
}
async fn singleplayer_robots(&self) -> Result<polariton::operation::Typed<C>, i16> {
self.err_on_banned().await?;
async fn singleplayer_robots(&self, factory: &dyn rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser) -> Result<polariton::operation::Typed<C>, i16> {
//self.err_on_banned().await?;
let mut vehicles = self.resolve_some_singleplayer_vehicles(factory, weapon_order).await?;
let current_slot = self.db.garage_selected(self.account.id).await.map_err(|e| {
log::error!("Failed to retrieve selected vehicle for user_id {} (singleplayer_robots): {}", self.account.id, e);
DATABASE_ERR
@@ -550,28 +683,30 @@ impl <C: Clone> super::User<C> for UserData {
INVALID_ROBOT_ERR
})?;
let user_uuid = self.token.uuid.clone();
let weapon_order = rc_database::schema::parse_int_csv(&current_slot.weapon_order).into_iter().map(|x| x as i32).collect::<Vec<_>>();
// real user MUST be last
vehicles.push(crate::data::player_data::PlayerData {
name: user_uuid.clone(),
display_name: self.account.display_name.clone(),
mastery: current_slot.mastery_level as i32,
tier: 1, // FIXME
robot_name: current_slot.name,
robot_map: current_slot.robot_data.clone(),
team: 0,
has_premium: false, // FIXME
robot_uuid: super::i64_as_uuid_str(current_slot.uuid).into(),
cpu: current_slot.total_robot_cpu as i32,
weapon_order: weapon_order.clone(),
colour_map: current_slot.colour_data.clone(),
is_ai: false,
spawn_effect: "Spawn_Warp".to_owned(), // FIXME
death_effect: "Explosion_Warp".to_owned(), // FIXME
player_rank: 1, // FIXME
weapon_rank: rc_database::schema::parse_int_csv(&current_slot.weapon_order).into_iter().map(|x| (x as i32, if x == 0 { 0 } else { 1 })).collect(),
});
Ok(crate::data::player_data::PlayerDatas {
players: vec![
crate::data::player_data::PlayerData {
name: user_uuid.clone(),
display_name: self.account.display_name.clone(),
mastery: current_slot.mastery_level as i32,
tier: 1, // FIXME
robot_name: current_slot.name,
robot_map: current_slot.robot_data,
team: 0,
has_premium: true, // FIXME
robot_uuid: super::i64_as_uuid_str(current_slot.uuid).into(),
cpu: current_slot.total_robot_cpu as i32,
weapon_order: rc_database::schema::parse_int_csv(&current_slot.weapon_order).into_iter().map(|x| x as i32).collect::<Vec<_>>(),
colour_map: current_slot.colour_data,
is_ai: false,
spawn_effect: "Spawn_Warp".to_owned(), // FIXME
death_effect: "Explosion_Warp".to_owned(), // FIXME
player_rank: 1, // FIXME
weapon_rank: rc_database::schema::parse_int_csv(&current_slot.weapon_order).into_iter().map(|x| (x as i32, if x == 0 { 0 } else { 1 })).collect(),
}
],
players: vehicles,
}.as_transmissible())
}

View File

@@ -73,7 +73,7 @@ pub trait User<C>: ChatUser {
async fn save_slot_customisations(&self, customs: CustomisationData) -> Result<(), i16>;
async fn get_slot_customisations(&self, uuid: &str) -> Result<GetCustomisationData<C>, i16>;
fn signup_date(&self) -> i64;
async fn singleplayer_robots(&self) -> Result<polariton::operation::Typed<C>, i16>;
async fn singleplayer_robots(&self, factory: &dyn rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser) -> Result<polariton::operation::Typed<C>, i16>;
async fn prepare_factory_upload(&self, vehicle: VehicleUploadData) -> Result<rc_factory::VehicleUploadInfo, i16>;
async fn last_seen(&self) -> Result<u64, i16>;
async fn get_avatar_info(&self) -> Result<GetAvatarInfo<C>, i16>;