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

Load the starting position from the LGB

It turns out (amazingly) that this data exists client-side, I guess because the
server and the client share the same planevent LGB file. So instead of
hardcoding the starting location in each city, it's now literally like retail*

* Except for the fact that we don't support rotation yet, and positions in pop
ranges are probably randomized too. But it's close!
This commit is contained in:
Joshua Goins 2025-03-28 22:34:34 -04:00
parent c4b9ad060b
commit 2bf9385079
6 changed files with 117 additions and 27 deletions

16
Cargo.lock generated
View file

@ -31,9 +31,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.8.1" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
dependencies = [ dependencies = [
"axum-core", "axum-core",
"bytes", "bytes",
@ -64,12 +64,12 @@ dependencies = [
[[package]] [[package]]
name = "axum-core" name = "axum-core"
version = "0.5.0" version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-util", "futures-core",
"http", "http",
"http-body", "http-body",
"http-body-util", "http-body-util",
@ -601,9 +601,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.2" version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
@ -637,7 +637,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "physis" name = "physis"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/redstrate/physis#eee7d5867fb7424592ff1249f28c90dbe1daec13" source = "git+https://github.com/redstrate/physis#a34361b9560a95bdd257ee4fe721dbe1156a5610"
dependencies = [ dependencies = [
"binrw", "binrw",
"bitflags 1.3.2", "bitflags 1.3.2",

View file

@ -1,16 +1,22 @@
--- TODO: find a way to hardcode it this way --- TODO: find a way to hardcode it this way
EVENT_ID = 1245187 EVENT_ID = 1245187
--- load defines from Opening Excel sheet, which has this and we don't need to hardcode it'
POS_START = 4101669
function Scene00000(player) function Scene00000(player)
player:play_scene(EVENT_ID, 00000, 4959237, 1) player:play_scene(EVENT_ID, 00000, 4959237, 1)
end end
function Scene00001(player) function Scene00001(player)
--- todo put player in correct position
player:play_scene(EVENT_ID, 00001, 4959237, 1) player:play_scene(EVENT_ID, 00001, 4959237, 1)
end end
function onEnterTerritory(player) function onEnterTerritory(player, zone)
--- move the player into the starting position
start_pos = zone:get_pop_range(POS_START)
player:set_position(start_pos)
Scene00000(player); Scene00000(player);
end end

View file

@ -7,7 +7,7 @@ use crate::{
Event, Event,
ipc::{ ipc::{
ActorControl, ActorControlCategory, BattleNpcSubKind, CommonSpawn, DisplayFlag, ActorControl, ActorControlCategory, BattleNpcSubKind, CommonSpawn, DisplayFlag,
EventPlay, EventStart, NpcSpawn, ObjectKind, OnlineStatus, PlayerSpawn, PlayerSubKind, EventStart, NpcSpawn, ObjectKind, OnlineStatus, PlayerSpawn, PlayerSubKind,
ServerZoneIpcData, ServerZoneIpcSegment, ServerZoneIpcData, ServerZoneIpcSegment,
}, },
}, },
@ -334,7 +334,7 @@ impl ChatHandler {
.event .event
.as_mut() .as_mut()
.unwrap() .unwrap()
.enter_territory(lua_player); .enter_territory(lua_player, connection.zone.as_ref().unwrap());
} }
_ => tracing::info!("Unrecognized debug command!"), _ => tracing::info!("Unrecognized debug command!"),
} }

View file

@ -2,7 +2,7 @@ use mlua::{Function, Lua};
use crate::config::get_config; use crate::config::get_config;
use super::LuaPlayer; use super::{LuaPlayer, Zone};
pub struct Event { pub struct Event {
lua: Lua, lua: Lua,
@ -22,14 +22,15 @@ impl Event {
Self { lua } Self { lua }
} }
pub fn enter_territory(&mut self, player: &mut LuaPlayer) { pub fn enter_territory(&mut self, player: &mut LuaPlayer, zone: &Zone) {
self.lua self.lua
.scope(|scope| { .scope(|scope| {
let player = scope.create_userdata_ref_mut(player).unwrap(); let player = scope.create_userdata_ref_mut(player).unwrap();
let zone = scope.create_userdata_ref(zone).unwrap();
let func: Function = self.lua.globals().get("onEnterTerritory").unwrap(); let func: Function = self.lua.globals().get("onEnterTerritory").unwrap();
func.call::<()>(player).unwrap(); func.call::<()>((player, zone)).unwrap();
Ok(()) Ok(())
}) })

View file

@ -1,14 +1,14 @@
use mlua::{UserData, UserDataMethods}; use mlua::{FromLua, Lua, UserData, UserDataMethods, Value};
use crate::{ use crate::{
common::{ObjectId, ObjectTypeId, timestamp_secs}, common::{ObjectId, ObjectTypeId, Position, timestamp_secs},
opcodes::ServerZoneIpcType, opcodes::ServerZoneIpcType,
packet::{PacketSegment, SegmentType}, packet::{PacketSegment, SegmentType},
}; };
use super::{ use super::{
PlayerData, StatusEffects, PlayerData, StatusEffects, Zone,
ipc::{EventPlay, ServerZoneIpcData, ServerZoneIpcSegment}, ipc::{ActorSetPos, EventPlay, ServerZoneIpcData, ServerZoneIpcSegment},
}; };
#[derive(Default)] #[derive(Default)]
@ -71,6 +71,25 @@ impl LuaPlayer {
segment_type: SegmentType::Ipc { data: ipc }, segment_type: SegmentType::Ipc { data: ipc },
}); });
} }
fn set_position(&mut self, position: Position) {
let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::ActorSetPos,
timestamp: timestamp_secs(),
data: ServerZoneIpcData::ActorSetPos(ActorSetPos {
unk: 0x020fa3b8,
position,
..Default::default()
}),
..Default::default()
};
self.queue_segment(PacketSegment {
source_actor: self.player_data.actor_id,
target_actor: self.player_data.actor_id,
segment_type: SegmentType::Ipc { data: ipc },
});
}
} }
impl UserData for LuaPlayer { impl UserData for LuaPlayer {
@ -93,5 +112,40 @@ impl UserData for LuaPlayer {
Ok(()) Ok(())
}, },
); );
methods.add_method_mut("set_position", |_, this, position: Position| {
this.set_position(position);
Ok(())
});
}
}
impl UserData for Position {}
impl FromLua for Position {
fn from_lua(value: Value, _: &Lua) -> mlua::Result<Self> {
match value {
Value::UserData(ud) => Ok(*ud.borrow::<Self>()?),
_ => unreachable!(),
}
}
}
impl UserData for Zone {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method(
"get_pop_range",
|_, this, id: u32| -> mlua::Result<Position> {
if let Some(pop_range) = this.find_pop_range(id) {
let trans = pop_range.0.transform.translation;
return Ok(Position {
x: trans[0],
y: trans[1],
z: trans[2],
});
}
// FIXME: return nil?
Ok(Position::default())
},
);
} }
} }

View file

@ -9,9 +9,16 @@ use physis::{
use crate::config::get_config; use crate::config::get_config;
/// Represents a loaded zone /// Represents a loaded zone
#[derive(Default)]
pub struct Zone { pub struct Zone {
pub id: u16, pub id: u16,
layer_group: Option<LayerGroup>, planevent: Option<LayerGroup>,
vfx: Option<LayerGroup>,
planmap: Option<LayerGroup>,
planner: Option<LayerGroup>,
bg: Option<LayerGroup>,
sound: Option<LayerGroup>,
planlive: Option<LayerGroup>,
} }
impl Zone { impl Zone {
@ -20,7 +27,7 @@ impl Zone {
let mut zone = Self { let mut zone = Self {
id, id,
layer_group: None, ..Default::default()
}; };
let Some(mut game_data) = GameData::from_existing(Platform::Win32, &config.game_location) let Some(mut game_data) = GameData::from_existing(Platform::Win32, &config.game_location)
@ -49,11 +56,22 @@ impl Zone {
return zone; return zone;
}; };
let path = format!("bg/{}/level/planmap.lgb", &bg_path[..level_index]); let mut load_lgb = |name: &str| -> Option<LayerGroup> {
let path = format!("bg/{}/level/{}.lgb", &bg_path[..level_index], name);
tracing::info!("Loading {path}");
let Some(lgb) = game_data.extract(&path) else { let Some(lgb) = game_data.extract(&path) else {
return zone; return None;
}; };
zone.layer_group = LayerGroup::from_existing(&lgb); LayerGroup::from_existing(&lgb)
};
zone.planevent = load_lgb("planevent");
zone.vfx = load_lgb("vfx");
zone.planmap = load_lgb("planmap");
zone.planner = load_lgb("planner");
zone.bg = load_lgb("bg");
zone.sound = load_lgb("sound");
zone.planlive = load_lgb("planlive");
zone zone
} }
@ -64,7 +82,7 @@ impl Zone {
instance_id: u32, instance_id: u32,
) -> Option<(&InstanceObject, &ExitRangeInstanceObject)> { ) -> Option<(&InstanceObject, &ExitRangeInstanceObject)> {
// TODO: also check position! // TODO: also check position!
for group in &self.layer_group.as_ref().unwrap().layers { for group in &self.planmap.as_ref().unwrap().layers {
for object in &group.objects { for object in &group.objects {
if let LayerEntryData::ExitRange(exit_range) = &object.data { if let LayerEntryData::ExitRange(exit_range) = &object.data {
if object.instance_id == instance_id { if object.instance_id == instance_id {
@ -82,7 +100,18 @@ impl Zone {
instance_id: u32, instance_id: u32,
) -> Option<(&InstanceObject, &PopRangeInstanceObject)> { ) -> Option<(&InstanceObject, &PopRangeInstanceObject)> {
// TODO: also check position! // TODO: also check position!
for group in &self.layer_group.as_ref().unwrap().layers { for group in &self.planmap.as_ref().unwrap().layers {
for object in &group.objects {
if let LayerEntryData::PopRange(pop_range) = &object.data {
if object.instance_id == instance_id {
return Some((object, pop_range));
}
}
}
}
// For certain PopRanges (e.g. the starting position in the opening zones)
for group in &self.planevent.as_ref().unwrap().layers {
for object in &group.objects { for object in &group.objects {
if let LayerEntryData::PopRange(pop_range) = &object.data { if let LayerEntryData::PopRange(pop_range) = &object.data {
if object.instance_id == instance_id { if object.instance_id == instance_id {