build and send the battle sector state
npc missions used to get a ServerErrorMessage back. now StartMission builds a LogicGameMode snapshot off the arena tilemap and answers 21903. towers come from assets/locations/*.csv the way initDefaultSector does it: tile coordinates times 500, leader index decided by which half of the map the tower sits in. the two king towers must be there, the client dereferences them without a null check. they live in buildings.csv, not characters.csv. LogicCharacter puts the base object fields fourth, not first. the buff component writes a fixed array sized by the character_buffs row count even with no buffs. training_arena parses to 2 kings, 4 princess towers, 36x64 subtiles. snapshot is 602 bytes over 6 objects. the real client has not seen it yet.
This commit is contained in:
parent
d439027735
commit
774e7c60cf
20 changed files with 874 additions and 62 deletions
214
crates/game-service/src/battle.rs
Normal file
214
crates/game-service/src/battle.rs
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use logic::battle::{
|
||||||
|
LogicBattle, LogicCharacter, LogicCharacterBuffComponent, LogicCombatComponent, LogicComponent,
|
||||||
|
LogicGameMode, LogicGameObject, LogicGameObjectEntry, LogicGameObjectManager,
|
||||||
|
LogicGameObjectRef, LogicHitpointComponent, LogicObjectBody, LogicSummoner, LogicTilemap,
|
||||||
|
LogicTime, LogicVector2, BATTLE_TYPE_NPC, CHARACTER_OBJECT_TYPE, DIRECTION_BOTTOM,
|
||||||
|
DIRECTION_TOP, SUBTILE_UNITS,
|
||||||
|
};
|
||||||
|
use logic::model::LogicClientAvatar;
|
||||||
|
use logic::{table, LogicDataRef, LogicDataTables, LogicRandom};
|
||||||
|
pub const LOCATION_FILE_COLUMN: &str = "FileName";
|
||||||
|
pub const CHARACTER_KING_TOWER: &str = "KingTower";
|
||||||
|
pub const CHARACTER_PRINCESS_TOWER: &str = "PrincessTower";
|
||||||
|
pub const BUFF_TABLE: i32 = 9;
|
||||||
|
pub struct TowerSpec {
|
||||||
|
pub data: LogicDataRef,
|
||||||
|
pub instance: i32,
|
||||||
|
pub tile: (i32, i32),
|
||||||
|
pub owner: i32,
|
||||||
|
pub level_index: i32,
|
||||||
|
pub is_leader: bool,
|
||||||
|
pub summoner: bool,
|
||||||
|
}
|
||||||
|
pub struct BattleBuilder {
|
||||||
|
root: PathBuf,
|
||||||
|
buff_type_count: usize,
|
||||||
|
}
|
||||||
|
fn tile_to_units(tile: i32) -> i32 {
|
||||||
|
tile.saturating_mul(SUBTILE_UNITS)
|
||||||
|
}
|
||||||
|
fn character_data(name: &str) -> LogicDataRef {
|
||||||
|
let combined = LogicDataRef::by_name(table::CHARACTERS_COMBINED, name);
|
||||||
|
if !combined.is_none() {
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
let building = LogicDataRef::by_name(table::BUILDINGS, name);
|
||||||
|
if !building.is_none() {
|
||||||
|
return building;
|
||||||
|
}
|
||||||
|
LogicDataRef::by_name(table::CHARACTERS, name)
|
||||||
|
}
|
||||||
|
impl BattleBuilder {
|
||||||
|
pub fn new(root: impl AsRef<Path>) -> Self {
|
||||||
|
let buff_type_count = LogicDataTables::instance()
|
||||||
|
.table(BUFF_TABLE)
|
||||||
|
.map(|rows| rows.count())
|
||||||
|
.unwrap_or(0);
|
||||||
|
Self {
|
||||||
|
root: root.as_ref().to_path_buf(),
|
||||||
|
buff_type_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn buff_type_count(&self) -> usize {
|
||||||
|
self.buff_type_count
|
||||||
|
}
|
||||||
|
pub fn tilemap_for(&self, location: &LogicDataRef) -> std::io::Result<LogicTilemap> {
|
||||||
|
let file_name = location
|
||||||
|
.data()
|
||||||
|
.map(|data| data.string(LOCATION_FILE_COLUMN).to_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if file_name.is_empty() {
|
||||||
|
return Err(std::io::Error::other("the location names no tilemap"));
|
||||||
|
}
|
||||||
|
LogicTilemap::load(&self.root, &file_name)
|
||||||
|
}
|
||||||
|
fn tower(&self, spec: TowerSpec) -> LogicGameObjectEntry {
|
||||||
|
let TowerSpec {
|
||||||
|
data,
|
||||||
|
instance,
|
||||||
|
tile,
|
||||||
|
owner,
|
||||||
|
level_index,
|
||||||
|
is_leader,
|
||||||
|
summoner,
|
||||||
|
} = spec;
|
||||||
|
let hitpoints = data
|
||||||
|
.data()
|
||||||
|
.map(|row| row.int_at("Hitpoints", level_index.max(0) as usize))
|
||||||
|
.filter(|value| *value > 0)
|
||||||
|
.unwrap_or(1);
|
||||||
|
let character = LogicCharacter {
|
||||||
|
level_index,
|
||||||
|
is_leader,
|
||||||
|
base: LogicGameObject {
|
||||||
|
position: LogicVector2::new(tile_to_units(tile.0), tile_to_units(tile.1)),
|
||||||
|
owner_index: owner,
|
||||||
|
..LogicGameObject::default()
|
||||||
|
},
|
||||||
|
direction: LogicVector2::new(
|
||||||
|
0,
|
||||||
|
if owner == 0 {
|
||||||
|
DIRECTION_TOP
|
||||||
|
} else {
|
||||||
|
DIRECTION_BOTTOM
|
||||||
|
},
|
||||||
|
),
|
||||||
|
..LogicCharacter::default()
|
||||||
|
};
|
||||||
|
let body = if summoner {
|
||||||
|
LogicObjectBody::Summoner(Box::new(LogicSummoner {
|
||||||
|
character,
|
||||||
|
..LogicSummoner::default()
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
LogicObjectBody::Character(Box::new(character))
|
||||||
|
};
|
||||||
|
LogicGameObjectEntry {
|
||||||
|
data,
|
||||||
|
global_id: LogicGameObjectRef::of(CHARACTER_OBJECT_TYPE + 1, instance),
|
||||||
|
body,
|
||||||
|
components: [
|
||||||
|
Some(LogicComponent::Combat(LogicCombatComponent::default())),
|
||||||
|
None,
|
||||||
|
Some(LogicComponent::Hitpoint(LogicHitpointComponent::healthy(
|
||||||
|
hitpoints,
|
||||||
|
))),
|
||||||
|
Some(LogicComponent::Buff(LogicCharacterBuffComponent::empty(
|
||||||
|
self.buff_type_count,
|
||||||
|
))),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn build(
|
||||||
|
&self,
|
||||||
|
location: LogicDataRef,
|
||||||
|
npc: LogicDataRef,
|
||||||
|
arena: LogicDataRef,
|
||||||
|
avatars: Vec<LogicClientAvatar>,
|
||||||
|
random_seed: i32,
|
||||||
|
) -> std::io::Result<LogicGameMode> {
|
||||||
|
let tilemap = self.tilemap_for(&location)?;
|
||||||
|
let height = tilemap.height();
|
||||||
|
let middle = tile_to_units(height) / 2;
|
||||||
|
let king = character_data(CHARACTER_KING_TOWER);
|
||||||
|
let princess = character_data(CHARACTER_PRINCESS_TOWER);
|
||||||
|
if king.is_none() {
|
||||||
|
return Err(std::io::Error::other(
|
||||||
|
"no KingTower row in the character tables",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut objects = LogicGameObjectManager::default();
|
||||||
|
let mut leaders = [LogicGameObjectRef::NONE; 2];
|
||||||
|
let mut towers: [Vec<LogicGameObjectRef>; 2] = [Vec::new(), Vec::new()];
|
||||||
|
let mut instance = 0;
|
||||||
|
for tile in tilemap.king_towers() {
|
||||||
|
let owner = usize::from(tile_to_units(tile.1) >= middle);
|
||||||
|
let entry = self.tower(TowerSpec {
|
||||||
|
data: king.clone(),
|
||||||
|
instance,
|
||||||
|
tile: *tile,
|
||||||
|
owner: owner as i32,
|
||||||
|
level_index: 0,
|
||||||
|
is_leader: true,
|
||||||
|
summoner: true,
|
||||||
|
});
|
||||||
|
leaders[owner] = entry.global_id;
|
||||||
|
objects.push(entry);
|
||||||
|
instance += 1;
|
||||||
|
}
|
||||||
|
if !princess.is_none() {
|
||||||
|
for tile in tilemap.princess_towers() {
|
||||||
|
let owner = usize::from(tile_to_units(tile.1) >= middle);
|
||||||
|
let entry = self.tower(TowerSpec {
|
||||||
|
data: princess.clone(),
|
||||||
|
instance,
|
||||||
|
tile: *tile,
|
||||||
|
owner: owner as i32,
|
||||||
|
level_index: 0,
|
||||||
|
is_leader: false,
|
||||||
|
summoner: false,
|
||||||
|
});
|
||||||
|
towers[owner].push(entry.global_id);
|
||||||
|
objects.push(entry);
|
||||||
|
instance += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if leaders.iter().any(LogicGameObjectRef::is_none) {
|
||||||
|
return Err(std::io::Error::other(format!(
|
||||||
|
"{} has {} king towers, the client needs exactly two",
|
||||||
|
location,
|
||||||
|
tilemap.king_towers().len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let account_ids = [
|
||||||
|
avatars
|
||||||
|
.first()
|
||||||
|
.map(|avatar| avatar.account_id)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
avatars
|
||||||
|
.get(1)
|
||||||
|
.map(|avatar| avatar.account_id)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
];
|
||||||
|
let battle = LogicBattle {
|
||||||
|
location,
|
||||||
|
npc,
|
||||||
|
arena,
|
||||||
|
account_ids,
|
||||||
|
battle_type: BATTLE_TYPE_NPC,
|
||||||
|
objects,
|
||||||
|
leaders,
|
||||||
|
leader_towers: towers,
|
||||||
|
..LogicBattle::default()
|
||||||
|
};
|
||||||
|
Ok(LogicGameMode {
|
||||||
|
random: LogicRandom::new(random_seed),
|
||||||
|
random_seed,
|
||||||
|
time: LogicTime::default(),
|
||||||
|
battle,
|
||||||
|
avatars,
|
||||||
|
..LogicGameMode::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod battle;
|
||||||
pub mod catalog;
|
pub mod catalog;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod home;
|
pub mod home;
|
||||||
|
|
@ -7,6 +8,7 @@ pub mod service;
|
||||||
pub mod shop;
|
pub mod shop;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
pub mod time;
|
pub mod time;
|
||||||
|
pub use battle::BattleBuilder;
|
||||||
pub use catalog::{Catalog, ARENA_FALLBACK_INSTANCE, GOLD_RESOURCE_FALLBACK_INSTANCE};
|
pub use catalog::{Catalog, ARENA_FALLBACK_INSTANCE, GOLD_RESOURCE_FALLBACK_INSTANCE};
|
||||||
pub use config::{CardRef, DataSelector, GameConfig, StarterProfile};
|
pub use config::{CardRef, DataSelector, GameConfig, StarterProfile};
|
||||||
pub use home::{build_avatar, build_home};
|
pub use home::{build_avatar, build_home};
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use logic::table;
|
||||||
use logic::{
|
use logic::{
|
||||||
AvailableServerCommandMessage, EndClientTurnMessage, LogicCommandManager, LogicDataRef,
|
AvailableServerCommandMessage, EndClientTurnMessage, LogicCommandManager, LogicDataRef,
|
||||||
LogicShopSeedChangedCommand, OutOfSyncMessage, OwnHomeDataMessage,
|
LogicShopSeedChangedCommand, OutOfSyncMessage, OwnHomeDataMessage, SectorStateMessage,
|
||||||
|
StartMissionMessage,
|
||||||
};
|
};
|
||||||
use service_rpc::{
|
use service_rpc::{
|
||||||
AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult,
|
AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult,
|
||||||
RpcService, WireMessage,
|
RpcService, WireMessage,
|
||||||
};
|
};
|
||||||
use titan::{Message, MessageMeta, Payload};
|
use titan::{Message, MessageMeta, Payload};
|
||||||
|
use crate::battle::BattleBuilder;
|
||||||
use crate::catalog::Catalog;
|
use crate::catalog::Catalog;
|
||||||
use crate::config::GameConfig;
|
use crate::config::GameConfig;
|
||||||
|
use crate::home::build_avatar;
|
||||||
use crate::home_mode::{available_server_command, HomeModeRegistry};
|
use crate::home_mode::{available_server_command, HomeModeRegistry};
|
||||||
use crate::rewards::RewardRoller;
|
use crate::rewards::RewardRoller;
|
||||||
use crate::shop::{ShopCatalog, ShopCycle};
|
use crate::shop::{ShopCatalog, ShopCycle};
|
||||||
|
|
@ -20,6 +24,7 @@ pub struct GameService {
|
||||||
profiles: ProfileStore,
|
profiles: ProfileStore,
|
||||||
catalog: Arc<Catalog>,
|
catalog: Arc<Catalog>,
|
||||||
shop: Arc<ShopCatalog>,
|
shop: Arc<ShopCatalog>,
|
||||||
|
battles: Arc<BattleBuilder>,
|
||||||
sessions: HomeModeRegistry,
|
sessions: HomeModeRegistry,
|
||||||
}
|
}
|
||||||
impl GameService {
|
impl GameService {
|
||||||
|
|
@ -31,6 +36,9 @@ impl GameService {
|
||||||
.map_err(connect_failed)?;
|
.map_err(connect_failed)?;
|
||||||
let profile_count = profiles.count().await?;
|
let profile_count = profiles.count().await?;
|
||||||
let shop = Arc::new(ShopCatalog::load(config.shop_path.as_deref()));
|
let shop = Arc::new(ShopCatalog::load(config.shop_path.as_deref()));
|
||||||
|
let battles = Arc::new(BattleBuilder::new(
|
||||||
|
config.csv_root.clone().unwrap_or_else(|| "assets".into()),
|
||||||
|
));
|
||||||
for offer in shop.describe() {
|
for offer in shop.describe() {
|
||||||
tracing::debug!(offer, "shop offer");
|
tracing::debug!(offer, "shop offer");
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +54,7 @@ impl GameService {
|
||||||
profiles,
|
profiles,
|
||||||
catalog,
|
catalog,
|
||||||
shop,
|
shop,
|
||||||
|
battles,
|
||||||
sessions: HomeModeRegistry::default(),
|
sessions: HomeModeRegistry::default(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -162,6 +171,43 @@ impl GameApi for GameService {
|
||||||
tracing::debug!(%account, ping_ms, "client capabilities");
|
tracing::debug!(%account, ping_ms, "client capabilities");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
async fn start_mission(
|
||||||
|
&self,
|
||||||
|
account: AccountRef,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
) -> RpcResult<Vec<WireMessage>> {
|
||||||
|
let mission = StartMissionMessage::from_bytes(&payload)
|
||||||
|
.map_err(|error| RpcError::Rejected(error.to_string()))?;
|
||||||
|
let profile = self.profile(account).await?;
|
||||||
|
let avatar = build_avatar(&profile);
|
||||||
|
let location = mission
|
||||||
|
.npc
|
||||||
|
.data()
|
||||||
|
.map(|npc| LogicDataRef::by_name(table::LOCATIONS, npc.string("Location")))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let battle = self
|
||||||
|
.battles
|
||||||
|
.build(
|
||||||
|
location.clone(),
|
||||||
|
mission.npc.clone(),
|
||||||
|
profile.arena.clone(),
|
||||||
|
vec![avatar.clone(), avatar],
|
||||||
|
self.config.random_seed,
|
||||||
|
)
|
||||||
|
.map_err(|error| RpcError::Rejected(error.to_string()))?;
|
||||||
|
let snapshot = battle
|
||||||
|
.snapshot()
|
||||||
|
.map_err(|error| RpcError::Rejected(error.to_string()))?;
|
||||||
|
tracing::info!(
|
||||||
|
%account,
|
||||||
|
npc = %mission.npc,
|
||||||
|
location = %location,
|
||||||
|
objects = battle.battle.objects.objects.len(),
|
||||||
|
bytes = snapshot.len(),
|
||||||
|
"sending a battle sector state"
|
||||||
|
);
|
||||||
|
Ok(vec![encode(&SectorStateMessage::new(snapshot))?])
|
||||||
|
}
|
||||||
async fn end_client_turn(
|
async fn end_client_turn(
|
||||||
&self,
|
&self,
|
||||||
account: AccountRef,
|
account: AccountRef,
|
||||||
|
|
@ -255,6 +301,9 @@ impl RpcService for GameService {
|
||||||
self.client_capabilities(account, ping_ms).await?;
|
self.client_capabilities(account, ping_ms).await?;
|
||||||
Ok(GameResponse::Empty)
|
Ok(GameResponse::Empty)
|
||||||
}
|
}
|
||||||
|
GameRequest::StartMission { account, payload } => Ok(GameResponse::messages(
|
||||||
|
self.start_mission(account, payload).await?,
|
||||||
|
)),
|
||||||
GameRequest::EndClientTurn { account, payload } => Ok(GameResponse::messages(
|
GameRequest::EndClientTurn { account, payload } => Ok(GameResponse::messages(
|
||||||
self.end_client_turn(account, payload).await?,
|
self.end_client_turn(account, payload).await?,
|
||||||
)),
|
)),
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,17 @@ impl GameApi for RemoteGame {
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
async fn start_mission(
|
||||||
|
&self,
|
||||||
|
account: AccountRef,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
) -> RpcResult<Vec<WireMessage>> {
|
||||||
|
Ok(self
|
||||||
|
.client
|
||||||
|
.call(&GameRequest::StartMission { account, payload })
|
||||||
|
.await?
|
||||||
|
.into_messages())
|
||||||
|
}
|
||||||
async fn end_client_turn(
|
async fn end_client_turn(
|
||||||
&self,
|
&self,
|
||||||
account: AccountRef,
|
account: AccountRef,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let buy_chest = std::env::args()
|
let buy_chest = std::env::args()
|
||||||
.skip_while(|arg| arg != "--buy-chest")
|
.skip_while(|arg| arg != "--buy-chest")
|
||||||
.nth(1);
|
.nth(1);
|
||||||
|
let start_mission = std::env::args()
|
||||||
|
.skip_while(|arg| arg != "--start-mission")
|
||||||
|
.nth(1);
|
||||||
let csv_root = std::env::var("SCROLL_CSV_ROOT").unwrap_or_else(|_| "assets".to_owned());
|
let csv_root = std::env::var("SCROLL_CSV_ROOT").unwrap_or_else(|_| "assets".to_owned());
|
||||||
match LogicDataTables::load_from_dir(&csv_root) {
|
match LogicDataTables::load_from_dir(&csv_root) {
|
||||||
Ok(tables) => {
|
Ok(tables) => {
|
||||||
|
|
@ -181,7 +184,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let checksum = home.home.chest_id_counter
|
let checksum = home.home.chest_id_counter
|
||||||
+ ((home.home.spell_collection.spells.len() as i32) << 16)
|
+ ((home.home.spell_collection.spells.len() as i32) << 16)
|
||||||
+ i32::from(desync);
|
+ i32::from(desync);
|
||||||
if let Some(name) = buy_chest.as_deref() {
|
if let Some(name) = start_mission.as_deref() {
|
||||||
|
let npc = logic::LogicDataRef::by_name(logic::table::NPCS, name);
|
||||||
|
if npc.is_none() {
|
||||||
|
println!("!! no npc named {name}");
|
||||||
|
} else {
|
||||||
|
println!(" starting mission {npc}");
|
||||||
|
probe.send(&logic::StartMissionMessage { npc }).await?;
|
||||||
|
println!("-> 14104 StartMissionMessage");
|
||||||
|
}
|
||||||
|
} else if let Some(name) = buy_chest.as_deref() {
|
||||||
let chest = logic::LogicDataRef::by_name(logic::table::TREASURE_CHESTS, name);
|
let chest = logic::LogicDataRef::by_name(logic::table::TREASURE_CHESTS, name);
|
||||||
match chest.as_treasure_chest() {
|
match chest.as_treasure_chest() {
|
||||||
None => println!("!! no treasure chest named {name}"),
|
None => println!("!! no treasure chest named {name}"),
|
||||||
|
|
@ -247,6 +259,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
probe.send(&KeepAliveMessage::default()).await?;
|
probe.send(&KeepAliveMessage::default()).await?;
|
||||||
println!("-> 10108 KeepAliveMessage");
|
println!("-> 10108 KeepAliveMessage");
|
||||||
}
|
}
|
||||||
|
21903 => {
|
||||||
|
println!("<- 21903 SectorStateMessage ({} bytes)", payload.len());
|
||||||
|
println!("lobby reached, battle snapshot received");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
message_type::KEEP_ALIVE_SERVER => {
|
message_type::KEEP_ALIVE_SERVER => {
|
||||||
println!("<- 20108 KeepAliveServerMessage");
|
println!("<- 20108 KeepAliveServerMessage");
|
||||||
println!("lobby reached, probe done");
|
println!("lobby reached, probe done");
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use logic::{
|
use logic::{
|
||||||
message_type, ClientCapabilitiesMessage, GoHomeMessage, KeepAliveServerMessage,
|
message_type, ClientCapabilitiesMessage, GoHomeMessage, KeepAliveServerMessage,
|
||||||
LoginFailedMessage, LoginMessage, LoginOkMessage, ServerErrorMessage, StartMissionMessage,
|
LoginFailedMessage, LoginMessage, LoginOkMessage, ServerErrorMessage,
|
||||||
};
|
};
|
||||||
use service_rpc::{
|
use service_rpc::{
|
||||||
AccountRef, DeviceInfo, HomeRequestKind, LoginOutcome, Session as AuthSession, WireMessage,
|
AccountRef, DeviceInfo, HomeRequestKind, LoginOutcome, Session as AuthSession, WireMessage,
|
||||||
|
|
@ -89,20 +89,25 @@ impl MessageManager {
|
||||||
self.push_home(HomeRequestKind::GoHome).await
|
self.push_home(HomeRequestKind::GoHome).await
|
||||||
}
|
}
|
||||||
message_type::START_MISSION => {
|
message_type::START_MISSION => {
|
||||||
let mission = incoming
|
let Some(account) = self.account else {
|
||||||
.downcast::<StartMissionMessage>()
|
return Ok(());
|
||||||
.expect("start mission message");
|
};
|
||||||
tracing::warn!(
|
match self
|
||||||
peer = %self.peer,
|
.backends
|
||||||
npc = %mission.npc,
|
.game
|
||||||
"client asked for an npc mission, which this server does not implement; \
|
.start_mission(account, incoming.payload)
|
||||||
give the avatar a non zero npc_win_count so it goes to the home screen instead"
|
.await
|
||||||
);
|
{
|
||||||
|
Ok(replies) => self.push_wire_messages(replies).await?,
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!(peer = %self.peer, %error, "could not start the mission");
|
||||||
self.sender
|
self.sender
|
||||||
.send_message(&ServerErrorMessage::new(
|
.send_message(&ServerErrorMessage::new(
|
||||||
"npc missions are not implemented by this server",
|
"this server could not build the battle",
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
message_type::END_CLIENT_TURN => {
|
message_type::END_CLIENT_TURN => {
|
||||||
|
|
|
||||||
145
crates/logic/src/battle/logic_character.rs
Normal file
145
crates/logic/src/battle/logic_character.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
use titan::{ByteStreamWriter, Result};
|
||||||
|
use crate::battle::logic_game_object::{LogicGameObject, LogicVector2};
|
||||||
|
pub const DIRECTION_TOP: i32 = 256;
|
||||||
|
pub const DIRECTION_BOTTOM: i32 = -256;
|
||||||
|
pub const DEFAULT_SIZE: i32 = 100;
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LogicCharacter {
|
||||||
|
pub level_index: i32,
|
||||||
|
pub flag_160: bool,
|
||||||
|
pub is_leader: bool,
|
||||||
|
pub base: LogicGameObject,
|
||||||
|
pub direction: LogicVector2,
|
||||||
|
pub state: i32,
|
||||||
|
pub size: i32,
|
||||||
|
pub growth_timer: i32,
|
||||||
|
pub spawn_timer: i32,
|
||||||
|
pub field_120: i32,
|
||||||
|
pub remaining_spawn_count: i32,
|
||||||
|
pub destruction_wave_timer: i32,
|
||||||
|
pub lane_id: i32,
|
||||||
|
pub deploy_timer: i32,
|
||||||
|
pub hide_timer: i32,
|
||||||
|
pub field_100: i32,
|
||||||
|
pub pending_physical_damage: i32,
|
||||||
|
pub mana_generate_limit: Option<i32>,
|
||||||
|
pub reload: Option<(i32, i32)>,
|
||||||
|
}
|
||||||
|
impl Default for LogicCharacter {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
level_index: 0,
|
||||||
|
flag_160: false,
|
||||||
|
is_leader: false,
|
||||||
|
base: LogicGameObject::default(),
|
||||||
|
direction: LogicVector2::new(0, DIRECTION_BOTTOM),
|
||||||
|
state: 0,
|
||||||
|
size: DEFAULT_SIZE,
|
||||||
|
growth_timer: 0,
|
||||||
|
spawn_timer: 0,
|
||||||
|
field_120: 0,
|
||||||
|
remaining_spawn_count: 0,
|
||||||
|
destruction_wave_timer: 0,
|
||||||
|
lane_id: 0,
|
||||||
|
deploy_timer: 0,
|
||||||
|
hide_timer: 0,
|
||||||
|
field_100: 0,
|
||||||
|
pending_physical_damage: 0,
|
||||||
|
mana_generate_limit: None,
|
||||||
|
reload: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl LogicCharacter {
|
||||||
|
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||||
|
writer.write_vint(self.level_index);
|
||||||
|
writer.write_boolean(self.flag_160);
|
||||||
|
writer.write_boolean(self.is_leader);
|
||||||
|
self.base.encode_base(writer)?;
|
||||||
|
writer.write_vint(self.direction.x);
|
||||||
|
writer.write_vint(self.direction.y);
|
||||||
|
writer.write_vint(self.state);
|
||||||
|
writer.write_vint(self.size);
|
||||||
|
writer.write_vint(self.growth_timer);
|
||||||
|
writer.write_vint(self.spawn_timer);
|
||||||
|
writer.write_vint(self.field_120);
|
||||||
|
writer.write_vint(self.remaining_spawn_count);
|
||||||
|
writer.write_vint(self.destruction_wave_timer);
|
||||||
|
writer.write_vint(self.lane_id);
|
||||||
|
writer.write_vint(self.deploy_timer);
|
||||||
|
writer.write_vint(self.hide_timer);
|
||||||
|
writer.write_vint(self.field_100);
|
||||||
|
writer.write_vint(self.pending_physical_damage);
|
||||||
|
if let Some(limit) = self.mana_generate_limit {
|
||||||
|
writer.write_vint(limit);
|
||||||
|
}
|
||||||
|
if let Some((timer, hits)) = self.reload {
|
||||||
|
writer.write_vint(timer);
|
||||||
|
writer.write_vint(hits);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LogicSummoner {
|
||||||
|
pub character: LogicCharacter,
|
||||||
|
pub deck: Option<LogicSummonerDeck>,
|
||||||
|
pub mana_regen_timer: i32,
|
||||||
|
pub mana: i32,
|
||||||
|
pub field_244: i32,
|
||||||
|
pub field_256: i32,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LogicSummonerDeck {
|
||||||
|
pub field_220: i32,
|
||||||
|
pub hand: [i32; 4],
|
||||||
|
pub draw_pile: Vec<i32>,
|
||||||
|
pub used_pile: Vec<i32>,
|
||||||
|
pub last_used_index: i32,
|
||||||
|
pub field_248: i32,
|
||||||
|
pub field_240: i32,
|
||||||
|
}
|
||||||
|
impl Default for LogicSummonerDeck {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
field_220: 0,
|
||||||
|
hand: [-1; 4],
|
||||||
|
draw_pile: Vec::new(),
|
||||||
|
used_pile: Vec::new(),
|
||||||
|
last_used_index: -1,
|
||||||
|
field_248: 0,
|
||||||
|
field_240: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl LogicSummoner {
|
||||||
|
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||||
|
self.character.encode(writer)?;
|
||||||
|
match &self.deck {
|
||||||
|
None => writer.write_boolean(false),
|
||||||
|
Some(deck) => {
|
||||||
|
writer.write_boolean(true);
|
||||||
|
writer.write_vint(deck.field_220);
|
||||||
|
for slot in deck.hand {
|
||||||
|
writer.write_vint(slot);
|
||||||
|
}
|
||||||
|
writer.write_vint(deck.draw_pile.len() as i32);
|
||||||
|
for value in &deck.draw_pile {
|
||||||
|
writer.write_vint(*value);
|
||||||
|
}
|
||||||
|
writer.write_vint(deck.used_pile.len() as i32);
|
||||||
|
for value in &deck.used_pile {
|
||||||
|
writer.write_vint(*value);
|
||||||
|
}
|
||||||
|
writer.write_vint(deck.last_used_index);
|
||||||
|
writer.write_vint(deck.field_248);
|
||||||
|
writer.write_vint(deck.field_240);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.write_vint(self.mana_regen_timer);
|
||||||
|
writer.write_vint(self.mana);
|
||||||
|
writer.write_vint(self.field_244);
|
||||||
|
writer.write_vint(self.field_256);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
126
crates/logic/src/battle/logic_component.rs
Normal file
126
crates/logic/src/battle/logic_component.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
use titan::{ByteStreamWriter, Payload, Result};
|
||||||
|
use crate::battle::logic_game_object_ref::LogicGameObjectRef;
|
||||||
|
pub const COMPONENT_COMBAT: usize = 0;
|
||||||
|
pub const COMPONENT_MOVEMENT: usize = 1;
|
||||||
|
pub const COMPONENT_HITPOINT: usize = 2;
|
||||||
|
pub const COMPONENT_BUFF: usize = 3;
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LogicCombatComponent {
|
||||||
|
pub flag_72: bool,
|
||||||
|
pub flag_73: bool,
|
||||||
|
pub field_48: i32,
|
||||||
|
pub field_52: i32,
|
||||||
|
pub field_60: i32,
|
||||||
|
pub field_64: i32,
|
||||||
|
pub field_68: i32,
|
||||||
|
pub target: LogicGameObjectRef,
|
||||||
|
pub attackers: Vec<(LogicGameObjectRef, i32)>,
|
||||||
|
pub observers: Vec<LogicGameObjectRef>,
|
||||||
|
}
|
||||||
|
impl LogicCombatComponent {
|
||||||
|
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||||
|
writer.write_boolean(self.flag_72);
|
||||||
|
writer.write_boolean(self.flag_73);
|
||||||
|
writer.write_vint(self.field_48);
|
||||||
|
writer.write_vint(self.field_52);
|
||||||
|
writer.write_vint(self.field_60);
|
||||||
|
writer.write_vint(self.field_64);
|
||||||
|
writer.write_vint(self.field_68);
|
||||||
|
writer.write_vint(self.attackers.len() as i32);
|
||||||
|
writer.write_vint(self.observers.len() as i32);
|
||||||
|
self.target.encode(writer)?;
|
||||||
|
for (reference, value) in &self.attackers {
|
||||||
|
reference.encode(writer)?;
|
||||||
|
writer.write_vint(*value);
|
||||||
|
}
|
||||||
|
for reference in &self.observers {
|
||||||
|
reference.encode(writer)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LogicHitpointComponent {
|
||||||
|
pub hitpoints: i32,
|
||||||
|
pub base_hitpoints: i32,
|
||||||
|
pub lifetime_damage: i32,
|
||||||
|
pub lifetime_accumulator: i32,
|
||||||
|
pub death_hit_angle: i32,
|
||||||
|
}
|
||||||
|
impl LogicHitpointComponent {
|
||||||
|
pub fn healthy(hitpoints: i32) -> Self {
|
||||||
|
Self {
|
||||||
|
hitpoints,
|
||||||
|
base_hitpoints: hitpoints,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||||
|
writer.write_vint(self.hitpoints);
|
||||||
|
writer.write_vint(self.base_hitpoints);
|
||||||
|
writer.write_vint(self.lifetime_damage);
|
||||||
|
if self.lifetime_damage != 0 {
|
||||||
|
writer.write_vint(self.lifetime_accumulator);
|
||||||
|
}
|
||||||
|
if self.hitpoints <= 0 {
|
||||||
|
writer.write_vint(self.death_hit_angle);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LogicCharacterBuffComponent {
|
||||||
|
pub field_32: i32,
|
||||||
|
pub field_36: i32,
|
||||||
|
pub field_40: i32,
|
||||||
|
pub buff_type_count: usize,
|
||||||
|
pub field_52: i32,
|
||||||
|
pub field_56: i32,
|
||||||
|
pub field_60: i32,
|
||||||
|
pub field_64: i32,
|
||||||
|
pub field_68: i32,
|
||||||
|
pub field_72: i32,
|
||||||
|
}
|
||||||
|
impl LogicCharacterBuffComponent {
|
||||||
|
pub fn empty(buff_type_count: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
buff_type_count,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||||
|
writer.write_vint(0);
|
||||||
|
writer.write_vint(0);
|
||||||
|
writer.write_vint(self.field_32);
|
||||||
|
writer.write_vint(self.field_36);
|
||||||
|
writer.write_vint(self.field_40);
|
||||||
|
for _ in 0..self.buff_type_count {
|
||||||
|
writer.write_vint(0);
|
||||||
|
}
|
||||||
|
for _ in 0..self.buff_type_count {
|
||||||
|
writer.write_boolean(false);
|
||||||
|
}
|
||||||
|
writer.write_vint(self.field_52);
|
||||||
|
writer.write_vint(self.field_56);
|
||||||
|
writer.write_vint(self.field_60);
|
||||||
|
writer.write_vint(self.field_64);
|
||||||
|
writer.write_vint(self.field_68);
|
||||||
|
writer.write_vint(self.field_72);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum LogicComponent {
|
||||||
|
Combat(LogicCombatComponent),
|
||||||
|
Hitpoint(LogicHitpointComponent),
|
||||||
|
Buff(LogicCharacterBuffComponent),
|
||||||
|
}
|
||||||
|
impl LogicComponent {
|
||||||
|
pub fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
LogicComponent::Combat(component) => component.encode(writer),
|
||||||
|
LogicComponent::Hitpoint(component) => component.encode(writer),
|
||||||
|
LogicComponent::Buff(component) => component.encode(writer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
crates/logic/src/battle/logic_game_mode.rs
Normal file
47
crates/logic/src/battle/logic_game_mode.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
use titan::{ByteStreamWriter, Payload, Result};
|
||||||
|
use crate::battle::logic_battle::LogicBattle;
|
||||||
|
use crate::battle::logic_time::LogicTime;
|
||||||
|
use crate::battle::logic_tutorial_manager::LogicTutorialManager;
|
||||||
|
use crate::logic_random::LogicRandom;
|
||||||
|
use crate::model::LogicClientAvatar;
|
||||||
|
pub const SECTION_BATTLE: i32 = 11;
|
||||||
|
pub const SECTION_TUTORIAL: i32 = 12;
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LogicGameMode {
|
||||||
|
pub server_tick: i32,
|
||||||
|
pub time: LogicTime,
|
||||||
|
pub random: LogicRandom,
|
||||||
|
pub random_seed: i32,
|
||||||
|
pub battle: LogicBattle,
|
||||||
|
pub avatars: Vec<LogicClientAvatar>,
|
||||||
|
pub tutorial_manager: LogicTutorialManager,
|
||||||
|
}
|
||||||
|
impl LogicGameMode {
|
||||||
|
pub fn snapshot(&self) -> Result<Vec<u8>> {
|
||||||
|
let mut writer = ByteStreamWriter::new();
|
||||||
|
self.encode(&mut writer)?;
|
||||||
|
Ok(writer.into_inner())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Payload for LogicGameMode {
|
||||||
|
fn encode(&self, writer: &mut ByteStreamWriter) -> Result<()> {
|
||||||
|
writer.write_vint(self.server_tick);
|
||||||
|
writer.write_vint(0);
|
||||||
|
writer.write_vint(SECTION_BATTLE);
|
||||||
|
self.time.encode(writer)?;
|
||||||
|
self.random.encode(writer)?;
|
||||||
|
writer.write_vint(self.random_seed);
|
||||||
|
self.battle.encode(writer)?;
|
||||||
|
for avatar in &self.avatars {
|
||||||
|
avatar.encode(writer)?;
|
||||||
|
}
|
||||||
|
writer.write_vint(SECTION_TUTORIAL);
|
||||||
|
self.tutorial_manager.encode(writer)?;
|
||||||
|
writer.write_vint(0);
|
||||||
|
writer.write_vint(0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn decode(_reader: &mut titan::ByteStreamReader<'_>) -> Result<Self> {
|
||||||
|
Err(titan::Error::Unsupported("LogicGameMode is encode only"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,18 +19,37 @@ impl LogicVector2 {
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Payload)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Payload)]
|
||||||
pub struct LogicGameObject {
|
pub struct LogicGameObject {
|
||||||
#[codec(vint)]
|
#[codec(vint)]
|
||||||
pub hitpoints: i32,
|
pub field_28: i32,
|
||||||
#[codec(vint)]
|
#[codec(vint)]
|
||||||
pub owner_index: i32,
|
pub field_56: i32,
|
||||||
pub position: LogicVector2,
|
pub position: LogicVector2,
|
||||||
#[codec(vint)]
|
#[codec(vint)]
|
||||||
pub state: i32,
|
pub owner_index: i32,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
impl LogicGameObject {
|
||||||
|
pub fn encode_base(&self, writer: &mut titan::ByteStreamWriter) -> titan::Result<()> {
|
||||||
|
<Self as Payload>::encode(self, writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum LogicObjectBody {
|
||||||
|
Character(Box<crate::battle::logic_character::LogicCharacter>),
|
||||||
|
Summoner(Box<crate::battle::logic_character::LogicSummoner>),
|
||||||
|
}
|
||||||
|
impl LogicObjectBody {
|
||||||
|
pub fn encode(&self, writer: &mut titan::ByteStreamWriter) -> titan::Result<()> {
|
||||||
|
match self {
|
||||||
|
LogicObjectBody::Character(character) => character.encode(writer),
|
||||||
|
LogicObjectBody::Summoner(summoner) => summoner.encode(writer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct LogicGameObjectEntry {
|
pub struct LogicGameObjectEntry {
|
||||||
pub data: LogicDataRef,
|
pub data: LogicDataRef,
|
||||||
pub global_id: LogicGameObjectRef,
|
pub global_id: LogicGameObjectRef,
|
||||||
pub object: LogicGameObject,
|
pub body: LogicObjectBody,
|
||||||
|
pub components: [Option<crate::battle::logic_component::LogicComponent>; COMPONENT_PASSES],
|
||||||
}
|
}
|
||||||
impl LogicGameObjectEntry {
|
impl LogicGameObjectEntry {
|
||||||
pub fn object_type(&self) -> i32 {
|
pub fn object_type(&self) -> i32 {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
use titan::{ByteStreamReader, ByteStreamWriter, Payload, Result};
|
use titan::{ByteStreamReader, ByteStreamWriter, Payload, Result};
|
||||||
use crate::battle::logic_game_object::{
|
use crate::battle::logic_game_object::{LogicGameObjectEntry, COMPONENT_PASSES, OBJECT_TYPE_COUNT};
|
||||||
LogicGameObject, LogicGameObjectEntry, COMPONENT_PASSES, OBJECT_TYPE_COUNT,
|
|
||||||
};
|
|
||||||
use crate::battle::logic_game_object_ref::LogicGameObjectRef;
|
|
||||||
use crate::data::LogicDataRef;
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct LogicGameObjectManager {
|
pub struct LogicGameObjectManager {
|
||||||
pub instance_counters: [i32; OBJECT_TYPE_COUNT],
|
pub instance_counters: [i32; OBJECT_TYPE_COUNT],
|
||||||
|
|
@ -44,38 +40,20 @@ impl Payload for LogicGameObjectManager {
|
||||||
entry.global_id.encode(writer)?;
|
entry.global_id.encode(writer)?;
|
||||||
}
|
}
|
||||||
for entry in &self.objects {
|
for entry in &self.objects {
|
||||||
entry.object.encode(writer)?;
|
entry.body.encode(writer)?;
|
||||||
|
}
|
||||||
|
for pass in 0..COMPONENT_PASSES {
|
||||||
|
for entry in &self.objects {
|
||||||
|
if let Some(component) = &entry.components[pass] {
|
||||||
|
component.encode(writer)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _ in 0..COMPONENT_PASSES {
|
|
||||||
for _ in &self.objects {}
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn decode(reader: &mut ByteStreamReader<'_>) -> Result<Self> {
|
fn decode(_reader: &mut ByteStreamReader<'_>) -> Result<Self> {
|
||||||
let mut instance_counters = [0; OBJECT_TYPE_COUNT];
|
Err(titan::Error::Unsupported(
|
||||||
for counter in instance_counters.iter_mut() {
|
"LogicGameObjectManager is encode only",
|
||||||
*counter = reader.read_vint()?;
|
))
|
||||||
}
|
|
||||||
let count = reader.read_vint()?.max(0) as usize;
|
|
||||||
let mut data = Vec::with_capacity(count);
|
|
||||||
for _ in 0..count {
|
|
||||||
data.push(LogicDataRef::decode(reader)?);
|
|
||||||
}
|
|
||||||
let mut ids = Vec::with_capacity(count);
|
|
||||||
for _ in 0..count {
|
|
||||||
ids.push(LogicGameObjectRef::decode(reader)?);
|
|
||||||
}
|
|
||||||
let mut objects = Vec::with_capacity(count);
|
|
||||||
for index in 0..count {
|
|
||||||
objects.push(LogicGameObjectEntry {
|
|
||||||
data: data[index].clone(),
|
|
||||||
global_id: ids[index],
|
|
||||||
object: LogicGameObject::decode(reader)?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
instance_counters,
|
|
||||||
objects,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
110
crates/logic/src/battle/logic_tilemap.rs
Normal file
110
crates/logic/src/battle/logic_tilemap.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
pub const SUBTILE_UNITS: i32 = 500;
|
||||||
|
pub const SECTION_OBJECTS: &str = "Objects";
|
||||||
|
pub const SECTION_MAP: &str = "Map";
|
||||||
|
pub const OBJECT_KING_TOWER: &str = "KingTower";
|
||||||
|
pub const OBJECT_PRINCESS_TOWER: &str = "PrincessTower";
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub struct LogicTilemap {
|
||||||
|
pub objects: Vec<(String, Vec<(i32, i32)>)>,
|
||||||
|
pub tiles: Vec<Vec<i32>>,
|
||||||
|
}
|
||||||
|
fn split_row(line: &str) -> Vec<String> {
|
||||||
|
let mut cells = Vec::new();
|
||||||
|
let mut current = String::new();
|
||||||
|
let mut quoted = false;
|
||||||
|
for character in line.chars() {
|
||||||
|
match character {
|
||||||
|
'"' => quoted = !quoted,
|
||||||
|
',' if !quoted => cells.push(std::mem::take(&mut current)),
|
||||||
|
'\r' => {}
|
||||||
|
_ => current.push(character),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cells.push(current);
|
||||||
|
cells
|
||||||
|
}
|
||||||
|
fn cell(row: &[String], index: usize) -> &str {
|
||||||
|
row.get(index).map(String::as_str).unwrap_or("").trim()
|
||||||
|
}
|
||||||
|
fn number(value: &str) -> Option<i32> {
|
||||||
|
value.parse::<i32>().ok()
|
||||||
|
}
|
||||||
|
impl LogicTilemap {
|
||||||
|
pub fn parse(source: &str) -> Self {
|
||||||
|
let mut tilemap = Self::default();
|
||||||
|
let mut section = String::new();
|
||||||
|
let mut object: Option<String> = None;
|
||||||
|
for line in source.lines() {
|
||||||
|
let row = split_row(line);
|
||||||
|
let head = cell(&row, 0);
|
||||||
|
if !head.is_empty() {
|
||||||
|
section = head.to_owned();
|
||||||
|
object = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match section.as_str() {
|
||||||
|
SECTION_OBJECTS => {
|
||||||
|
let name = cell(&row, 1);
|
||||||
|
if !name.is_empty() && name != "string" && name != "int" {
|
||||||
|
object = Some(name.to_owned());
|
||||||
|
tilemap.objects.push((name.to_owned(), Vec::new()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (Some(x), Some(y)) = (number(cell(&row, 2)), number(cell(&row, 3))) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if object.is_some() {
|
||||||
|
if let Some(entry) = tilemap.objects.last_mut() {
|
||||||
|
entry.1.push((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SECTION_MAP => {
|
||||||
|
let values: Vec<i32> = row[1..]
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.trim())
|
||||||
|
.take_while(|value| !value.is_empty())
|
||||||
|
.filter_map(number)
|
||||||
|
.collect();
|
||||||
|
if !values.is_empty() {
|
||||||
|
tilemap.tiles.push(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tilemap
|
||||||
|
}
|
||||||
|
pub fn load(root: &Path, file_name: &str) -> std::io::Result<Self> {
|
||||||
|
let path = root.join(file_name);
|
||||||
|
let source = std::fs::read_to_string(&path)?;
|
||||||
|
Ok(Self::parse(&source))
|
||||||
|
}
|
||||||
|
pub fn height(&self) -> i32 {
|
||||||
|
self.tiles.len() as i32
|
||||||
|
}
|
||||||
|
pub fn width(&self) -> i32 {
|
||||||
|
self.tiles.iter().map(Vec::len).max().unwrap_or(0) as i32
|
||||||
|
}
|
||||||
|
pub fn placements(&self, name: &str) -> &[(i32, i32)] {
|
||||||
|
self.objects
|
||||||
|
.iter()
|
||||||
|
.find(|(object, _)| object == name)
|
||||||
|
.map(|(_, positions)| positions.as_slice())
|
||||||
|
.unwrap_or(&[])
|
||||||
|
}
|
||||||
|
pub fn king_towers(&self) -> &[(i32, i32)] {
|
||||||
|
self.placements(OBJECT_KING_TOWER)
|
||||||
|
}
|
||||||
|
pub fn princess_towers(&self) -> &[(i32, i32)] {
|
||||||
|
self.placements(OBJECT_PRINCESS_TOWER)
|
||||||
|
}
|
||||||
|
pub fn counts(&self) -> HashMap<&str, usize> {
|
||||||
|
self.objects
|
||||||
|
.iter()
|
||||||
|
.map(|(name, positions)| (name.as_str(), positions.len()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,31 @@
|
||||||
mod logic_battle;
|
mod logic_battle;
|
||||||
|
mod logic_character;
|
||||||
|
mod logic_component;
|
||||||
|
mod logic_game_mode;
|
||||||
mod logic_game_object;
|
mod logic_game_object;
|
||||||
mod logic_game_object_manager;
|
mod logic_game_object_manager;
|
||||||
mod logic_game_object_ref;
|
mod logic_game_object_ref;
|
||||||
|
mod logic_tilemap;
|
||||||
mod logic_time;
|
mod logic_time;
|
||||||
mod logic_tutorial_manager;
|
mod logic_tutorial_manager;
|
||||||
pub use logic_battle::{
|
pub use logic_battle::{
|
||||||
LogicBattle, BATTLE_INT_ARRAY, BATTLE_TRAILING_INTS, BATTLE_TYPE_NPC, BATTLE_TYPE_PVP,
|
LogicBattle, BATTLE_INT_ARRAY, BATTLE_TRAILING_INTS, BATTLE_TYPE_NPC, BATTLE_TYPE_PVP,
|
||||||
BATTLE_TYPE_REPLAY,
|
BATTLE_TYPE_REPLAY,
|
||||||
};
|
};
|
||||||
|
pub use logic_character::{
|
||||||
|
LogicCharacter, LogicSummoner, LogicSummonerDeck, DEFAULT_SIZE, DIRECTION_BOTTOM, DIRECTION_TOP,
|
||||||
|
};
|
||||||
|
pub use logic_component::{
|
||||||
|
LogicCharacterBuffComponent, LogicCombatComponent, LogicComponent, LogicHitpointComponent,
|
||||||
|
COMPONENT_BUFF, COMPONENT_COMBAT, COMPONENT_HITPOINT, COMPONENT_MOVEMENT,
|
||||||
|
};
|
||||||
|
pub use logic_game_mode::{LogicGameMode, SECTION_BATTLE, SECTION_TUTORIAL};
|
||||||
pub use logic_game_object::{
|
pub use logic_game_object::{
|
||||||
LogicGameObject, LogicGameObjectEntry, LogicVector2, CHARACTER_OBJECT_TYPE, COMPONENT_PASSES,
|
LogicGameObject, LogicGameObjectEntry, LogicObjectBody, LogicVector2, CHARACTER_OBJECT_TYPE,
|
||||||
OBJECT_TYPE_COUNT,
|
COMPONENT_PASSES, OBJECT_TYPE_COUNT,
|
||||||
};
|
};
|
||||||
pub use logic_game_object_manager::LogicGameObjectManager;
|
pub use logic_game_object_manager::LogicGameObjectManager;
|
||||||
pub use logic_game_object_ref::LogicGameObjectRef;
|
pub use logic_game_object_ref::LogicGameObjectRef;
|
||||||
|
pub use logic_tilemap::{LogicTilemap, OBJECT_KING_TOWER, OBJECT_PRINCESS_TOWER, SUBTILE_UNITS};
|
||||||
pub use logic_time::LogicTime;
|
pub use logic_time::LogicTime;
|
||||||
pub use logic_tutorial_manager::LogicTutorialManager;
|
pub use logic_tutorial_manager::LogicTutorialManager;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ mod login_failed;
|
||||||
mod login_ok;
|
mod login_ok;
|
||||||
mod out_of_sync;
|
mod out_of_sync;
|
||||||
mod own_home_data;
|
mod own_home_data;
|
||||||
|
mod sector_state;
|
||||||
mod server_error;
|
mod server_error;
|
||||||
mod start_mission;
|
mod start_mission;
|
||||||
pub use available_server_command::AvailableServerCommandMessage;
|
pub use available_server_command::AvailableServerCommandMessage;
|
||||||
|
|
@ -29,6 +30,7 @@ pub use login_failed::{LoginFailedMessage, LoginFailureReason};
|
||||||
pub use login_ok::LoginOkMessage;
|
pub use login_ok::LoginOkMessage;
|
||||||
pub use out_of_sync::OutOfSyncMessage;
|
pub use out_of_sync::OutOfSyncMessage;
|
||||||
pub use own_home_data::OwnHomeDataMessage;
|
pub use own_home_data::OwnHomeDataMessage;
|
||||||
|
pub use sector_state::SectorStateMessage;
|
||||||
pub use server_error::ServerErrorMessage;
|
pub use server_error::ServerErrorMessage;
|
||||||
pub use start_mission::StartMissionMessage;
|
pub use start_mission::StartMissionMessage;
|
||||||
pub mod message_type {
|
pub mod message_type {
|
||||||
|
|
|
||||||
12
crates/logic/src/messages/sector_state.rs
Normal file
12
crates/logic/src/messages/sector_state.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
use titan::Message;
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Message)]
|
||||||
|
#[message(id = 21903, direction = "server", name = "SectorStateMessage")]
|
||||||
|
#[codec(raw)]
|
||||||
|
pub struct SectorStateMessage {
|
||||||
|
pub snapshot: Vec<u8>,
|
||||||
|
}
|
||||||
|
impl SectorStateMessage {
|
||||||
|
pub fn new(snapshot: Vec<u8>) -> Self {
|
||||||
|
Self { snapshot }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,6 +43,11 @@ pub enum GameRequest {
|
||||||
#[serde(with = "crate::base64::serde_bytes")]
|
#[serde(with = "crate::base64::serde_bytes")]
|
||||||
payload: Vec<u8>,
|
payload: Vec<u8>,
|
||||||
},
|
},
|
||||||
|
StartMission {
|
||||||
|
account: AccountRef,
|
||||||
|
#[serde(with = "crate::base64::serde_bytes")]
|
||||||
|
payload: Vec<u8>,
|
||||||
|
},
|
||||||
Disconnect {
|
Disconnect {
|
||||||
account: AccountRef,
|
account: AccountRef,
|
||||||
},
|
},
|
||||||
|
|
@ -72,6 +77,11 @@ pub trait GameApi: Send + Sync + 'static {
|
||||||
kind: HomeRequestKind,
|
kind: HomeRequestKind,
|
||||||
) -> RpcResult<Vec<WireMessage>>;
|
) -> RpcResult<Vec<WireMessage>>;
|
||||||
async fn client_capabilities(&self, account: AccountRef, ping_ms: i32) -> RpcResult<()>;
|
async fn client_capabilities(&self, account: AccountRef, ping_ms: i32) -> RpcResult<()>;
|
||||||
|
async fn start_mission(
|
||||||
|
&self,
|
||||||
|
account: AccountRef,
|
||||||
|
payload: Vec<u8>,
|
||||||
|
) -> RpcResult<Vec<WireMessage>>;
|
||||||
async fn end_client_turn(
|
async fn end_client_turn(
|
||||||
&self,
|
&self,
|
||||||
account: AccountRef,
|
account: AccountRef,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ impl Default for FieldSpec {
|
||||||
}
|
}
|
||||||
pub fn expand_payload(input: &DeriveInput) -> Result<TokenStream> {
|
pub fn expand_payload(input: &DeriveInput) -> Result<TokenStream> {
|
||||||
let ident = &input.ident;
|
let ident = &input.ident;
|
||||||
|
if has_raw(input)? {
|
||||||
|
return expand_raw(input);
|
||||||
|
}
|
||||||
let partial = has_partial(input)?;
|
let partial = has_partial(input)?;
|
||||||
let fields = match &input.data {
|
let fields = match &input.data {
|
||||||
Data::Struct(data) => match &data.fields {
|
Data::Struct(data) => match &data.fields {
|
||||||
|
|
@ -124,21 +127,61 @@ pub fn expand_payload(input: &DeriveInput) -> Result<TokenStream> {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
fn expand_raw(input: &DeriveInput) -> Result<TokenStream> {
|
||||||
|
let ident = &input.ident;
|
||||||
|
let Data::Struct(data) = &input.data else {
|
||||||
|
return Err(Error::new_spanned(ident, "#[codec(raw)] needs a struct"));
|
||||||
|
};
|
||||||
|
let Fields::Named(named) = &data.fields else {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
ident,
|
||||||
|
"#[codec(raw)] needs one named field",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let fields: Vec<_> = named.named.iter().collect();
|
||||||
|
if fields.len() != 1 {
|
||||||
|
return Err(Error::new_spanned(
|
||||||
|
ident,
|
||||||
|
"#[codec(raw)] needs exactly one field",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let name = fields[0].ident.as_ref().expect("named field");
|
||||||
|
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
Ok(quote! {
|
||||||
|
impl #impl_generics ::titan::message::Payload for #ident #ty_generics #where_clause {
|
||||||
|
fn encode(&self, writer: &mut ::titan::io::ByteStreamWriter) -> ::titan::error::Result<()> {
|
||||||
|
writer.write_raw(&self.#name);
|
||||||
|
::core::result::Result::Ok(())
|
||||||
|
}
|
||||||
|
fn decode(reader: &mut ::titan::io::ByteStreamReader<'_>) -> ::titan::error::Result<Self> {
|
||||||
|
::core::result::Result::Ok(Self { #name: reader.read_remaining() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
fn has_partial(input: &DeriveInput) -> Result<bool> {
|
fn has_partial(input: &DeriveInput) -> Result<bool> {
|
||||||
let mut partial = false;
|
container_flag(input, "partial")
|
||||||
|
}
|
||||||
|
fn has_raw(input: &DeriveInput) -> Result<bool> {
|
||||||
|
container_flag(input, "raw")
|
||||||
|
}
|
||||||
|
fn container_flag(input: &DeriveInput, wanted: &str) -> Result<bool> {
|
||||||
|
let mut found = false;
|
||||||
for attr in &input.attrs {
|
for attr in &input.attrs {
|
||||||
if !attr.path().is_ident("codec") {
|
if !attr.path().is_ident("codec") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
attr.parse_nested_meta(|meta| {
|
attr.parse_nested_meta(|meta| {
|
||||||
if meta.path.is_ident("partial") {
|
if meta.path.is_ident("partial") || meta.path.is_ident("raw") {
|
||||||
partial = true;
|
if meta.path.is_ident(wanted) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(meta.error("unsupported container level #[codec(...)] key"))
|
Err(meta.error("unsupported container level #[codec(...)] key"))
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Ok(partial)
|
Ok(found)
|
||||||
}
|
}
|
||||||
fn parse_field_spec(attrs: &[syn::Attribute]) -> Result<FieldSpec> {
|
fn parse_field_spec(attrs: &[syn::Attribute]) -> Result<FieldSpec> {
|
||||||
let mut spec = FieldSpec::default();
|
let mut spec = FieldSpec::default();
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ pub enum Error {
|
||||||
UnknownCommandType(i32),
|
UnknownCommandType(i32),
|
||||||
#[error("trailing {0} unread byte(s)")]
|
#[error("trailing {0} unread byte(s)")]
|
||||||
TrailingBytes(usize),
|
TrailingBytes(usize),
|
||||||
|
#[error("{0}")]
|
||||||
|
Unsupported(&'static str),
|
||||||
#[error("io: {0}")]
|
#[error("io: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,10 @@ impl<'a> ByteStreamReader<'a> {
|
||||||
}
|
}
|
||||||
Ok(Some(self.take(length as usize)?.to_vec()))
|
Ok(Some(self.take(length as usize)?.to_vec()))
|
||||||
}
|
}
|
||||||
|
pub fn read_remaining(&mut self) -> Vec<u8> {
|
||||||
|
let rest = self.remaining();
|
||||||
|
self.take(rest).map(<[u8]>::to_vec).unwrap_or_default()
|
||||||
|
}
|
||||||
pub fn expect_consumed(&self) -> Result<()> {
|
pub fn expect_consumed(&self) -> Result<()> {
|
||||||
match self.remaining() {
|
match self.remaining() {
|
||||||
0 => Ok(()),
|
0 => Ok(()),
|
||||||
|
|
|
||||||
|
|
@ -156,6 +156,9 @@ impl ByteStreamWriter {
|
||||||
self.buffer.extend_from_slice(bytes);
|
self.buffer.extend_from_slice(bytes);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
pub fn write_raw(&mut self, value: &[u8]) {
|
||||||
|
self.buffer.extend_from_slice(value);
|
||||||
|
}
|
||||||
pub fn write_bytes(&mut self, value: Option<&[u8]>) {
|
pub fn write_bytes(&mut self, value: Option<&[u8]>) {
|
||||||
match value {
|
match value {
|
||||||
None => self.write_int_raw(-1),
|
None => self.write_int_raw(-1),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue