encode the movement component and push snapshots
a character with Speed gets a LogicMovementComponent on the client, and until now we had no encoder for it, so any snapshot carrying a unit would have desynced. the layout is four booleans, a path length, that many path nodes, and eighteen more vints - one conditional, no data driven ones. charge time defaults to the -1 the client uses when ChargeRange is empty, which is every card but the Prince. with that in place the session pushes a fresh SectorStateMessage once a second down the battle ticker. every push runs through verify_snapshot first and is dropped rather than sent if it does not read back, the same guard that caught the decks. the verifier learned the movement pass, and lost the leftover SCROLL_BATTLE_SUMMONER switch that the builder had already shed.
This commit is contained in:
parent
054321ad50
commit
3bb923cd17
5 changed files with 136 additions and 15 deletions
|
|
@ -2,7 +2,8 @@ use std::path::{Path, PathBuf};
|
|||
use logic::battle::{
|
||||
LogicBattle, LogicCharacter, LogicCharacterBuffComponent, LogicCombatComponent, LogicComponent,
|
||||
LogicGameMode, LogicGameObject, LogicGameObjectEntry, LogicGameObjectManager,
|
||||
LogicGameObjectRef, LogicHitpointComponent, LogicObjectBody, LogicSummoner, LogicSummonerDeck,
|
||||
LogicGameObjectRef, LogicHitpointComponent, LogicMovementComponent, LogicObjectBody,
|
||||
LogicSummoner, LogicSummonerDeck,
|
||||
LogicTilemap,
|
||||
LogicTime, LogicVector2, BATTLE_TYPE_NPC, CHARACTER_OBJECT_TYPE, COMPONENT_PASSES,
|
||||
DIRECTION_BOTTOM,
|
||||
|
|
@ -92,10 +93,10 @@ impl BattleBuilder {
|
|||
.filter(|value| *value > 0)
|
||||
.unwrap_or(1)
|
||||
}
|
||||
fn components_for(&self, hitpoints: i32) -> [Option<LogicComponent>; COMPONENT_PASSES] {
|
||||
fn components_for(&self, hitpoints: i32, moves: bool) -> [Option<LogicComponent>; COMPONENT_PASSES] {
|
||||
[
|
||||
Some(LogicComponent::Combat(LogicCombatComponent::default())),
|
||||
None,
|
||||
moves.then(|| LogicComponent::Movement(LogicMovementComponent::default())),
|
||||
Some(LogicComponent::Hitpoint(LogicHitpointComponent::healthy(
|
||||
hitpoints,
|
||||
))),
|
||||
|
|
@ -113,6 +114,10 @@ impl BattleBuilder {
|
|||
level_index,
|
||||
} = spec;
|
||||
let hitpoints = Self::hitpoints_of(&data, level_index);
|
||||
let moves = data
|
||||
.data()
|
||||
.map(|row| row.int(CHARACTER_SPEED_COLUMN) > 0)
|
||||
.unwrap_or(false);
|
||||
let character = LogicCharacter {
|
||||
level_index,
|
||||
base: LogicGameObject {
|
||||
|
|
@ -130,7 +135,7 @@ impl BattleBuilder {
|
|||
),
|
||||
..LogicCharacter::default()
|
||||
};
|
||||
let components = self.components_for(hitpoints);
|
||||
let components = self.components_for(hitpoints, moves);
|
||||
LogicGameObjectEntry::new(
|
||||
data,
|
||||
LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, instance),
|
||||
|
|
@ -215,7 +220,7 @@ impl BattleBuilder {
|
|||
data,
|
||||
LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, instance),
|
||||
body,
|
||||
self.components_for(hitpoints),
|
||||
self.components_for(hitpoints, false),
|
||||
)
|
||||
}
|
||||
pub fn build(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use std::collections::HashMap;
|
||||
pub const SNAPSHOT_INTERVAL_TICKS: i32 = 20;
|
||||
use std::time::Instant;
|
||||
use logic::battle::{LogicGameMode, LogicGameObjectEntry, BATTLE_TICKS_PER_SECOND};
|
||||
use logic::battle::LogicBattleEvent;
|
||||
use logic::battle::{verify_snapshot, LogicBattleEvent};
|
||||
use logic::SectorStateMessage;
|
||||
use logic::{BattleEventMessage, LogicDataRef, LogicRandom};
|
||||
use titan::LogicLong;
|
||||
use service_rpc::{AccountRef, WireMessage};
|
||||
|
|
@ -15,6 +17,7 @@ pub struct BattleSession {
|
|||
random: LogicRandom,
|
||||
taunts: Vec<LogicDataRef>,
|
||||
next_bot_emote: i32,
|
||||
next_snapshot: i32,
|
||||
}
|
||||
impl BattleSession {
|
||||
pub fn new(mode: LogicGameMode, taunts: Vec<LogicDataRef>) -> Self {
|
||||
|
|
@ -29,8 +32,23 @@ impl BattleSession {
|
|||
random,
|
||||
taunts,
|
||||
next_bot_emote,
|
||||
next_snapshot: SNAPSHOT_INTERVAL_TICKS,
|
||||
}
|
||||
}
|
||||
fn snapshot_message(&mut self) -> Option<WireMessage> {
|
||||
let snapshot = self.mode.snapshot().ok()?;
|
||||
let report = verify_snapshot(&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));
|
||||
|
|
@ -93,6 +111,12 @@ impl BattleSession {
|
|||
self.tick += 1;
|
||||
self.release_queued(self.tick);
|
||||
self.mode.battle.tick();
|
||||
if self.tick >= self.next_snapshot {
|
||||
self.next_snapshot = self.tick + SNAPSHOT_INTERVAL_TICKS;
|
||||
if let Some(message) = self.snapshot_message() {
|
||||
self.outbound.push(message);
|
||||
}
|
||||
}
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use titan::{ByteStreamWriter, Payload, Result};
|
||||
use crate::battle::logic_game_object::LogicVector2;
|
||||
use crate::battle::logic_game_object_ref::LogicGameObjectRef;
|
||||
pub const COMPONENT_COMBAT: usize = 0;
|
||||
pub const COMPONENT_MOVEMENT: usize = 1;
|
||||
|
|
@ -41,6 +42,86 @@ impl LogicCombatComponent {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
pub const CHARGE_TIME_DISABLED: i32 = -1;
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LogicMovementComponent {
|
||||
pub is_pushed_back: bool,
|
||||
pub is_stopped: bool,
|
||||
pub is_panicking: bool,
|
||||
pub path_node_reached: bool,
|
||||
pub path: Vec<i32>,
|
||||
pub collision_push_count: i32,
|
||||
pub collision_push_sum_x: i32,
|
||||
pub collision_push_sum_y: i32,
|
||||
pub panic_timer: i32,
|
||||
pub panic_target_x: i32,
|
||||
pub panic_target_y: i32,
|
||||
pub panic_origin_x: i32,
|
||||
pub panic_origin_y: i32,
|
||||
pub charge_time: i32,
|
||||
pub pushback_target: LogicVector2,
|
||||
pub avoidance_side_step: i32,
|
||||
pub path_target_normal: LogicVector2,
|
||||
pub pushback_speed: i32,
|
||||
pub move_timer: i32,
|
||||
pub jump_distance: i32,
|
||||
}
|
||||
impl Default for LogicMovementComponent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
is_pushed_back: false,
|
||||
is_stopped: false,
|
||||
is_panicking: false,
|
||||
path_node_reached: false,
|
||||
path: Vec::new(),
|
||||
collision_push_count: 0,
|
||||
collision_push_sum_x: 0,
|
||||
collision_push_sum_y: 0,
|
||||
panic_timer: 0,
|
||||
panic_target_x: 0,
|
||||
panic_target_y: 0,
|
||||
panic_origin_x: 0,
|
||||
panic_origin_y: 0,
|
||||
charge_time: CHARGE_TIME_DISABLED,
|
||||
pushback_target: LogicVector2::default(),
|
||||
avoidance_side_step: 0,
|
||||
path_target_normal: LogicVector2::default(),
|
||||
pushback_speed: 0,
|
||||
move_timer: 0,
|
||||
jump_distance: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl LogicMovementComponent {
|
||||
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||
writer.write_boolean(self.is_pushed_back);
|
||||
writer.write_boolean(self.is_stopped);
|
||||
writer.write_boolean(self.is_panicking);
|
||||
writer.write_boolean(self.path_node_reached);
|
||||
writer.write_vint(self.path.len() as i32);
|
||||
for node in &self.path {
|
||||
writer.write_vint(*node);
|
||||
}
|
||||
writer.write_vint(self.collision_push_count);
|
||||
writer.write_vint(self.collision_push_sum_x);
|
||||
writer.write_vint(self.collision_push_sum_y);
|
||||
writer.write_vint(self.panic_timer);
|
||||
writer.write_vint(self.panic_target_x);
|
||||
writer.write_vint(self.panic_target_y);
|
||||
writer.write_vint(self.panic_origin_x);
|
||||
writer.write_vint(self.panic_origin_y);
|
||||
writer.write_vint(self.charge_time);
|
||||
writer.write_vint(self.pushback_target.x);
|
||||
writer.write_vint(self.pushback_target.y);
|
||||
writer.write_vint(self.avoidance_side_step);
|
||||
writer.write_vint(self.path_target_normal.x);
|
||||
writer.write_vint(self.path_target_normal.y);
|
||||
writer.write_vint(self.pushback_speed);
|
||||
writer.write_vint(self.move_timer);
|
||||
writer.write_vint(self.jump_distance);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct LogicHitpointComponent {
|
||||
pub hitpoints: i32,
|
||||
|
|
@ -114,6 +195,7 @@ impl LogicCharacterBuffComponent {
|
|||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LogicComponent {
|
||||
Combat(LogicCombatComponent),
|
||||
Movement(LogicMovementComponent),
|
||||
Hitpoint(LogicHitpointComponent),
|
||||
Buff(LogicCharacterBuffComponent),
|
||||
}
|
||||
|
|
@ -121,6 +203,7 @@ impl LogicComponent {
|
|||
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||
match self {
|
||||
LogicComponent::Combat(component) => component.encode(writer),
|
||||
LogicComponent::Movement(component) => component.encode(writer),
|
||||
LogicComponent::Hitpoint(component) => component.encode(writer),
|
||||
LogicComponent::Buff(component) => component.encode(writer),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub use logic_character::{
|
|||
LogicCharacter, LogicSummoner, LogicSummonerDeck, DEFAULT_SIZE, DIRECTION_BOTTOM, DIRECTION_TOP,
|
||||
};
|
||||
pub use logic_component::{
|
||||
LogicMovementComponent,
|
||||
LogicCharacterBuffComponent, LogicCombatComponent, LogicComponent, LogicHitpointComponent,
|
||||
COMPONENT_BUFF, COMPONENT_COMBAT, COMPONENT_HITPOINT, COMPONENT_MOVEMENT,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use crate::battle::logic_game_mode::{SECTION_BATTLE, SECTION_TUTORIAL};
|
|||
use crate::data::{table, LogicDataRef, LogicDataTables};
|
||||
use crate::model::DECK_SLOT_COUNT;
|
||||
pub const BUFF_ARRAY_TABLE: i32 = table::DAMAGE_TYPES;
|
||||
pub const MOVEMENT_SPEED_COLUMN: &str = "Speed";
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SnapshotReport {
|
||||
pub steps: Vec<(String, usize)>,
|
||||
|
|
@ -91,6 +92,15 @@ impl<'a, 'b> Verifier<'a, 'b> {
|
|||
self.vints(4)?;
|
||||
Ok(())
|
||||
}
|
||||
fn movement(&mut self) -> Result<()> {
|
||||
for _ in 0..4 {
|
||||
self.reader.read_boolean()?;
|
||||
}
|
||||
let path = self.reader.read_vint()?.max(0) as usize;
|
||||
self.vints(path)?;
|
||||
self.vints(18)?;
|
||||
Ok(())
|
||||
}
|
||||
fn combat(&mut self) -> Result<()> {
|
||||
self.reader.read_boolean()?;
|
||||
self.reader.read_boolean()?;
|
||||
|
|
@ -201,16 +211,9 @@ impl<'a, 'b> Verifier<'a, 'b> {
|
|||
for _ in 0..count {
|
||||
self.global_id()?;
|
||||
}
|
||||
let summoner_bodies = !matches!(
|
||||
std::env::var("SCROLL_BATTLE_SUMMONER")
|
||||
.unwrap_or_default()
|
||||
.as_str(),
|
||||
"0" | "false" | "off"
|
||||
);
|
||||
let summoner = LogicDataTables::instance()
|
||||
.data_by_name(table::CHARACTERS_COMBINED, "KingTower")
|
||||
.map(|row| row.global_id())
|
||||
.filter(|_| summoner_bodies);
|
||||
.map(|row| row.global_id());
|
||||
self.mark("object_bodies");
|
||||
for entry in &data {
|
||||
let is_summoner = entry.global_id().is_some() && entry.global_id() == summoner;
|
||||
|
|
@ -227,9 +230,14 @@ impl<'a, 'b> Verifier<'a, 'b> {
|
|||
.unwrap_or(0);
|
||||
self.mark("components");
|
||||
for pass in 0..4 {
|
||||
for _ in &data {
|
||||
for entry in &data {
|
||||
let moves = entry
|
||||
.data()
|
||||
.map(|row| row.int(MOVEMENT_SPEED_COLUMN) > 0)
|
||||
.unwrap_or(false);
|
||||
match pass {
|
||||
0 => self.combat()?,
|
||||
1 if moves => self.movement()?,
|
||||
2 => self.hitpoint()?,
|
||||
3 => self.buff(buff_rows)?,
|
||||
_ => {}
|
||||
|
|
|
|||
Loading…
Reference in a new issue