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

Add customisable game mode sequence (mp and sp still broken) #16

This commit is contained in:
NG (Graham)
2025-05-27 22:07:04 -04:00
parent 34b41eff9c
commit 9d9f8a73fc
10 changed files with 534 additions and 71 deletions

1
Cargo.lock generated
View File

@@ -3068,6 +3068,7 @@ dependencies = [
"log",
"polariton",
"polariton_server",
"rand 0.9.0",
"rc_database",
"rc_factory",
"serde",

View File

@@ -17,6 +17,7 @@ chrono = "0.4"
polariton_server.workspace = true
tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util", "time" ] }
async-trait.workspace = true
rand = { version = "0.9", features = [ "thread_rng" ] }
# auth
libfj.workspace = true

View File

@@ -39,3 +39,102 @@ impl GameModeConfigs {
})
}
}
pub enum GameMap {
Mars1,
Mars2,
Mars3,
Neptune1,
Neptune2,
Neptune3,
Earth1,
Earth2,
}
impl GameMap {
#[inline]
pub fn as_str(&self) -> &'static str {
match self {
Self::Mars1 => "RC_Planet_Mars_01_CTF",
Self::Mars2 => "RC_Planet_Mars_02_BA",
Self::Mars3 => "RC_Planet_Mars_03_BA",
Self::Neptune1 => "RC_Planet_Neptune_01_CTF",
Self::Neptune2 => "RC_Planet_Neptune_02_BA",
Self::Neptune3 => "RC_Planet_Neptune_03_BA",
Self::Earth1 => "RC_Planet_Earth_01_BA",
Self::Earth2 => "RC_Planet_Earth_02_BA",
}
}
#[inline]
pub fn from_persist(map: crate::persist::config::GameMap) -> Self {
match map {
crate::persist::config::GameMap::Mars1 => Self::Mars1,
crate::persist::config::GameMap::Mars2 => Self::Mars2,
crate::persist::config::GameMap::Mars3 => Self::Mars3,
crate::persist::config::GameMap::Neptune1 => Self::Neptune1,
crate::persist::config::GameMap::Neptune2 => Self::Neptune2,
crate::persist::config::GameMap::Neptune3 => Self::Neptune3,
crate::persist::config::GameMap::Earth1 => Self::Earth1,
crate::persist::config::GameMap::Earth2 => Self::Earth2,
}
}
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum GameMode {
BattleArena = 0,
SuddenDeath = 1,
Pit = 2,
TestMode = 3,
SinglePlayer = 4,
TeamDeathmatch = 5,
Campaign = 6,
}
impl GameMode {
pub fn as_str(&self) -> &'static str {
match self {
GameMode::BattleArena => "BattleArena",
GameMode::SuddenDeath => "SuddenDeath",
GameMode::Pit => "Pit",
GameMode::TestMode => "TestMode",
GameMode::SinglePlayer => "SinglePlayerTDM",
GameMode::TeamDeathmatch => "TeamDeathmatch",
GameMode::Campaign => "Campaign",
}
}
#[inline]
pub fn from_persist(mode: crate::persist::config::GameType) -> Self {
match mode {
crate::persist::config::GameType::BattleArena => Self::BattleArena,
crate::persist::config::GameType::SuddenDeath => Self::SuddenDeath,
crate::persist::config::GameType::Pit => Self::Pit,
crate::persist::config::GameType::TestMode => Self::TestMode,
crate::persist::config::GameType::SinglePlayer => Self::SinglePlayer,
crate::persist::config::GameType::TeamDeathmatch => Self::TeamDeathmatch,
crate::persist::config::GameType::Campaign => Self::Campaign,
}
}
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum MapVisibility {
Good = 0,
Poor = 1,
Bad = 2, // VeryPoor
}
impl MapVisibility {
#[inline]
pub fn from_persist(mode: crate::persist::config::GameVisibility) -> Self {
match mode {
crate::persist::config::GameVisibility::Good => Self::Good,
crate::persist::config::GameVisibility::Poor => Self::Poor,
crate::persist::config::GameVisibility::Bad => Self::Bad,
}
}
}

View File

@@ -10,6 +10,8 @@ pub struct BattleConfig {
pub games: GameModes,
#[serde(default = "default_campaigns")]
pub singleplayer: super::Campaigns,
#[serde(default = "default_rotation")]
pub rotation: GameEventSequence,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
@@ -103,6 +105,124 @@ impl std::convert::Into<crate::data::game_mode::GameModeConfigs> for GameModes {
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GameEventSequence {
pub strategy: GameRotationStrategy,
pub modes: Vec<GameEvents>,
}
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
pub enum GameRotationStrategy {
Sequence,
Random,
}
impl GameRotationStrategy {
pub fn into_conf(self) -> crate::persist::config::GameRotationStrategy {
match self {
Self::Sequence => crate::persist::config::GameRotationStrategy::Sequence,
Self::Random => crate::persist::config::GameRotationStrategy::Random,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GameEvents {
pub singleplayer: GameEvent,
pub multiplayer: GameEvent,
pub duration_s: u64, // seconds
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct GameEvent {
pub map: GameMap,
pub visibility: GameVisibility,
pub mode: GameType,
pub auto_heal: bool,
}
impl GameEvent {
pub fn into_conf(self) -> crate::persist::config::GameEvent {
crate::persist::config::GameEvent {
map: self.map.into_conf(),
visibility: self.visibility.into_conf(),
mode: self.mode.into_conf(),
auto_heal: self.auto_heal,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
pub enum GameMap {
// TODO put some more obvious aliases on these
Mars1,
Mars2,
Mars3,
Neptune1,
Neptune2,
Neptune3,
Earth1,
Earth2,
}
impl GameMap {
fn into_conf(self) -> crate::persist::config::GameMap {
match self {
Self::Mars1 => crate::persist::config::GameMap::Mars1,
Self::Mars2 => crate::persist::config::GameMap::Mars2,
Self::Mars3 => crate::persist::config::GameMap::Mars3,
Self::Neptune1 => crate::persist::config::GameMap::Neptune1,
Self::Neptune2 => crate::persist::config::GameMap::Neptune2,
Self::Neptune3 => crate::persist::config::GameMap::Neptune3,
Self::Earth1 => crate::persist::config::GameMap::Earth1,
Self::Earth2 => crate::persist::config::GameMap::Earth2,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
pub enum GameVisibility {
Good,
Poor,
Bad,
}
impl GameVisibility {
fn into_conf(self) -> crate::persist::config::GameVisibility {
match self {
Self::Good => crate::persist::config::GameVisibility::Good,
Self::Poor => crate::persist::config::GameVisibility::Poor,
Self::Bad => crate::persist::config::GameVisibility::Bad,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
pub enum GameType {
BattleArena,
SuddenDeath,
Pit,
TestMode,
SinglePlayer,
TeamDeathmatch,
Campaign,
}
impl GameType {
fn into_conf(self) -> crate::persist::config::GameType {
match self {
Self::BattleArena => crate::persist::config::GameType::BattleArena,
Self::SuddenDeath => crate::persist::config::GameType::SuddenDeath,
Self::Pit => crate::persist::config::GameType::Pit,
Self::TestMode => crate::persist::config::GameType::TestMode,
Self::SinglePlayer => crate::persist::config::GameType::SinglePlayer,
Self::TeamDeathmatch => crate::persist::config::GameType::TeamDeathmatch,
Self::Campaign => crate::persist::config::GameType::Campaign,
}
}
}
fn default_game_modes() -> GameModes {
GameModes {
battle_arena: GameMode {
@@ -200,3 +320,131 @@ fn default_campaigns() -> super::Campaigns {
]
}
}
fn default_rotation() -> GameEventSequence {
GameEventSequence {
strategy: GameRotationStrategy::Sequence,
modes: vec![
GameEvents {
singleplayer: GameEvent {
map: GameMap::Neptune1,
visibility: GameVisibility::Good,
mode: GameType::SuddenDeath,
auto_heal: true,
},
multiplayer: GameEvent {
map: GameMap::Neptune3,
visibility: GameVisibility::Poor,
mode: GameType::BattleArena,
auto_heal: true,
},
duration_s: 5*60, // 5 minutes
},
GameEvents {
singleplayer: GameEvent {
map: GameMap::Neptune2,
visibility: GameVisibility::Good,
mode: GameType::SuddenDeath,
auto_heal: true,
},
multiplayer: GameEvent {
map: GameMap::Mars1,
visibility: GameVisibility::Poor,
mode: GameType::Pit,
auto_heal: true,
},
duration_s: 5*60,
},
GameEvents {
singleplayer: GameEvent {
map: GameMap::Neptune3,
visibility: GameVisibility::Good,
mode: GameType::SuddenDeath,
auto_heal: true,
},
multiplayer: GameEvent {
map: GameMap::Mars1,
visibility: GameVisibility::Poor,
mode: GameType::TestMode,
auto_heal: true,
},
duration_s: 5*60,
},
GameEvents {
singleplayer: GameEvent {
map: GameMap::Mars1,
visibility: GameVisibility::Good,
mode: GameType::SuddenDeath,
auto_heal: true,
},
multiplayer: GameEvent {
map: GameMap::Neptune3,
visibility: GameVisibility::Poor,
mode: GameType::BattleArena,
auto_heal: true,
},
duration_s: 5*60,
},
GameEvents {
singleplayer: GameEvent {
map: GameMap::Mars2,
visibility: GameVisibility::Good,
mode: GameType::SuddenDeath,
auto_heal: true,
},
multiplayer: GameEvent {
map: GameMap::Mars1,
visibility: GameVisibility::Poor,
mode: GameType::Pit,
auto_heal: true,
},
duration_s: 5*60,
},
GameEvents {
singleplayer: GameEvent {
map: GameMap::Mars3,
visibility: GameVisibility::Good,
mode: GameType::SuddenDeath,
auto_heal: true,
},
multiplayer: GameEvent {
map: GameMap::Mars1,
visibility: GameVisibility::Poor,
mode: GameType::TestMode,
auto_heal: true,
},
duration_s: 5*60,
},
GameEvents {
singleplayer: GameEvent {
map: GameMap::Earth1,
visibility: GameVisibility::Good,
mode: GameType::SuddenDeath,
auto_heal: true,
},
multiplayer: GameEvent {
map: GameMap::Mars1,
visibility: GameVisibility::Poor,
mode: GameType::Pit,
auto_heal: true,
},
duration_s: 5*60,
},
GameEvents {
singleplayer: GameEvent {
map: GameMap::Earth2,
visibility: GameVisibility::Good,
mode: GameType::SuddenDeath,
auto_heal: true,
},
multiplayer: GameEvent {
map: GameMap::Mars1,
visibility: GameVisibility::Poor,
mode: GameType::TestMode,
auto_heal: true,
},
duration_s: 5*60,
},
]
}
}

View File

@@ -288,4 +288,19 @@ impl <C: Clone> super::ConfigProvider<C> for CubeConfig {
commands: self.chat.commands.clone(),
}
}
fn gamemode_events(&self) -> super::GameEventSequence {
let strategy = self.battle.rotation.strategy.into_conf();
let first = strategy.next(self.battle.rotation.modes.len() - 1, self.battle.rotation.modes.len());
super::GameEventSequence {
strategy: self.battle.rotation.strategy.into_conf(),
modes: self.battle.rotation.modes.clone().into_iter().map(|event| super::GameEvents {
singleplayer: event.singleplayer.into_conf(),
multiplayer: event.multiplayer.into_conf(),
duration: std::time::Duration::from_secs(event.duration_s),
}).collect(),
index: first,
started: chrono::Utc::now().timestamp(),
}
}
}

View File

@@ -2,7 +2,7 @@ mod cubes_json;
pub use cubes_json::CubeConfig;
mod traits;
pub use traits::{ConfigProvider, CompleteCampaignProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig};
pub use traits::{ConfigProvider, CompleteCampaignProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType};
pub type ConfigImpl = CubeConfig;

View File

@@ -24,6 +24,7 @@ pub trait ConfigProvider<C: Clone> {
async fn factory(&self) -> Result<crate::factory::Factory, Box<dyn std::error::Error + 'static>>;
fn cubes(&self) -> &'_ std::collections::HashMap<String, crate::persist::Cube>;
fn chat_system_config(&self) -> ChatSystemConfig;
fn gamemode_events(&self) -> GameEventSequence;
}
pub struct CompleteCampaignProvider {
@@ -128,3 +129,131 @@ pub struct ChatSystemConfig {
pub command_channel: String,
pub commands: Vec<crate::persist::ChatCommand>,
}
#[derive(Clone, Debug)]
pub struct GameEventSequence {
pub strategy: GameRotationStrategy,
pub modes: Vec<GameEvents>,
pub index: usize,
pub started: i64,
}
impl GameEventSequence {
pub fn now(&mut self) -> GameEventTransmissible {
let time_now = chrono::Utc::now().timestamp();
let mut item_now = &self.modes[self.index];
if time_now >= (item_now.duration.as_secs() as i64) + self.started {
// needs refresh
self.index = self.strategy.next(self.index, self.modes.len());
item_now = &self.modes[self.index];
self.started = time_now;
}
let remaining_ticks = ((item_now.duration.as_secs() as i64) - (time_now - self.started)) * 10_000_000;
GameEventTransmissible {
maps: Typed::Arr(polariton::operation::Arr {
ty: polariton::serdes::TypePrefix::Str,
items: vec![
Typed::Str(crate::data::game_mode::GameMap::from_persist(item_now.singleplayer.map).as_str().into()),
Typed::Str(crate::data::game_mode::GameMap::from_persist(item_now.multiplayer.map).as_str().into()),
],
}),
visibilities: Typed::Arr(polariton::operation::Arr {
ty: polariton::serdes::TypePrefix::Int,
items: vec![
Typed::Int(crate::data::game_mode::MapVisibility::from_persist(item_now.singleplayer.visibility) as _),
Typed::Int(crate::data::game_mode::MapVisibility::from_persist(item_now.multiplayer.visibility) as _),
],
}),
modes: Typed::Arr(polariton::operation::Arr {
ty: polariton::serdes::TypePrefix::Int,
items: vec![
Typed::Int(crate::data::game_mode::GameMode::from_persist(item_now.singleplayer.mode) as _),
Typed::Int(crate::data::game_mode::GameMode::from_persist(item_now.multiplayer.mode) as _),
],
}),
auto_heals: Typed::Arr(polariton::operation::Arr {
ty: polariton::serdes::TypePrefix::Bool,
items: vec![
Typed::Bool(item_now.singleplayer.auto_heal),
Typed::Bool(item_now.multiplayer.auto_heal),
],
}),
remaining_ticks: Typed::Long(remaining_ticks),
}
}
}
pub struct GameEventTransmissible {
pub maps: Typed,
pub visibilities: Typed,
pub modes: Typed,
pub auto_heals: Typed,
pub remaining_ticks: Typed,
}
#[derive(Clone, Debug)]
pub enum GameRotationStrategy {
Sequence,
Random,
}
impl GameRotationStrategy {
pub(super) fn next(&self, last: usize, count: usize) -> usize {
match self {
Self::Sequence => (last+1) % count,
Self::Random => {
use rand::prelude::*;
let mut rng = rand::rng();
let num: u64 = rng.random();
let num = num.clamp(0, usize::MAX as u64) as usize;
num % count
}
}
}
}
#[derive(Clone, Debug)]
pub struct GameEvents {
pub singleplayer: GameEvent,
pub multiplayer: GameEvent,
pub duration: std::time::Duration,
}
#[derive(Clone, Debug)]
pub struct GameEvent {
pub map: GameMap,
pub visibility: GameVisibility,
pub mode: GameType,
pub auto_heal: bool,
}
#[derive(Clone, Debug, Copy)]
pub enum GameMap {
Mars1,
Mars2,
Mars3,
Neptune1,
Neptune2,
Neptune3,
Earth1,
Earth2,
}
#[derive(Clone, Debug, Copy)]
pub enum GameVisibility {
Good,
Poor,
Bad,
}
#[derive(Clone, Debug, Copy)]
pub enum GameType {
BattleArena,
SuddenDeath,
Pit,
TestMode,
SinglePlayer,
TeamDeathmatch,
Campaign,
}

View File

@@ -1,38 +1,6 @@
#![allow(dead_code)]
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum GameMode {
BattleArena = 0,
SuddenDeath = 1,
Pit = 2,
TestMode = 3,
SinglePlayer = 4,
TeamDeathmatch = 5,
Campaign = 6,
}
impl GameMode {
pub fn as_str(&self) -> &'static str {
match self {
GameMode::BattleArena => "BattleArena",
GameMode::SuddenDeath => "SuddenDeath",
GameMode::Pit => "Pit",
GameMode::TestMode => "TestMode",
GameMode::SinglePlayer => "SinglePlayerTDM",
GameMode::TeamDeathmatch => "TeamDeathmatch",
GameMode::Campaign => "Campaign",
}
}
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum MapVisibility {
Good = 0,
Poor = 1,
Bad = 2, // VeryPoor
}
pub use rc_core::data::game_mode::GameMode;
#[repr(u8)]
#[derive(Copy, Clone)]

View File

@@ -1,7 +1,7 @@
use polariton_server::operations::SimpleFunc;
use polariton::{operation::{Arr, ParameterTable, Typed}, serdes::TypePrefix};
use polariton_server::operations::{Operation, OperationCode};
//use polariton::{operation::{Arr, ParameterTable, Typed}, serdes::TypePrefix};
use crate::data::custom_games::*;
const CODE: u8 = 24;
const MAP_NAMES_PARAM_KEY: u8 = 78;
const VISIBILITY_PARAM_KEY: u8 = 66;
@@ -20,38 +20,40 @@ RC_Planet_Mars_01_CTF
RC_Planet_Neptune_01_CTF
*/
pub(super) fn event_system_params_provider() -> SimpleFunc<24, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> {
SimpleFunc::new(|params, _| {
pub struct GameEventsParamsProvider {
sequence: std::sync::Mutex<rc_core::persist::config::GameEventSequence>,
}
#[async_trait::async_trait]
impl Operation<()> for GameEventsParamsProvider {
type User = crate::UserTy;
async fn handle_async(&self, params: polariton::operation::ParameterTable<()>, _user: &Self::User) -> polariton::operation::OperationResponse<()> {
let mut params = params.to_dict();
params.insert(MAP_NAMES_PARAM_KEY, Typed::Arr(Arr {
ty: TypePrefix::Str, // str
items: vec![
Typed::Str("RC_Planet_Neptune_03_BA".into()),
Typed::Str("RC_Planet_Mars_02_BA".into()),
],
}));
params.insert(VISIBILITY_PARAM_KEY, Typed::Arr(Arr {
ty: TypePrefix::Int, // int
items: vec![
Typed::Int(MapVisibility::Good as _),
Typed::Int(MapVisibility::Bad as _),
],
}));
params.insert(MODE_PARAM_KEY, Typed::Arr(Arr {
ty: TypePrefix::Int, // int
items: vec![
Typed::Int(GameMode::SinglePlayer as _),
Typed::Int(GameMode::BattleArena as _),
],
}));
params.insert(AUTO_HEAL_PARAM_KEY, Typed::Arr(Arr {
ty: TypePrefix::Bool, // bool
items: vec![
Typed::Bool(true.into()),
Typed::Bool(false.into()),
],
}));
params.insert(REMAINING_TICKS_PARAM_KEY, Typed::Long(24 * 60 * 60 * 1_000_000 * 10 /* 24 hours in ticks (100ns units) */));
Ok(params.into())
})
let current_mode = self.sequence.lock().unwrap().now();
params.insert(MAP_NAMES_PARAM_KEY, current_mode.maps);
params.insert(VISIBILITY_PARAM_KEY, current_mode.visibilities);
params.insert(MODE_PARAM_KEY, current_mode.modes);
params.insert(AUTO_HEAL_PARAM_KEY, current_mode.auto_heals);
params.insert(REMAINING_TICKS_PARAM_KEY, current_mode.remaining_ticks);
polariton::operation::OperationResponse {
code: Self::op_code(),
return_code: 0,
message: polariton::operation::Typed::Null,
params: params.into(),
}
}
}
impl OperationCode for GameEventsParamsProvider {
fn op_code() -> u8 {
CODE
}
}
pub(super) fn event_system_params_provider(conf: &rc_core::ConfigImpl) -> GameEventsParamsProvider {
let game_seq = <rc_core::ConfigImpl as rc_core::ConfigProvider<()>>::gamemode_events(conf);
GameEventsParamsProvider {
sequence: std::sync::Mutex::new(game_seq),
}
}

View File

@@ -144,7 +144,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
.add(custom_game_session::get_custom_session_provider())
.add(user_xp::get_user_xp_provider())
.add(garage_upgrades::garage_upgrades_provider(&init_ctx.cubes))
.add(game_event_params::event_system_params_provider())
.add(game_event_params::event_system_params_provider(&init_ctx.cubes))
.add(garage_bay_uuid::garage_id_provider())
.add(tech_tree_data::tech_tree_layout_provider(&init_ctx.cubes))
.add(item_shop_bundles::item_bundle_provider())