the client never asks for the state, so the objects it decoded before the battle screen installed its listener were stuck without models for the whole battle - and every later snapshot matched them by global id and reused them, so the "newly created" flag the visual depends on was never set again. the first sector command is the client telling us it is up. at that point every object is handed a fresh id: nothing matches, so the client builds them all from scratch and they get models. the leaders and the tower lists are remapped with them and the column is re-sorted, since the client binary searches it. the battle itself is complete as of this run: a princess tower fell and the first crown was scored, towers (2, 1) and stars (1, 0) in the log.
459 lines
16 KiB
Rust
459 lines
16 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
pub const SNAPSHOT_INTERVAL_TICKS: i32 = 4;
|
|
pub const MAX_CATCH_UP_TICKS: i32 = 40;
|
|
use std::time::Instant;
|
|
use logic::battle::{
|
|
LogicGameMode, LogicGameObjectEntry, LogicGameObjectRef, LogicVector2,
|
|
BATTLE_TICKS_PER_SECOND, BATTLE_TYPE_PVP, CHARACTER_OBJECT_TYPE,
|
|
};
|
|
use logic::battle::{verify_snapshot, LogicBattleEvent};
|
|
use logic::SectorStateMessage;
|
|
use logic::{BattleEventMessage, LogicDataRef, LogicRandom};
|
|
use titan::LogicLong;
|
|
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,
|
|
next_snapshot: i32,
|
|
next_bot_play: i32,
|
|
pub pushed_snapshots: u32,
|
|
next_instance: i32,
|
|
announced: bool,
|
|
pending_bot_play: Option<(LogicDataRef, LogicVector2, i32)>,
|
|
}
|
|
impl BattleSession {
|
|
pub fn new(mode: LogicGameMode, taunts: Vec<LogicDataRef>) -> Self {
|
|
let first_instance = mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.filter_map(|entry| entry.global_id.0.map(|id| id.instance_id))
|
|
.max()
|
|
.map(|last| last + 1)
|
|
.unwrap_or(0);
|
|
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,
|
|
next_snapshot: SNAPSHOT_INTERVAL_TICKS,
|
|
next_bot_play: crate::bot::BOT_OPENING_DELAY_SECONDS * BATTLE_TICKS_PER_SECOND,
|
|
pushed_snapshots: 0,
|
|
next_instance: first_instance,
|
|
announced: false,
|
|
pending_bot_play: None,
|
|
}
|
|
}
|
|
pub fn reannounce(&mut self) {
|
|
let mut remap: Vec<(LogicGameObjectRef, LogicGameObjectRef)> = Vec::new();
|
|
for entry in self.mode.battle.objects.objects.iter_mut() {
|
|
let fresh = LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, self.next_instance);
|
|
self.next_instance += 1;
|
|
remap.push((entry.global_id, fresh));
|
|
entry.global_id = fresh;
|
|
}
|
|
let translate = |old: &LogicGameObjectRef| -> Option<LogicGameObjectRef> {
|
|
remap
|
|
.iter()
|
|
.find(|(was, _)| was == old)
|
|
.map(|(_, now)| *now)
|
|
};
|
|
for leader in self.mode.battle.leaders.iter_mut() {
|
|
if let Some(now) = translate(leader) {
|
|
*leader = now;
|
|
}
|
|
}
|
|
for towers in self.mode.battle.leader_towers.iter_mut() {
|
|
for tower in towers.iter_mut() {
|
|
if let Some(now) = translate(tower) {
|
|
*tower = now;
|
|
}
|
|
}
|
|
}
|
|
self.mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.sort_by_key(|entry| entry.global_id.0.map(|id| (id.class_id, id.instance_id)));
|
|
self.next_snapshot = self.tick;
|
|
}
|
|
pub fn take_bot_play(&mut self) -> Option<(LogicDataRef, LogicVector2, i32)> {
|
|
self.pending_bot_play.take()
|
|
}
|
|
pub fn bot_play(&mut self) -> Option<(LogicDataRef, LogicVector2, i32)> {
|
|
let deck = self.mode.battle.decks.get(1)?.as_ref()?;
|
|
let filled: Vec<LogicDataRef> = deck
|
|
.slots
|
|
.iter()
|
|
.flatten()
|
|
.map(|spell| spell.data.clone())
|
|
.collect();
|
|
if filled.is_empty() {
|
|
return None;
|
|
}
|
|
let card = filled
|
|
.get(self.random.next(filled.len() as i32).max(0) as usize)?
|
|
.clone();
|
|
let towers: Vec<(i32, i32)> = self
|
|
.mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.filter(|entry| entry.owner_index() == crate::bot::BOT_OWNER_INDEX)
|
|
.map(|entry| entry.position())
|
|
.collect();
|
|
let leader = self.mode.battle.leader(crate::bot::BOT_OWNER_INDEX as usize)?;
|
|
let (king_x, king_y) = leader.position();
|
|
let ahead = towers
|
|
.iter()
|
|
.map(|(_, y)| *y)
|
|
.fold(king_y, |best, y| if (y - king_y).abs() > (best - king_y).abs() { y } else { best });
|
|
let lane_x = towers
|
|
.iter()
|
|
.filter(|(_, y)| *y == ahead)
|
|
.map(|(x, _)| *x)
|
|
.collect::<Vec<_>>();
|
|
let x = lane_x
|
|
.get(self.random.next(lane_x.len().max(1) as i32).max(0) as usize)
|
|
.copied()
|
|
.unwrap_or(king_x);
|
|
Some((card, LogicVector2::new(x, ahead), self.next_instance()))
|
|
}
|
|
pub fn pushes_snapshots(&self) -> bool {
|
|
self.mode.battle.battle_type == BATTLE_TYPE_PVP
|
|
}
|
|
pub fn mana(&self) -> Option<i32> {
|
|
self.mode.battle.objects.objects.iter().find_map(|entry| {
|
|
match &entry.body {
|
|
logic::battle::LogicObjectBody::Summoner(summoner)
|
|
if entry.owner_index() == 0 =>
|
|
{
|
|
Some(summoner.mana)
|
|
}
|
|
_ => None,
|
|
}
|
|
})
|
|
}
|
|
pub fn snapshot_message(&mut self) -> Option<WireMessage> {
|
|
let snapshot = self.mode.snapshot().ok()?;
|
|
let report = verify_snapshot(&snapshot);
|
|
let unit = self
|
|
.mode
|
|
.battle
|
|
.objects
|
|
.objects
|
|
.iter()
|
|
.find(|entry| entry.stats().speed > 0)
|
|
.map(|entry| (entry.position(), entry.owner_index()));
|
|
tracing::debug!(
|
|
tick = self.tick,
|
|
bytes = snapshot.len(),
|
|
ok = report.is_ok(),
|
|
unit = ?unit,
|
|
"battle snapshot"
|
|
);
|
|
if !report.is_ok() {
|
|
tracing::error!(
|
|
bytes = snapshot.len(),
|
|
trailing = report.trailing,
|
|
error = report.error.as_deref().unwrap_or("-"),
|
|
"the battle snapshot does not read back, not pushing it"
|
|
);
|
|
return None;
|
|
}
|
|
crate::wire::encode(&SectorStateMessage::new(snapshot)).ok()
|
|
}
|
|
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
|
|
}
|
|
pub fn tick(&self) -> i32 {
|
|
self.tick
|
|
}
|
|
pub fn seconds(&self) -> i32 {
|
|
self.tick / BATTLE_TICKS_PER_SECOND
|
|
}
|
|
pub fn queue(&mut self, at_tick: i32, entries: Vec<LogicGameObjectEntry>) {
|
|
if entries.is_empty() {
|
|
return;
|
|
}
|
|
self.queued.push((at_tick.max(self.tick + 1), entries));
|
|
}
|
|
pub fn advance_to(&mut self, tick: i32) {
|
|
let tick = tick.min(self.tick + MAX_CATCH_UP_TICKS);
|
|
while self.tick < tick {
|
|
self.tick += 1;
|
|
self.mode.time.tick = self.tick;
|
|
self.release_queued(self.tick);
|
|
self.mode.battle.tick(self.tick);
|
|
if self.pushes_snapshots() && self.tick >= self.next_snapshot {
|
|
self.next_snapshot = self.tick + SNAPSHOT_INTERVAL_TICKS;
|
|
if let Some(message) = self.snapshot_message() {
|
|
self.pushed_snapshots += 1;
|
|
self.outbound.push(message);
|
|
}
|
|
}
|
|
if self.tick >= self.next_bot_play {
|
|
let span = crate::bot::BOT_PLAY_MAX_SECONDS - crate::bot::BOT_PLAY_MIN_SECONDS;
|
|
let wait = crate::bot::BOT_PLAY_MIN_SECONDS + self.random.next(span.max(1));
|
|
self.next_bot_play = self.tick + wait * BATTLE_TICKS_PER_SECOND;
|
|
if let Some((card, position, instance)) = self.bot_play() {
|
|
self.pending_bot_play = Some((card, position, instance));
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fn release_queued(&mut self, tick: i32) {
|
|
let mut due = Vec::new();
|
|
self.queued.retain(|(at, entries)| {
|
|
if *at <= tick {
|
|
due.extend(entries.iter().cloned());
|
|
false
|
|
} else {
|
|
true
|
|
}
|
|
});
|
|
for entry in due {
|
|
self.mode.battle.objects.push(entry);
|
|
}
|
|
}
|
|
pub fn is_finished(&self) -> bool {
|
|
self.mode.battle.is_end_condition_matched(self.tick)
|
|
}
|
|
pub fn stars(&self) -> (i32, i32) {
|
|
(self.mode.battle.stars(0), self.mode.battle.stars(1))
|
|
}
|
|
pub fn checksum(&self) -> Option<i32> {
|
|
self.mode.calculate_checksum().ok()
|
|
}
|
|
pub fn towers_standing(&self) -> (usize, usize) {
|
|
(
|
|
self.mode.battle.leader_towers[0].len(),
|
|
self.mode.battle.leader_towers[1].len(),
|
|
)
|
|
}
|
|
pub fn owner_of(&self, account: LogicLong) -> Option<i32> {
|
|
self.mode
|
|
.battle
|
|
.account_ids
|
|
.iter()
|
|
.position(|id| *id == account)
|
|
.map(|index| index as i32)
|
|
}
|
|
pub fn next_instance(&mut self) -> i32 {
|
|
let instance = self.next_instance;
|
|
self.next_instance += 1;
|
|
instance
|
|
}
|
|
pub fn deck_card(&self, owner: i32, slot: i32) -> Option<LogicDataRef> {
|
|
let deck = self.mode.battle.decks.get(owner.max(0) as usize)?.as_ref()?;
|
|
let spell = deck.slots.get(slot.max(0) as usize)?.as_ref()?;
|
|
Some(spell.data.clone())
|
|
}
|
|
pub fn push(&mut self, entry: LogicGameObjectEntry) {
|
|
self.mode.battle.objects.push(entry);
|
|
}
|
|
pub fn object_count(&self) -> usize {
|
|
self.mode.battle.objects.objects.len()
|
|
}
|
|
}
|
|
#[derive(Default)]
|
|
pub struct BattleRegistry {
|
|
sessions: Mutex<HashMap<AccountRef, Arc<Mutex<BattleSession>>>>,
|
|
}
|
|
impl BattleRegistry {
|
|
pub async fn start(
|
|
&self,
|
|
players: &[AccountRef],
|
|
mode: LogicGameMode,
|
|
taunts: Vec<LogicDataRef>,
|
|
) {
|
|
let session = Arc::new(Mutex::new(BattleSession::new(mode, taunts)));
|
|
let mut sessions = self.sessions.lock().await;
|
|
for account in players {
|
|
sessions.insert(*account, Arc::clone(&session));
|
|
}
|
|
}
|
|
async fn session(&self, account: AccountRef) -> Option<Arc<Mutex<BattleSession>>> {
|
|
self.sessions.lock().await.get(&account).cloned()
|
|
}
|
|
pub async fn is_running(&self, account: AccountRef) -> bool {
|
|
self.sessions.lock().await.contains_key(&account)
|
|
}
|
|
pub async fn finish(&self, account: AccountRef) -> Option<Arc<Mutex<BattleSession>>> {
|
|
self.sessions.lock().await.remove(&account)
|
|
}
|
|
pub async fn tick<F>(&self, account: AccountRef, summon: F) -> Vec<WireMessage>
|
|
where
|
|
F: Fn(&LogicDataRef, LogicVector2, i32, i32) -> Vec<LogicGameObjectEntry>,
|
|
{
|
|
let Some(handle) = self.session(account).await else {
|
|
return Vec::new();
|
|
};
|
|
let mut session = handle.lock().await;
|
|
session.advance_to_now();
|
|
if let Some((card, position, instance)) = session.take_bot_play() {
|
|
let entries = summon(&card, position, crate::bot::BOT_OWNER_INDEX, instance);
|
|
if !entries.is_empty() {
|
|
tracing::debug!(card = %card, count = entries.len(), "the bot played a card");
|
|
for entry in entries {
|
|
session.push(entry);
|
|
}
|
|
}
|
|
}
|
|
session.take_outbound()
|
|
}
|
|
pub async fn answer_emote(&self, account: AccountRef) {
|
|
let Some(handle) = self.session(account).await else {
|
|
return;
|
|
};
|
|
let mut session = handle.lock().await;
|
|
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 resend(&self, account: AccountRef) -> Option<WireMessage> {
|
|
let handle = self.session(account).await?;
|
|
let mut session = handle.lock().await;
|
|
session.reannounce();
|
|
session.snapshot_message()
|
|
}
|
|
pub async fn announce_once(&self, account: AccountRef) -> bool {
|
|
let Some(handle) = self.session(account).await else {
|
|
return false;
|
|
};
|
|
let mut session = handle.lock().await;
|
|
if session.announced {
|
|
return false;
|
|
}
|
|
session.announced = true;
|
|
session.reannounce();
|
|
true
|
|
}
|
|
pub async fn send(&self, account: AccountRef, message: WireMessage) {
|
|
if let Some(handle) = self.session(account).await {
|
|
handle.lock().await.push_outbound(message);
|
|
}
|
|
}
|
|
pub async fn advance(&self, account: AccountRef, tick: i32) -> Option<BattleProgress> {
|
|
let handle = self.session(account).await?;
|
|
let mut session = handle.lock().await;
|
|
session.advance_to(tick);
|
|
Some(BattleProgress {
|
|
seconds: session.seconds(),
|
|
finished: session.is_finished(),
|
|
stars: session.stars(),
|
|
objects: session.object_count(),
|
|
towers: session.towers_standing(),
|
|
checksum: session.checksum(),
|
|
mana: session.mana(),
|
|
pushed: session.pushed_snapshots,
|
|
})
|
|
}
|
|
pub async fn play<F>(
|
|
&self,
|
|
account: AccountRef,
|
|
executor: LogicLong,
|
|
slot: i32,
|
|
position: logic::battle::LogicVector2,
|
|
at_tick: i32,
|
|
summon: F,
|
|
) -> usize
|
|
where
|
|
F: Fn(&LogicDataRef, logic::battle::LogicVector2, i32, i32) -> Vec<LogicGameObjectEntry>,
|
|
{
|
|
let Some(handle) = self.session(account).await else {
|
|
return 0;
|
|
};
|
|
let mut session = handle.lock().await;
|
|
let Some(owner) = session.owner_of(executor) else {
|
|
return 0;
|
|
};
|
|
let Some(card) = session.deck_card(owner, slot) else {
|
|
return 0;
|
|
};
|
|
let entries = summon(&card, position, owner, session.next_instance());
|
|
let spawned = entries.len();
|
|
session.queue(at_tick, entries);
|
|
spawned
|
|
}
|
|
}
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct BattleProgress {
|
|
pub seconds: i32,
|
|
pub finished: bool,
|
|
pub stars: (i32, i32),
|
|
pub objects: usize,
|
|
pub towers: (usize, usize),
|
|
pub checksum: Option<i32>,
|
|
pub mana: Option<i32>,
|
|
pub pushed: u32,
|
|
}
|