push messages to the client during a battle
the client never receives individual commands in a battle - SectorManager has receiveSectorState, receiveCompressedSectorState and a heartbeat, and nothing else. so everything the opponent does has to reach the player inside server state, which means the server needs to be able to speak first. it could not: the rpc only ever answered. the gateway now runs a ticker for the length of a battle, calling a new battle_tick on the service five times a second and writing whatever it returns straight to the socket. the simulation stays in the service and the socket stays in the gateway. the first rider is emotes. the player's SendBattleEventMessage reaches the service instead of being logged and dropped, and the bot answers with a taunt of its own; it also sends one unprompted every twelve to thirty seconds, drawn from the rows of taunts.csv that TauntMenu marks as usable. the reply carries the opponent account so it renders on their side of the arena.
This commit is contained in:
parent
d6cfc81c71
commit
2ea283cdda
10 changed files with 260 additions and 26 deletions
|
|
@ -1,22 +1,78 @@
|
|||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
use logic::battle::{LogicGameMode, LogicGameObjectEntry, BATTLE_TICKS_PER_SECOND};
|
||||
use logic::LogicDataRef;
|
||||
use logic::battle::LogicBattleEvent;
|
||||
use logic::{BattleEventMessage, LogicDataRef, LogicRandom};
|
||||
use titan::LogicLong;
|
||||
use service_rpc::AccountRef;
|
||||
use service_rpc::{AccountRef, WireMessage};
|
||||
use tokio::sync::Mutex;
|
||||
pub struct BattleSession {
|
||||
mode: LogicGameMode,
|
||||
tick: i32,
|
||||
queued: Vec<(i32, Vec<LogicGameObjectEntry>)>,
|
||||
outbound: Vec<WireMessage>,
|
||||
started_at: Instant,
|
||||
random: LogicRandom,
|
||||
taunts: Vec<LogicDataRef>,
|
||||
next_bot_emote: i32,
|
||||
}
|
||||
impl BattleSession {
|
||||
pub fn new(mode: LogicGameMode) -> Self {
|
||||
pub fn new(mode: LogicGameMode, taunts: Vec<LogicDataRef>) -> Self {
|
||||
let mut random = LogicRandom::new(mode.random_seed);
|
||||
let next_bot_emote = Self::roll_emote_tick(&mut random, 0);
|
||||
Self {
|
||||
mode,
|
||||
tick: 0,
|
||||
queued: Vec::new(),
|
||||
outbound: Vec::new(),
|
||||
started_at: Instant::now(),
|
||||
random,
|
||||
taunts,
|
||||
next_bot_emote,
|
||||
}
|
||||
}
|
||||
fn roll_emote_tick(random: &mut LogicRandom, from: i32) -> i32 {
|
||||
let span = crate::bot::BOT_EMOTE_MAX_SECONDS - crate::bot::BOT_EMOTE_MIN_SECONDS;
|
||||
let seconds = crate::bot::BOT_EMOTE_MIN_SECONDS + random.next(span.max(1));
|
||||
from + seconds * BATTLE_TICKS_PER_SECOND
|
||||
}
|
||||
pub fn opponent_account(&self) -> LogicLong {
|
||||
self.mode
|
||||
.battle
|
||||
.account_ids
|
||||
.get(1)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
pub fn bot_emote(&mut self) -> Option<LogicBattleEvent> {
|
||||
if self.taunts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let index = self.random.next(self.taunts.len() as i32).max(0) as usize;
|
||||
let taunt = self.taunts.get(index)?;
|
||||
let instance = taunt.global_id().map(|id| id.instance_id).unwrap_or(0);
|
||||
Some(LogicBattleEvent {
|
||||
event_type: 1,
|
||||
account_id: self.opponent_account(),
|
||||
ticks: vec![self.tick],
|
||||
coords: Vec::new(),
|
||||
params: vec![instance],
|
||||
})
|
||||
}
|
||||
pub fn elapsed_tick(&self) -> i32 {
|
||||
let millis = self.started_at.elapsed().as_millis() as i64;
|
||||
(millis * BATTLE_TICKS_PER_SECOND as i64 / 1000) as i32
|
||||
}
|
||||
pub fn advance_to_now(&mut self) {
|
||||
let tick = self.elapsed_tick();
|
||||
self.advance_to(tick);
|
||||
}
|
||||
pub fn push_outbound(&mut self, message: WireMessage) {
|
||||
self.outbound.push(message);
|
||||
}
|
||||
pub fn take_outbound(&mut self) -> Vec<WireMessage> {
|
||||
std::mem::take(&mut self.outbound)
|
||||
}
|
||||
pub fn mode(&self) -> &LogicGameMode {
|
||||
&self.mode
|
||||
}
|
||||
|
|
@ -36,6 +92,14 @@ impl BattleSession {
|
|||
while self.tick < tick {
|
||||
self.tick += 1;
|
||||
self.release_queued(self.tick);
|
||||
if self.tick >= self.next_bot_emote {
|
||||
self.next_bot_emote = Self::roll_emote_tick(&mut self.random, self.tick);
|
||||
if let Some(event) = self.bot_emote() {
|
||||
if let Ok(message) = crate::wire::encode(&BattleEventMessage::new(event)) {
|
||||
self.outbound.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mode.time.tick = self.tick;
|
||||
}
|
||||
|
|
@ -95,15 +159,40 @@ pub struct BattleRegistry {
|
|||
sessions: Mutex<HashMap<AccountRef, BattleSession>>,
|
||||
}
|
||||
impl BattleRegistry {
|
||||
pub async fn start(&self, account: AccountRef, mode: LogicGameMode) {
|
||||
pub async fn start(&self, account: AccountRef, mode: LogicGameMode, taunts: Vec<LogicDataRef>) {
|
||||
self.sessions
|
||||
.lock()
|
||||
.await
|
||||
.insert(account, BattleSession::new(mode));
|
||||
.insert(account, BattleSession::new(mode, taunts));
|
||||
}
|
||||
pub async fn finish(&self, account: AccountRef) -> Option<BattleSession> {
|
||||
self.sessions.lock().await.remove(&account)
|
||||
}
|
||||
pub async fn tick(&self, account: AccountRef) -> Vec<WireMessage> {
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
let Some(session) = sessions.get_mut(&account) else {
|
||||
return Vec::new();
|
||||
};
|
||||
session.advance_to_now();
|
||||
session.take_outbound()
|
||||
}
|
||||
pub async fn answer_emote(&self, account: AccountRef) {
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
let Some(session) = sessions.get_mut(&account) else {
|
||||
return;
|
||||
};
|
||||
let Some(event) = session.bot_emote() else {
|
||||
return;
|
||||
};
|
||||
if let Ok(message) = crate::wire::encode(&BattleEventMessage::new(event)) {
|
||||
session.push_outbound(message);
|
||||
}
|
||||
}
|
||||
pub async fn send(&self, account: AccountRef, message: WireMessage) {
|
||||
if let Some(session) = self.sessions.lock().await.get_mut(&account) {
|
||||
session.push_outbound(message);
|
||||
}
|
||||
}
|
||||
pub async fn advance(&self, account: AccountRef, tick: i32) -> Option<BattleProgress> {
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
let session = sessions.get_mut(&account)?;
|
||||
|
|
|
|||
|
|
@ -2,8 +2,23 @@ use logic::model::{LogicClientAvatar, LogicSpell, LogicSpellDeck, DECK_SLOT_COUN
|
|||
use logic::{table, LogicDataRef, LogicDataTables};
|
||||
use titan::LogicLong;
|
||||
pub const BOT_NAME: &str = "Bot";
|
||||
pub const TAUNT_MENU_COLUMN: &str = "TauntMenu";
|
||||
pub const BOT_EMOTE_MIN_SECONDS: i32 = 12;
|
||||
pub const BOT_EMOTE_MAX_SECONDS: i32 = 30;
|
||||
pub const DECK_SPELLS_COLUMN: &str = "Spells";
|
||||
pub const DECK_LEVEL_COLUMN: &str = "SpellLevel";
|
||||
pub fn menu_taunts() -> Vec<LogicDataRef> {
|
||||
LogicDataTables::instance()
|
||||
.table(table::TAUNTS)
|
||||
.map(|rows| {
|
||||
rows.iter()
|
||||
.filter(|row| row.boolean(TAUNT_MENU_COLUMN))
|
||||
.cloned()
|
||||
.map(LogicDataRef::from)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
pub struct BotPlayer {
|
||||
pub avatar: LogicClientAvatar,
|
||||
pub deck: LogicSpellDeck,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub mod service;
|
|||
pub mod shop;
|
||||
pub mod store;
|
||||
pub mod time;
|
||||
pub mod wire;
|
||||
pub use battle::BattleBuilder;
|
||||
pub use battle_session::{BattleProgress, BattleRegistry, BattleSession};
|
||||
pub use bot::BotPlayer;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use logic::table;
|
|||
use logic::{
|
||||
AvailableServerCommandMessage, EndClientTurnMessage, LogicCommandManager, LogicDataRef,
|
||||
LogicShopSeedChangedCommand, OutOfSyncMessage, OwnHomeDataMessage, SectorStateMessage,
|
||||
SendBattleEventMessage,
|
||||
StartMissionMessage, StopHomeLogicMessage,
|
||||
};
|
||||
use service_rpc::{
|
||||
|
|
@ -153,7 +154,9 @@ impl GameService {
|
|||
format!("{} trailing byte(s)", report.trailing)
|
||||
})));
|
||||
}
|
||||
self.running_battles.start(account, battle).await;
|
||||
self.running_battles
|
||||
.start(account, battle, crate::bot::menu_taunts())
|
||||
.await;
|
||||
Ok(vec![encode(&SectorStateMessage::new(snapshot))?])
|
||||
}
|
||||
async fn roller(&self, account: AccountRef) -> RpcResult<RewardRoller> {
|
||||
|
|
@ -180,14 +183,7 @@ fn encode<M>(message: &M) -> RpcResult<WireMessage>
|
|||
where
|
||||
M: MessageMeta + Message,
|
||||
{
|
||||
let payload = message
|
||||
.to_bytes()
|
||||
.map_err(|error| RpcError::Rejected(error.to_string()))?;
|
||||
Ok(WireMessage::new(
|
||||
M::MESSAGE_TYPE,
|
||||
M::MESSAGE_VERSION,
|
||||
payload,
|
||||
))
|
||||
crate::wire::encode(message)
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl GameApi for GameService {
|
||||
|
|
@ -397,6 +393,22 @@ impl GameApi for GameService {
|
|||
}
|
||||
Ok(replies)
|
||||
}
|
||||
async fn battle_tick(&self, account: AccountRef) -> RpcResult<Vec<WireMessage>> {
|
||||
Ok(self.running_battles.tick(account).await)
|
||||
}
|
||||
async fn battle_event(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()> {
|
||||
let Ok(sent) = SendBattleEventMessage::from_bytes(&payload) else {
|
||||
return Ok(());
|
||||
};
|
||||
tracing::debug!(
|
||||
%account,
|
||||
event_type = sent.event.event_type,
|
||||
params = ?sent.event.params,
|
||||
"battle event from the player"
|
||||
);
|
||||
self.running_battles.answer_emote(account).await;
|
||||
Ok(())
|
||||
}
|
||||
async fn disconnect(&self, account: AccountRef) -> RpcResult<()> {
|
||||
self.persist(account).await?;
|
||||
let sessions = self.sessions.session_count().await;
|
||||
|
|
@ -429,6 +441,13 @@ impl RpcService for GameService {
|
|||
GameRequest::EndClientTurn { account, payload } => Ok(GameResponse::messages(
|
||||
self.end_client_turn(account, payload).await?,
|
||||
)),
|
||||
GameRequest::BattleTick { account } => Ok(GameResponse::messages(
|
||||
self.battle_tick(account).await?,
|
||||
)),
|
||||
GameRequest::BattleEvent { account, payload } => {
|
||||
self.battle_event(account, payload).await?;
|
||||
Ok(GameResponse::Empty)
|
||||
}
|
||||
GameRequest::Disconnect { account } => {
|
||||
self.disconnect(account).await?;
|
||||
Ok(GameResponse::Empty)
|
||||
|
|
|
|||
15
crates/game-service/src/wire.rs
Normal file
15
crates/game-service/src/wire.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use service_rpc::{RpcError, RpcResult, WireMessage};
|
||||
use titan::{Message, MessageMeta};
|
||||
pub fn encode<M>(message: &M) -> RpcResult<WireMessage>
|
||||
where
|
||||
M: MessageMeta + Message,
|
||||
{
|
||||
let payload = message
|
||||
.to_bytes()
|
||||
.map_err(|error| RpcError::Rejected(error.to_string()))?;
|
||||
Ok(WireMessage::new(
|
||||
M::MESSAGE_TYPE,
|
||||
M::MESSAGE_VERSION,
|
||||
payload,
|
||||
))
|
||||
}
|
||||
|
|
@ -122,6 +122,19 @@ impl GameApi for RemoteGame {
|
|||
.await?
|
||||
.into_messages())
|
||||
}
|
||||
async fn battle_tick(&self, account: AccountRef) -> RpcResult<Vec<WireMessage>> {
|
||||
Ok(self
|
||||
.client
|
||||
.call(&GameRequest::BattleTick { account })
|
||||
.await?
|
||||
.into_messages())
|
||||
}
|
||||
async fn battle_event(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()> {
|
||||
self.client
|
||||
.call(&GameRequest::BattleEvent { account, payload })
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn disconnect(&self, account: AccountRef) -> RpcResult<()> {
|
||||
self.client
|
||||
.call(&GameRequest::Disconnect { account })
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::task::JoinHandle;
|
||||
use logic::{
|
||||
message_type, CancelMatchmakeDoneMessage, ClientCapabilitiesMessage, GoHomeMessage,
|
||||
KeepAliveServerMessage, LoginFailedMessage, LoginMessage, LoginOkMessage, SendBattleEventMessage,
|
||||
ServerErrorMessage,
|
||||
KeepAliveServerMessage, LoginFailedMessage, LoginMessage, LoginOkMessage, ServerErrorMessage,
|
||||
};
|
||||
use service_rpc::{
|
||||
AccountRef, DeviceInfo, HomeRequestKind, LoginOutcome, Session as AuthSession, WireMessage,
|
||||
|
|
@ -21,12 +22,19 @@ pub enum RoutingError {
|
|||
Unauthenticated(u16),
|
||||
}
|
||||
type RoutingResult<T> = Result<T, RoutingError>;
|
||||
pub const BATTLE_TICK_INTERVAL: Duration = Duration::from_millis(200);
|
||||
pub struct MessageManager {
|
||||
peer: SocketAddr,
|
||||
config: Arc<GatewayConfig>,
|
||||
backends: Arc<Backends>,
|
||||
sender: MessagingSender,
|
||||
account: Option<AccountRef>,
|
||||
battle_ticker: Option<JoinHandle<()>>,
|
||||
}
|
||||
impl Drop for MessageManager {
|
||||
fn drop(&mut self) {
|
||||
self.stop_battle_ticker();
|
||||
}
|
||||
}
|
||||
impl MessageManager {
|
||||
pub fn new(
|
||||
|
|
@ -41,6 +49,7 @@ impl MessageManager {
|
|||
backends,
|
||||
sender,
|
||||
account: None,
|
||||
battle_ticker: None,
|
||||
}
|
||||
}
|
||||
pub fn account(&self) -> Option<AccountRef> {
|
||||
|
|
@ -87,16 +96,20 @@ impl MessageManager {
|
|||
}
|
||||
message_type::GO_HOME => {
|
||||
let _ = incoming.downcast::<GoHomeMessage>();
|
||||
self.stop_battle_ticker();
|
||||
self.push_home(HomeRequestKind::GoHome).await
|
||||
}
|
||||
message_type::SEND_BATTLE_EVENT => {
|
||||
if let Some(sent) = incoming.downcast::<SendBattleEventMessage>() {
|
||||
tracing::debug!(
|
||||
peer = %self.peer,
|
||||
event_type = sent.event.event_type,
|
||||
params = ?sent.event.params,
|
||||
"battle event"
|
||||
);
|
||||
let Some(account) = self.account else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Err(error) = self
|
||||
.backends
|
||||
.game
|
||||
.battle_event(account, incoming.payload)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(peer = %self.peer, %error, "could not deliver the battle event");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -117,7 +130,10 @@ impl MessageManager {
|
|||
.home_logic_stopped(account, incoming.payload)
|
||||
.await
|
||||
{
|
||||
Ok(replies) => self.push_wire_messages(replies).await?,
|
||||
Ok(replies) => {
|
||||
self.push_wire_messages(replies).await?;
|
||||
self.start_battle_ticker();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(peer = %self.peer, %error, "could not match the player");
|
||||
self.sender
|
||||
|
|
@ -137,7 +153,10 @@ impl MessageManager {
|
|||
.start_mission(account, incoming.payload)
|
||||
.await
|
||||
{
|
||||
Ok(replies) => self.push_wire_messages(replies).await?,
|
||||
Ok(replies) => {
|
||||
self.push_wire_messages(replies).await?;
|
||||
self.start_battle_ticker();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(peer = %self.peer, %error, "could not start the mission");
|
||||
self.sender
|
||||
|
|
@ -264,6 +283,48 @@ impl MessageManager {
|
|||
let messages = self.backends.game.load_home(account, kind).await?;
|
||||
self.push_wire_messages(messages).await
|
||||
}
|
||||
fn start_battle_ticker(&mut self) {
|
||||
let Some(account) = self.account else {
|
||||
return;
|
||||
};
|
||||
self.stop_battle_ticker();
|
||||
let backends = Arc::clone(&self.backends);
|
||||
let sender = self.sender.clone();
|
||||
let peer = self.peer;
|
||||
self.battle_ticker = Some(tokio::spawn(async move {
|
||||
let mut ticker = tokio::time::interval(BATTLE_TICK_INTERVAL);
|
||||
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
if sender.is_closed() {
|
||||
return;
|
||||
}
|
||||
match backends.game.battle_tick(account).await {
|
||||
Ok(messages) => {
|
||||
for message in messages {
|
||||
let outbound = OutboundMessage::raw(
|
||||
message.message_type,
|
||||
message.message_version,
|
||||
message.payload,
|
||||
);
|
||||
if sender.send(outbound).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%peer, %error, "the battle tick failed, stopping the ticker");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
fn stop_battle_ticker(&mut self) {
|
||||
if let Some(handle) = self.battle_ticker.take() {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
async fn push_wire_messages(&self, messages: Vec<WireMessage>) -> RoutingResult<()> {
|
||||
for message in messages {
|
||||
self.sender
|
||||
|
|
|
|||
|
|
@ -5,3 +5,13 @@ use crate::battle::LogicBattleEvent;
|
|||
pub struct SendBattleEventMessage {
|
||||
pub event: LogicBattleEvent,
|
||||
}
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Message)]
|
||||
#[message(id = 22952, direction = "server", name = "BattleEventMessage")]
|
||||
pub struct BattleEventMessage {
|
||||
pub event: LogicBattleEvent,
|
||||
}
|
||||
impl BattleEventMessage {
|
||||
pub fn new(event: LogicBattleEvent) -> Self {
|
||||
Self { event }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub use login_ok::LoginOkMessage;
|
|||
pub use matchmake::{CancelMatchmakeDoneMessage, HomeLogicStoppedMessage, StopHomeLogicMessage};
|
||||
pub use out_of_sync::OutOfSyncMessage;
|
||||
pub use own_home_data::OwnHomeDataMessage;
|
||||
pub use battle_event::SendBattleEventMessage;
|
||||
pub use battle_event::{BattleEventMessage, SendBattleEventMessage};
|
||||
pub use sector_state::SectorStateMessage;
|
||||
pub use server_error::ServerErrorMessage;
|
||||
pub use start_mission::StartMissionMessage;
|
||||
|
|
@ -52,6 +52,7 @@ pub mod message_type {
|
|||
pub const HOME_LOGIC_STOPPED: u16 = 14105;
|
||||
pub const CANCEL_MATCHMAKE: u16 = 14107;
|
||||
pub const SEND_BATTLE_EVENT: u16 = 12951;
|
||||
pub const BATTLE_EVENT: u16 = 22952;
|
||||
pub const SERVER_HELLO: u16 = 20100;
|
||||
pub const LOGIN_FAILED: u16 = 20103;
|
||||
pub const LOGIN_OK: u16 = 20104;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ pub enum GameRequest {
|
|||
#[serde(with = "crate::base64::serde_bytes")]
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
BattleTick {
|
||||
account: AccountRef,
|
||||
},
|
||||
BattleEvent {
|
||||
account: AccountRef,
|
||||
#[serde(with = "crate::base64::serde_bytes")]
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
Disconnect {
|
||||
account: AccountRef,
|
||||
},
|
||||
|
|
@ -97,5 +105,7 @@ pub trait GameApi: Send + Sync + 'static {
|
|||
account: AccountRef,
|
||||
payload: Vec<u8>,
|
||||
) -> RpcResult<Vec<WireMessage>>;
|
||||
async fn battle_tick(&self, account: AccountRef) -> RpcResult<Vec<WireMessage>>;
|
||||
async fn battle_event(&self, account: AccountRef, payload: Vec<u8>) -> RpcResult<()>;
|
||||
async fn disconnect(&self, account: AccountRef) -> RpcResult<()>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue