1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-22 07:27:44 +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]]
name = "axum"
version = "0.8.1"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
checksum = "de45108900e1f9b9242f7f2e254aa3e2c029c921c258fe9e6b4217eeebd54288"
dependencies = [
"axum-core",
"bytes",
@ -64,12 +64,12 @@ dependencies = [
[[package]]
name = "axum-core"
version = "0.5.0"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
dependencies = [
"bytes",
"futures-util",
"futures-core",
"http",
"http-body",
"http-body-util",
@ -601,9 +601,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.2"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "parking_lot"
@ -637,7 +637,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "physis"
version = "0.4.0"
source = "git+https://github.com/redstrate/physis#eee7d5867fb7424592ff1249f28c90dbe1daec13"
source = "git+https://github.com/redstrate/physis#a34361b9560a95bdd257ee4fe721dbe1156a5610"
dependencies = [
"binrw",
"bitflags 1.3.2",

View file

@ -1,16 +1,22 @@
--- TODO: find a way to hardcode it this way
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)
player:play_scene(EVENT_ID, 00000, 4959237, 1)
end
function Scene00001(player)
--- todo put player in correct position
player:play_scene(EVENT_ID, 00001, 4959237, 1)
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);
end

View file

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

View file

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

View file

@ -1,14 +1,14 @@
use mlua::{UserData, UserDataMethods};
use mlua::{FromLua, Lua, UserData, UserDataMethods, Value};
use crate::{
common::{ObjectId, ObjectTypeId, timestamp_secs},
common::{ObjectId, ObjectTypeId, Position, timestamp_secs},
opcodes::ServerZoneIpcType,
packet::{PacketSegment, SegmentType},
};
use super::{
PlayerData, StatusEffects,
ipc::{EventPlay, ServerZoneIpcData, ServerZoneIpcSegment},
PlayerData, StatusEffects, Zone,
ipc::{ActorSetPos, EventPlay, ServerZoneIpcData, ServerZoneIpcSegment},
};
#[derive(Default)]
@ -71,6 +71,25 @@ impl LuaPlayer {
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 {
@ -93,5 +112,40 @@ impl UserData for LuaPlayer {
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;
/// Represents a loaded zone
#[derive(Default)]
pub struct Zone {
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 {
@ -20,7 +27,7 @@ impl Zone {
let mut zone = Self {
id,
layer_group: None,
..Default::default()
};
let Some(mut game_data) = GameData::from_existing(Platform::Win32, &config.game_location)
@ -49,11 +56,22 @@ impl Zone {
return zone;
};
let path = format!("bg/{}/level/planmap.lgb", &bg_path[..level_index]);
let Some(lgb) = game_data.extract(&path) else {
return zone;
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 {
return None;
};
LayerGroup::from_existing(&lgb)
};
zone.layer_group = 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
}
@ -64,7 +82,7 @@ impl Zone {
instance_id: u32,
) -> Option<(&InstanceObject, &ExitRangeInstanceObject)> {
// 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::ExitRange(exit_range) = &object.data {
if object.instance_id == instance_id {
@ -82,7 +100,18 @@ impl Zone {
instance_id: u32,
) -> Option<(&InstanceObject, &PopRangeInstanceObject)> {
// 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 {
if let LayerEntryData::PopRange(pop_range) = &object.data {
if object.instance_id == instance_id {