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 config;
|
||||
pub mod home;
|
||||
|
|
@ -7,6 +8,7 @@ pub mod service;
|
|||
pub mod shop;
|
||||
pub mod store;
|
||||
pub mod time;
|
||||
pub use battle::BattleBuilder;
|
||||
pub use catalog::{Catalog, ARENA_FALLBACK_INSTANCE, GOLD_RESOURCE_FALLBACK_INSTANCE};
|
||||
pub use config::{CardRef, DataSelector, GameConfig, StarterProfile};
|
||||
pub use home::{build_avatar, build_home};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
use std::sync::Arc;
|
||||
use logic::table;
|
||||
use logic::{
|
||||
AvailableServerCommandMessage, EndClientTurnMessage, LogicCommandManager, LogicDataRef,
|
||||
LogicShopSeedChangedCommand, OutOfSyncMessage, OwnHomeDataMessage,
|
||||
LogicShopSeedChangedCommand, OutOfSyncMessage, OwnHomeDataMessage, SectorStateMessage,
|
||||
StartMissionMessage,
|
||||
};
|
||||
use service_rpc::{
|
||||
AccountRef, GameApi, GameRequest, GameResponse, HomeRequestKind, RpcError, RpcResult,
|
||||
RpcService, WireMessage,
|
||||
};
|
||||
use titan::{Message, MessageMeta, Payload};
|
||||
use crate::battle::BattleBuilder;
|
||||
use crate::catalog::Catalog;
|
||||
use crate::config::GameConfig;
|
||||
use crate::home::build_avatar;
|
||||
use crate::home_mode::{available_server_command, HomeModeRegistry};
|
||||
use crate::rewards::RewardRoller;
|
||||
use crate::shop::{ShopCatalog, ShopCycle};
|
||||
|
|
@ -20,6 +24,7 @@ pub struct GameService {
|
|||
profiles: ProfileStore,
|
||||
catalog: Arc<Catalog>,
|
||||
shop: Arc<ShopCatalog>,
|
||||
battles: Arc<BattleBuilder>,
|
||||
sessions: HomeModeRegistry,
|
||||
}
|
||||
impl GameService {
|
||||
|
|
@ -31,6 +36,9 @@ impl GameService {
|
|||
.map_err(connect_failed)?;
|
||||
let profile_count = profiles.count().await?;
|
||||
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() {
|
||||
tracing::debug!(offer, "shop offer");
|
||||
}
|
||||
|
|
@ -46,6 +54,7 @@ impl GameService {
|
|||
profiles,
|
||||
catalog,
|
||||
shop,
|
||||
battles,
|
||||
sessions: HomeModeRegistry::default(),
|
||||
}))
|
||||
}
|
||||
|
|
@ -162,6 +171,43 @@ impl GameApi for GameService {
|
|||
tracing::debug!(%account, ping_ms, "client capabilities");
|
||||
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(
|
||||
&self,
|
||||
account: AccountRef,
|
||||
|
|
@ -255,6 +301,9 @@ impl RpcService for GameService {
|
|||
self.client_capabilities(account, ping_ms).await?;
|
||||
Ok(GameResponse::Empty)
|
||||
}
|
||||
GameRequest::StartMission { account, payload } => Ok(GameResponse::messages(
|
||||
self.start_mission(account, payload).await?,
|
||||
)),
|
||||
GameRequest::EndClientTurn { account, payload } => Ok(GameResponse::messages(
|
||||
self.end_client_turn(account, payload).await?,
|
||||
)),
|
||||
|
|
|
|||
|
|
@ -89,6 +89,17 @@ impl GameApi for RemoteGame {
|
|||
.await?;
|
||||
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(
|
||||
&self,
|
||||
account: AccountRef,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let buy_chest = std::env::args()
|
||||
.skip_while(|arg| arg != "--buy-chest")
|
||||
.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());
|
||||
match LogicDataTables::load_from_dir(&csv_root) {
|
||||
Ok(tables) => {
|
||||
|
|
@ -181,7 +184,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let checksum = home.home.chest_id_counter
|
||||
+ ((home.home.spell_collection.spells.len() as i32) << 16)
|
||||
+ 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);
|
||||
match chest.as_treasure_chest() {
|
||||
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?;
|
||||
println!("-> 10108 KeepAliveMessage");
|
||||
}
|
||||
21903 => {
|
||||
println!("<- 21903 SectorStateMessage ({} bytes)", payload.len());
|
||||
println!("lobby reached, battle snapshot received");
|
||||
return Ok(());
|
||||
}
|
||||
message_type::KEEP_ALIVE_SERVER => {
|
||||
println!("<- 20108 KeepAliveServerMessage");
|
||||
println!("lobby reached, probe done");
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::net::SocketAddr;
|
|||
use std::sync::Arc;
|
||||
use logic::{
|
||||
message_type, ClientCapabilitiesMessage, GoHomeMessage, KeepAliveServerMessage,
|
||||
LoginFailedMessage, LoginMessage, LoginOkMessage, ServerErrorMessage, StartMissionMessage,
|
||||
LoginFailedMessage, LoginMessage, LoginOkMessage, ServerErrorMessage,
|
||||
};
|
||||
use service_rpc::{
|
||||
AccountRef, DeviceInfo, HomeRequestKind, LoginOutcome, Session as AuthSession, WireMessage,
|
||||
|
|
@ -89,20 +89,25 @@ impl MessageManager {
|
|||
self.push_home(HomeRequestKind::GoHome).await
|
||||
}
|
||||
message_type::START_MISSION => {
|
||||
let mission = incoming
|
||||
.downcast::<StartMissionMessage>()
|
||||
.expect("start mission message");
|
||||
tracing::warn!(
|
||||
peer = %self.peer,
|
||||
npc = %mission.npc,
|
||||
"client asked for an npc mission, which this server does not implement; \
|
||||
give the avatar a non zero npc_win_count so it goes to the home screen instead"
|
||||
);
|
||||
self.sender
|
||||
.send_message(&ServerErrorMessage::new(
|
||||
"npc missions are not implemented by this server",
|
||||
))
|
||||
.await?;
|
||||
let Some(account) = self.account else {
|
||||
return Ok(());
|
||||
};
|
||||
match self
|
||||
.backends
|
||||
.game
|
||||
.start_mission(account, incoming.payload)
|
||||
.await
|
||||
{
|
||||
Ok(replies) => self.push_wire_messages(replies).await?,
|
||||
Err(error) => {
|
||||
tracing::warn!(peer = %self.peer, %error, "could not start the mission");
|
||||
self.sender
|
||||
.send_message(&ServerErrorMessage::new(
|
||||
"this server could not build the battle",
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
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)]
|
||||
pub struct LogicGameObject {
|
||||
#[codec(vint)]
|
||||
pub hitpoints: i32,
|
||||
pub field_28: i32,
|
||||
#[codec(vint)]
|
||||
pub owner_index: i32,
|
||||
pub field_56: i32,
|
||||
pub position: LogicVector2,
|
||||
#[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 data: LogicDataRef,
|
||||
pub global_id: LogicGameObjectRef,
|
||||
pub object: LogicGameObject,
|
||||
pub body: LogicObjectBody,
|
||||
pub components: [Option<crate::battle::logic_component::LogicComponent>; COMPONENT_PASSES],
|
||||
}
|
||||
impl LogicGameObjectEntry {
|
||||
pub fn object_type(&self) -> i32 {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
use titan::{ByteStreamReader, ByteStreamWriter, Payload, Result};
|
||||
use crate::battle::logic_game_object::{
|
||||
LogicGameObject, LogicGameObjectEntry, COMPONENT_PASSES, OBJECT_TYPE_COUNT,
|
||||
};
|
||||
use crate::battle::logic_game_object_ref::LogicGameObjectRef;
|
||||
use crate::data::LogicDataRef;
|
||||
use crate::battle::logic_game_object::{LogicGameObjectEntry, COMPONENT_PASSES, OBJECT_TYPE_COUNT};
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LogicGameObjectManager {
|
||||
pub instance_counters: [i32; OBJECT_TYPE_COUNT],
|
||||
|
|
@ -44,38 +40,20 @@ impl Payload for LogicGameObjectManager {
|
|||
entry.global_id.encode(writer)?;
|
||||
}
|
||||
for entry in &self.objects {
|
||||
entry.object.encode(writer)?;
|
||||
entry.body.encode(writer)?;
|
||||
}
|
||||
for _ in 0..COMPONENT_PASSES {
|
||||
for _ in &self.objects {}
|
||||
for pass in 0..COMPONENT_PASSES {
|
||||
for entry in &self.objects {
|
||||
if let Some(component) = &entry.components[pass] {
|
||||
component.encode(writer)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn decode(reader: &mut ByteStreamReader<'_>) -> Result<Self> {
|
||||
let mut instance_counters = [0; OBJECT_TYPE_COUNT];
|
||||
for counter in instance_counters.iter_mut() {
|
||||
*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,
|
||||
})
|
||||
fn decode(_reader: &mut ByteStreamReader<'_>) -> Result<Self> {
|
||||
Err(titan::Error::Unsupported(
|
||||
"LogicGameObjectManager is encode only",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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_character;
|
||||
mod logic_component;
|
||||
mod logic_game_mode;
|
||||
mod logic_game_object;
|
||||
mod logic_game_object_manager;
|
||||
mod logic_game_object_ref;
|
||||
mod logic_tilemap;
|
||||
mod logic_time;
|
||||
mod logic_tutorial_manager;
|
||||
pub use logic_battle::{
|
||||
LogicBattle, BATTLE_INT_ARRAY, BATTLE_TRAILING_INTS, BATTLE_TYPE_NPC, BATTLE_TYPE_PVP,
|
||||
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::{
|
||||
LogicGameObject, LogicGameObjectEntry, LogicVector2, CHARACTER_OBJECT_TYPE, COMPONENT_PASSES,
|
||||
OBJECT_TYPE_COUNT,
|
||||
LogicGameObject, LogicGameObjectEntry, LogicObjectBody, LogicVector2, CHARACTER_OBJECT_TYPE,
|
||||
COMPONENT_PASSES, OBJECT_TYPE_COUNT,
|
||||
};
|
||||
pub use logic_game_object_manager::LogicGameObjectManager;
|
||||
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_tutorial_manager::LogicTutorialManager;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ mod login_failed;
|
|||
mod login_ok;
|
||||
mod out_of_sync;
|
||||
mod own_home_data;
|
||||
mod sector_state;
|
||||
mod server_error;
|
||||
mod start_mission;
|
||||
pub use available_server_command::AvailableServerCommandMessage;
|
||||
|
|
@ -29,6 +30,7 @@ pub use login_failed::{LoginFailedMessage, LoginFailureReason};
|
|||
pub use login_ok::LoginOkMessage;
|
||||
pub use out_of_sync::OutOfSyncMessage;
|
||||
pub use own_home_data::OwnHomeDataMessage;
|
||||
pub use sector_state::SectorStateMessage;
|
||||
pub use server_error::ServerErrorMessage;
|
||||
pub use start_mission::StartMissionMessage;
|
||||
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")]
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
StartMission {
|
||||
account: AccountRef,
|
||||
#[serde(with = "crate::base64::serde_bytes")]
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
Disconnect {
|
||||
account: AccountRef,
|
||||
},
|
||||
|
|
@ -72,6 +77,11 @@ pub trait GameApi: Send + Sync + 'static {
|
|||
kind: HomeRequestKind,
|
||||
) -> RpcResult<Vec<WireMessage>>;
|
||||
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(
|
||||
&self,
|
||||
account: AccountRef,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ impl Default for FieldSpec {
|
|||
}
|
||||
pub fn expand_payload(input: &DeriveInput) -> Result<TokenStream> {
|
||||
let ident = &input.ident;
|
||||
if has_raw(input)? {
|
||||
return expand_raw(input);
|
||||
}
|
||||
let partial = has_partial(input)?;
|
||||
let fields = match &input.data {
|
||||
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> {
|
||||
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 {
|
||||
if !attr.path().is_ident("codec") {
|
||||
continue;
|
||||
}
|
||||
attr.parse_nested_meta(|meta| {
|
||||
if meta.path.is_ident("partial") {
|
||||
partial = true;
|
||||
if meta.path.is_ident("partial") || meta.path.is_ident("raw") {
|
||||
if meta.path.is_ident(wanted) {
|
||||
found = true;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
Err(meta.error("unsupported container level #[codec(...)] key"))
|
||||
})?;
|
||||
}
|
||||
Ok(partial)
|
||||
Ok(found)
|
||||
}
|
||||
fn parse_field_spec(attrs: &[syn::Attribute]) -> Result<FieldSpec> {
|
||||
let mut spec = FieldSpec::default();
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ pub enum Error {
|
|||
UnknownCommandType(i32),
|
||||
#[error("trailing {0} unread byte(s)")]
|
||||
TrailingBytes(usize),
|
||||
#[error("{0}")]
|
||||
Unsupported(&'static str),
|
||||
#[error("io: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,6 +151,10 @@ impl<'a> ByteStreamReader<'a> {
|
|||
}
|
||||
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<()> {
|
||||
match self.remaining() {
|
||||
0 => Ok(()),
|
||||
|
|
|
|||
|
|
@ -156,6 +156,9 @@ impl ByteStreamWriter {
|
|||
self.buffer.extend_from_slice(bytes);
|
||||
Ok(())
|
||||
}
|
||||
pub fn write_raw(&mut self, value: &[u8]) {
|
||||
self.buffer.extend_from_slice(value);
|
||||
}
|
||||
pub fn write_bytes(&mut self, value: Option<&[u8]>) {
|
||||
match value {
|
||||
None => self.write_int_raw(-1),
|
||||
|
|
|
|||
Loading…
Reference in a new issue