1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-27 00:47:45 +00:00

Grab bag of various fixes

I have unsuccessfully tried to spawn another actor, the game recieves it (and
adds it to the object table) but they are marked invisible. Besises, this also
contains various field improvements and initial support for social lists.
This commit is contained in:
Joshua Goins 2025-03-16 14:07:56 -04:00
parent 1cbc5c72b9
commit 51f6ad6744
11 changed files with 311 additions and 134 deletions

View file

@ -7,8 +7,8 @@ use kawari::ipc::{GameMasterCommandType, IPCOpCode, IPCSegment, IPCStructData};
use kawari::oodle::FFXIVOodle;
use kawari::packet::{PacketSegment, SegmentType, State, send_keep_alive};
use kawari::world::{
ActorControlSelf, ActorControlType, ChatHandler, InitZone, PlayerSetup, PlayerSpawn,
PlayerStats, Position, UpdateClassInfo, Zone, ZoneConnection,
ActorControlSelf, ActorControlType, ChatHandler, InitZone, PlayerEntry, PlayerSetup,
PlayerSpawn, PlayerStats, Position, SocialList, UpdateClassInfo, Zone, ZoneConnection,
};
use kawari::{CHAR_NAME, CONTENT_ID, CUSTOMIZE_DATA, WORLD_ID, ZONE_ID, timestamp_secs};
use tokio::io::AsyncReadExt;
@ -38,6 +38,7 @@ async fn main() {
socket,
state,
player_id: 0,
spawn_index: 0,
zone: Zone::load(ZONE_ID),
};
@ -91,6 +92,7 @@ async fn main() {
target_actor: 0,
segment_type: SegmentType::ZoneInitialize {
player_id: *player_id,
timestamp: timestamp_secs(),
},
})
.await;
@ -107,6 +109,7 @@ async fn main() {
target_actor: 0,
segment_type: SegmentType::ZoneInitialize {
player_id: *player_id,
timestamp: timestamp_secs(),
},
})
.await;
@ -114,12 +117,10 @@ async fn main() {
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::InitializeChat,
server_id: 0,
timestamp: 0,
timestamp: timestamp_secs(),
data: IPCStructData::InitializeChat { unk: [0; 8] },
..Default::default()
};
connection
@ -146,16 +147,14 @@ async fn main() {
// IPC Init(?)
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::InitResponse,
server_id: 0,
op_code: IPCOpCode::Unk5,
timestamp: timestamp_secs(),
data: IPCStructData::InitResponse {
unk1: 0,
character_id: connection.player_id,
unk2: 0,
},
..Default::default()
};
connection
@ -170,10 +169,7 @@ async fn main() {
// Control Data
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::ActorControlSelf,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::ActorControlSelf(
ActorControlSelf {
@ -187,6 +183,7 @@ async fn main() {
param6: 0,
},
),
..Default::default()
};
connection
@ -201,10 +198,7 @@ async fn main() {
// Stats
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::PlayerStats,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::PlayerStats(PlayerStats {
strength: 1,
@ -212,6 +206,7 @@ async fn main() {
mp: 100,
..Default::default()
}),
..Default::default()
};
connection
@ -226,18 +221,17 @@ async fn main() {
// Player Setup
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::PlayerSetup,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::PlayerSetup(PlayerSetup {
content_id: CONTENT_ID,
exp: [10000; 32],
levels: [100; 32],
name: CHAR_NAME.to_string(),
char_id: connection.player_id,
..Default::default()
}),
..Default::default()
};
connection
@ -259,15 +253,13 @@ async fn main() {
// send welcome message
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::ServerChatMessage,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::ServerChatMessage {
message: "Welcome to Kawari!".to_string(),
unk: 0,
},
..Default::default()
};
connection
@ -282,12 +274,10 @@ async fn main() {
// send player spawn
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::PlayerSpawn,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::PlayerSpawn(PlayerSpawn {
content_id: CONTENT_ID,
current_world_id: WORLD_ID,
home_world_id: WORLD_ID,
title: 1,
@ -298,11 +288,11 @@ async fn main() {
mp_curr: 100,
mp_max: 100,
model_type: 1,
spawn_index: 1,
state: 1,
gm_rank: 3,
look: CUSTOMIZE_DATA,
fc_tag: "LOCAL".to_string(),
subtype: 4,
models: [
0, // head
89, // body
@ -319,6 +309,7 @@ async fn main() {
.unwrap_or(Position::default()),
..Default::default()
}),
..Default::default()
};
connection
@ -333,14 +324,12 @@ async fn main() {
// fade in?
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::PrepareZoning,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::PrepareZoning {
unk: [0, 0, 0, 0],
},
..Default::default()
};
connection
@ -373,8 +362,65 @@ async fn main() {
IPCStructData::Unk5 { .. } => {
tracing::info!("Recieved Unk5!");
}
IPCStructData::Unk6 { .. } => {
tracing::info!("Recieved Unk6!");
IPCStructData::SocialListRequest(request) => {
tracing::info!("Recieved social list request!");
match &request.request_type {
kawari::world::SocialListRequestType::Party => {
let ipc = IPCSegment {
op_code: IPCOpCode::SocialList,
timestamp: timestamp_secs(),
data: IPCStructData::SocialList(SocialList {
request_type: request.request_type,
sequence: request.count,
entries: vec![PlayerEntry {
content_id: CONTENT_ID,
zone_id: connection.zone.id,
zone_id1: 0x0100,
class_job: 36,
level: 100,
one: 1,
name: CHAR_NAME.to_string(),
fc_tag: "LOCAL".to_string(),
..Default::default()
}],
}),
..Default::default()
};
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc {
data: ipc,
},
})
.await;
}
kawari::world::SocialListRequestType::Friends => {
let ipc = IPCSegment {
op_code: IPCOpCode::SocialList,
timestamp: timestamp_secs(),
data: IPCStructData::SocialList(SocialList {
request_type: request.request_type,
sequence: request.count,
entries: Default::default(),
}),
..Default::default()
};
connection
.send_segment(PacketSegment {
source_actor: connection.player_id,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc {
data: ipc,
},
})
.await;
}
}
}
IPCStructData::Unk7 {
timestamp, unk1, ..
@ -384,15 +430,13 @@ async fn main() {
// send unk11 in response
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::Unk11,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::Unk11 {
timestamp: *timestamp,
unk: 333,
},
..Default::default()
};
connection
@ -413,12 +457,10 @@ async fn main() {
// tell the client to disconnect
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LogOutComplete,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::LogOutComplete { unk: [0; 8] },
..Default::default()
};
connection
@ -488,14 +530,12 @@ async fn main() {
// fade out?
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::PrepareZoning,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::PrepareZoning {
unk: [0x01000000, 0, 0, 0],
},
..Default::default()
};
connection
@ -510,14 +550,12 @@ async fn main() {
// fade out? x2
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::PrepareZoning,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::PrepareZoning {
unk: [0, 0x00000085, 0x00030000, 0x000008ff], // last thing is probably a float?
},
..Default::default()
};
connection

View file

@ -5,7 +5,7 @@ use crate::{
common::{read_string, write_string},
world::{
ActorControlSelf, ChatMessage, InitZone, PlayerSetup, PlayerSpawn, PlayerStats, Position,
UpdateClassInfo,
SocialList, SocialListRequest, UpdateClassInfo,
},
};
@ -38,8 +38,6 @@ pub enum IPCOpCode {
/// Sent by the client when they successfully initialize with the server, and they need several bits of information (e.g. what zone to load)
InitRequest = 0x2ED,
/// Sent by the server as response to ZoneInitRequest.
InitResponse = 0x1EF,
/// Sent by the server that tells the client which zone to load
InitZone = 0x0311,
/// Sent by the server for... something
@ -58,16 +56,17 @@ pub enum IPCOpCode {
// FIXME: 32 bytes of something from the client, not sure what yet
Unk1 = 0x37C,
// FIXME: 16 bytes of something from the client, not sure what yet
Unk2 = 0x1A1,
Unk2 = 0x2E5,
// FIXME: 8 bytes of something from the client, not sure what yet
Unk3 = 0x326,
// FIXME: 8 bytes of something from the client, not sure what yet
Unk4 = 0x143,
SetSearchInfoHandler = 0x3B2, // TODO: assumed,
// FIXME: 8 bytes of something from the client, not sure what yet
/// ALSO Sent by the server as response to ZoneInitRequest.
Unk5 = 0x2D0,
// FIXME: 8 bytes of something from the client, not sure what yet
Unk6 = 0x2E5,
// Sent by the client when it requests the friends list and other related info
SocialListRequest = 0x1A1,
// FIXME: 32 bytes of something from the client, not sure what yet
Unk7 = 0x2B5,
UpdatePositionHandler = 0x249, // TODO: assumed
@ -109,6 +108,18 @@ pub enum IPCOpCode {
PrepareZoning = 0x308,
// Sent by the client for unknown reasons
Unk14 = 0x87,
// Sent by the server???
Unk15 = 0x28C,
// Sent by the server before init zone???
Unk16 = 0x3AB,
// Sent by the server
ActorControl = 0x1B9,
// Sent by the server
ActorMove = 0x3D8,
// Sent by the server
Unk17 = 0x2A1,
// Sent by the server in response to SocialListRequest
SocialList = 0x36C,
}
#[binrw]
@ -313,11 +324,8 @@ pub enum IPCStructData {
// TODO: full of possibly interesting information
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::Unk6))]
Unk6 {
// TODO: full of possibly interesting information
unk: [u8; 8],
},
#[br(pre_assert(*magic == IPCOpCode::SocialListRequest))]
SocialListRequest(SocialListRequest),
#[br(pre_assert(*magic == IPCOpCode::Unk7))]
Unk7 {
// TODO: full of possibly interesting information
@ -523,6 +531,24 @@ pub enum IPCStructData {
},
#[br(pre_assert(false))]
PrepareZoning { unk: [u32; 4] },
#[br(pre_assert(false))]
Unk15 { unk: u32, player_id: u32 },
#[br(pre_assert(false))]
Unk16 { unk: [u8; 136] },
#[br(pre_assert(false))]
ActorControl {
#[brw(pad_after = 20)] // empty
unk: u32,
},
#[br(pre_assert(false))]
ActorMove {
#[brw(pad_after = 4)] // empty
pos: Position,
},
#[br(pre_assert(false))]
Unk17 { unk: [u8; 104] },
#[br(pre_assert(false))]
SocialList(SocialList),
}
#[binrw]
@ -542,6 +568,22 @@ pub struct IPCSegment {
pub data: IPCStructData,
}
impl Default for IPCSegment {
fn default() -> Self {
Self {
unk1: 0x14,
unk2: 0,
op_code: IPCOpCode::InitializeChat,
server_id: 0,
timestamp: 0,
data: IPCStructData::ClientVersionInfo {
session_id: String::new(),
version_info: String::new(),
},
}
}
}
impl IPCSegment {
pub fn calc_size(&self) -> u32 {
let header = 16;
@ -562,7 +604,7 @@ impl IPCSegment {
IPCStructData::InitZone { .. } => 103,
IPCStructData::ActorControlSelf { .. } => 32,
IPCStructData::PlayerStats { .. } => 224,
IPCStructData::PlayerSetup { .. } => 2545,
IPCStructData::PlayerSetup { .. } => 2784,
IPCStructData::UpdateClassInfo { .. } => 48,
IPCStructData::FinishLoading { .. } => todo!(),
IPCStructData::PlayerSpawn { .. } => 656,
@ -572,7 +614,7 @@ impl IPCSegment {
IPCStructData::Unk4 { .. } => todo!(),
IPCStructData::SetSearchInfoHandler { .. } => todo!(),
IPCStructData::Unk5 { .. } => todo!(),
IPCStructData::Unk6 { .. } => todo!(),
IPCStructData::SocialListRequest { .. } => todo!(),
IPCStructData::Unk7 { .. } => todo!(),
IPCStructData::UpdatePositionHandler { .. } => todo!(),
IPCStructData::LogOut { .. } => todo!(),
@ -594,6 +636,12 @@ impl IPCSegment {
IPCStructData::Unk13 { .. } => todo!(),
IPCStructData::PrepareZoning { .. } => 16,
IPCStructData::Unk14 { .. } => todo!(),
IPCStructData::Unk15 { .. } => 8,
IPCStructData::Unk16 { .. } => 136,
IPCStructData::ActorControl { .. } => 24,
IPCStructData::ActorMove { .. } => 16,
IPCStructData::Unk17 { .. } => 104,
IPCStructData::SocialList { .. } => 1136,
}
}
}

View file

@ -69,8 +69,9 @@ pub enum SegmentType {
KeepAliveResponse { id: u32, timestamp: u32 },
#[brw(magic = 0x2u32)]
ZoneInitialize {
#[brw(pad_after = 36)]
player_id: u32,
#[brw(pad_after = 32)]
timestamp: u32,
},
}

View file

@ -1,3 +1,15 @@
use std::io::Cursor;
use binrw::BinRead;
use crate::{
CHAR_NAME, CUSTOMIZE_DATA, WORLD_ID,
ipc::{IPCOpCode, IPCSegment, IPCStructData},
packet::{PacketSegment, SegmentType},
timestamp_secs,
world::PlayerSpawn,
};
use super::{ChatMessage, Position, ZoneConnection};
pub struct ChatHandler {}
@ -21,6 +33,61 @@ impl ChatHandler {
})
.await;
}
"!spawnactor" => {
println!("Spawning actor...");
// send player spawn
{
let ipc = IPCSegment {
unk1: 20,
unk2: 0,
op_code: IPCOpCode::PlayerSpawn,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::PlayerSpawn(PlayerSpawn {
some_unique_id: 1,
content_id: 1,
current_world_id: WORLD_ID,
home_world_id: WORLD_ID,
title: 1,
class_job: 35,
name: CHAR_NAME.to_string(),
hp_curr: 100,
hp_max: 100,
mp_curr: 100,
mp_max: 100,
model_type: 1,
state: 1,
gm_rank: 3,
spawn_index: connection.get_free_spawn_index(),
look: CUSTOMIZE_DATA,
fc_tag: "LOCAL".to_string(),
models: [
0, // head
89, // body
89, // hands
89, // legs
89, // feet
0, // ears
0, // neck
0, // wrists
0, // left finger
0, // right finger
],
pos: Position::default(),
..Default::default()
}),
};
connection
.send_segment(PacketSegment {
source_actor: 0x106ad804,
target_actor: connection.player_id,
segment_type: SegmentType::Ipc { data: ipc },
})
.await;
}
}
_ => tracing::info!("Unrecognized debug command!"),
}
}

View file

@ -1,3 +1,6 @@
use std::io::Cursor;
use binrw::BinRead;
use tokio::net::TcpStream;
use crate::{
@ -19,6 +22,7 @@ pub struct ZoneConnection {
pub player_id: u32,
pub zone: Zone,
pub spawn_index: u8,
}
impl ZoneConnection {
@ -40,16 +44,14 @@ impl ZoneConnection {
// set pos
{
let ipc = IPCSegment {
unk1: 14,
unk2: 0,
op_code: IPCOpCode::ActorSetPos,
server_id: WORLD_ID,
timestamp: timestamp_secs(),
data: IPCStructData::ActorSetPos(ActorSetPos {
unk: 0x020fa3b8,
position,
..Default::default()
}),
..Default::default()
};
let response_packet = PacketSegment {
@ -73,10 +75,7 @@ impl ZoneConnection {
// Player Class Info
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::UpdateClassInfo,
server_id: 69, // lol
timestamp: timestamp_secs(),
data: IPCStructData::UpdateClassInfo(UpdateClassInfo {
class_id: 35,
@ -85,6 +84,7 @@ impl ZoneConnection {
class_level: 90,
..Default::default()
}),
..Default::default()
};
self.send_segment(PacketSegment {
@ -95,56 +95,13 @@ impl ZoneConnection {
.await;
}
// unk10
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::Unk10,
server_id: 69, // lol
timestamp: timestamp_secs(),
data: IPCStructData::Unk10 {
unk: 0x41a0000000000002,
},
};
self.send_segment(PacketSegment {
source_actor: self.player_id,
target_actor: self.player_id,
segment_type: SegmentType::Ipc { data: ipc },
})
.await;
}
// unk9
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::Unk9,
server_id: 69, // lol
timestamp: timestamp_secs(),
data: IPCStructData::Unk9 { unk: [0; 24] },
};
self.send_segment(PacketSegment {
source_actor: self.player_id,
target_actor: self.player_id,
segment_type: SegmentType::Ipc { data: ipc },
})
.await;
}
// TODO: maybe only sent on initial login not every zone?
// link shell information
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::LinkShellInformation,
server_id: 69, // lol
timestamp: timestamp_secs(),
data: IPCStructData::LinkShellInformation { unk: [0; 456] },
..Default::default()
};
self.send_segment(PacketSegment {
@ -155,39 +112,20 @@ impl ZoneConnection {
.await;
}
// unk8
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::Unk8,
server_id: 69, // lol
timestamp: timestamp_secs(),
data: IPCStructData::Unk8 { unk: [0; 808] },
};
self.send_segment(PacketSegment {
source_actor: self.player_id,
target_actor: self.player_id,
segment_type: SegmentType::Ipc { data: ipc },
})
.await;
}
// TODO: send unk16?
// Init Zone
{
let ipc = IPCSegment {
unk1: 0,
unk2: 0,
op_code: IPCOpCode::InitZone,
server_id: 0,
timestamp: timestamp_secs(),
data: IPCStructData::InitZone(InitZone {
server_id: WORLD_ID,
server_id: 0,
zone_id: self.zone.id,
weather_id: 1,
..Default::default()
}),
..Default::default()
};
self.send_segment(PacketSegment {
@ -198,4 +136,9 @@ impl ZoneConnection {
.await;
}
}
pub fn get_free_spawn_index(&mut self) -> u8 {
self.spawn_index += 1;
return self.spawn_index;
}
}

View file

@ -11,7 +11,6 @@ pub struct InitZone {
pub content_finder_condition_id: u16,
pub layer_set_id: u32,
pub layout_id: u32,
#[br(dbg)]
pub weather_id: u16, // index into Weather sheet probably?
pub unk_really: u16,
pub unk_bitmask1: u8,

View file

@ -34,3 +34,9 @@ pub use connection::ZoneConnection;
mod chat_message;
pub use chat_message::ChatMessage;
mod social_list;
pub use social_list::PlayerEntry;
pub use social_list::SocialList;
pub use social_list::SocialListRequest;
pub use social_list::SocialListRequestType;

View file

@ -82,7 +82,9 @@ pub struct PlayerSetup {
#[bw(pad_size_to = 33)]
pub mount_guide_mask: Vec<u8>,
pub ornament_mask: [u8; 4],
pub unknown281: [u8; 23],
#[br(count = 85)]
#[bw(pad_size_to = 85)]
pub unknown281: Vec<u8>,
#[br(count = CHAR_NAME_MAX_LENGTH)]
#[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)]

View file

@ -11,7 +11,11 @@ use super::status_effect::StatusEffect;
#[brw(little)]
#[derive(Debug, Clone, Default)]
pub struct PlayerSpawn {
pub aafafaf: [u8; 16],
// also shows up in the friends list.
pub some_unique_id: u32,
#[brw(pad_before = 4)] // always empty?
pub content_id: u64,
pub title: u16,
pub u1b: u16,
@ -121,6 +125,7 @@ mod tests {
assert_eq!(player_spawn.mp_curr, 10000);
assert_eq!(player_spawn.mp_max, 10000);
assert_eq!(player_spawn.state, 1);
assert_eq!(player_spawn.spawn_index, 0);
assert_eq!(player_spawn.level, 1);
assert_eq!(player_spawn.class_job, 1); // adventurer
assert_eq!(player_spawn.scale, 36);
@ -132,5 +137,6 @@ mod tests {
assert_eq!(player_spawn.look.gender, 1);
assert_eq!(player_spawn.look.bust, 100);
assert_eq!(player_spawn.fc_tag, "");
assert_eq!(player_spawn.subtype, 4);
}
}

62
src/world/social_list.rs Normal file
View file

@ -0,0 +1,62 @@
use binrw::binrw;
use crate::{
CHAR_NAME_MAX_LENGTH,
common::{read_string, write_string},
};
#[binrw]
#[brw(repr = u8)]
#[derive(Debug, Clone, Copy)]
pub enum SocialListRequestType {
Party = 0x1,
Friends = 0x2,
}
#[binrw]
#[derive(Debug, Clone)]
pub struct SocialListRequest {
#[brw(pad_before = 10)] // empty
pub request_type: SocialListRequestType,
#[brw(pad_after = 4)] // empty
pub count: u8,
}
#[binrw]
#[derive(Debug, Clone, Default)]
pub struct PlayerEntry {
pub content_id: u64,
pub unk: [u8; 12],
pub zone_id: u16,
pub zone_id1: u16,
pub unk2: [u8; 8],
pub online_status_mask: u64,
pub class_job: u8,
pub padding: u8,
pub level: u8,
pub padding1: u8,
pub padding2: u16,
pub one: u32,
#[br(count = CHAR_NAME_MAX_LENGTH)]
#[bw(pad_size_to = CHAR_NAME_MAX_LENGTH)]
#[br(map = read_string)]
#[bw(map = write_string)]
pub name: String,
#[br(count = 6)]
#[bw(pad_size_to = 6)]
#[br(map = read_string)]
#[bw(map = write_string)]
pub fc_tag: String,
}
#[binrw]
#[derive(Debug, Clone)]
pub struct SocialList {
#[brw(pad_before = 12)] // empty
pub request_type: SocialListRequestType,
pub sequence: u8,
#[brw(pad_before = 2)] // empty
#[br(count = 10)]
#[bw(pad_size_to = 112 * 10)]
pub entries: Vec<PlayerEntry>,
}

View file

@ -22,7 +22,9 @@ impl Zone {
GameData::from_existing(Platform::Win32, &config.game_location).unwrap();
let exh = game_data.read_excel_sheet_header("TerritoryType").unwrap();
let exd = game_data.read_excel_sheet("TerritoryType", &exh, Language::None, 0).unwrap();
let exd = game_data
.read_excel_sheet("TerritoryType", &exh, Language::None, 0)
.unwrap();
let territory_type_row = &exd.read_row(&exh, id as u32).unwrap()[0];
@ -31,7 +33,10 @@ impl Zone {
panic!("Unexpected type!");
};
let path = format!("bg/{}/level/planmap.lgb", &bg_path[..bg_path.find("/level/").unwrap()]);
let path = format!(
"bg/{}/level/planmap.lgb",
&bg_path[..bg_path.find("/level/").unwrap()]
);
let lgb = game_data.extract(&path).unwrap();
let layer_group = LayerGroup::from_existing(&lgb).unwrap();
Self { id, layer_group }