2025-03-16 14:07:56 -04:00
|
|
|
use crate::{
|
2025-03-23 18:14:14 -04:00
|
|
|
common::{CustomizeData, ObjectId, ObjectTypeId, Position, timestamp_secs},
|
2025-03-22 17:00:21 -04:00
|
|
|
config::get_config,
|
2025-05-02 00:47:11 -04:00
|
|
|
ipc::zone::{
|
|
|
|
ActorControl, ActorControlCategory, BattleNpcSubKind, ChatMessage, CommonSpawn,
|
|
|
|
DisplayFlag, EventStart, NpcSpawn, ObjectKind, OnlineStatus, PlayerSpawn, PlayerSubKind,
|
|
|
|
ServerZoneIpcData, ServerZoneIpcSegment,
|
|
|
|
},
|
2025-03-26 18:28:51 -04:00
|
|
|
opcodes::ServerZoneIpcType,
|
2025-05-02 00:03:36 -04:00
|
|
|
packet::{PacketSegment, SegmentData, SegmentType},
|
2025-05-02 00:47:11 -04:00
|
|
|
world::{Actor, Event},
|
2025-03-16 14:07:56 -04:00
|
|
|
};
|
|
|
|
|
2025-05-02 00:47:11 -04:00
|
|
|
use super::{LuaPlayer, ZoneConnection};
|
2025-03-15 20:36:39 -04:00
|
|
|
|
2025-03-22 17:06:16 -04:00
|
|
|
pub const CUSTOMIZE_DATA: CustomizeData = CustomizeData {
|
|
|
|
race: 4,
|
|
|
|
gender: 1,
|
|
|
|
age: 1,
|
|
|
|
height: 50,
|
|
|
|
subrace: 7,
|
|
|
|
face: 3,
|
|
|
|
hair: 5,
|
|
|
|
enable_highlights: 0,
|
|
|
|
skin_tone: 10,
|
|
|
|
right_eye_color: 75,
|
|
|
|
hair_tone: 50,
|
|
|
|
highlights: 0,
|
|
|
|
facial_features: 1,
|
|
|
|
facial_feature_color: 19,
|
|
|
|
eyebrows: 1,
|
|
|
|
left_eye_color: 75,
|
|
|
|
eyes: 1,
|
|
|
|
nose: 0,
|
|
|
|
jaw: 1,
|
|
|
|
mouth: 1,
|
|
|
|
lips_tone_fur_pattern: 169,
|
|
|
|
race_feature_size: 100,
|
|
|
|
race_feature_type: 1,
|
|
|
|
bust: 100,
|
|
|
|
face_paint: 0,
|
|
|
|
face_paint_color: 167,
|
|
|
|
};
|
|
|
|
|
2025-03-15 20:36:39 -04:00
|
|
|
pub struct ChatHandler {}
|
|
|
|
|
|
|
|
impl ChatHandler {
|
2025-03-28 21:28:30 -04:00
|
|
|
pub async fn handle_chat_message(
|
|
|
|
connection: &mut ZoneConnection,
|
|
|
|
lua_player: &mut LuaPlayer,
|
|
|
|
chat_message: &ChatMessage,
|
|
|
|
) {
|
2025-03-15 20:36:39 -04:00
|
|
|
tracing::info!("Client sent chat message: {}!", chat_message.message);
|
|
|
|
|
|
|
|
let parts: Vec<&str> = chat_message.message.split(' ').collect();
|
|
|
|
match parts[0] {
|
2025-04-01 19:15:08 -04:00
|
|
|
"!spawnplayer" => {
|
2025-03-22 17:00:21 -04:00
|
|
|
let config = get_config();
|
|
|
|
|
2025-03-16 14:07:56 -04:00
|
|
|
// send player spawn
|
|
|
|
{
|
2025-03-16 17:43:29 -04:00
|
|
|
let ipc = ServerZoneIpcSegment {
|
2025-03-16 14:07:56 -04:00
|
|
|
unk1: 20,
|
|
|
|
unk2: 0,
|
2025-03-16 17:43:29 -04:00
|
|
|
op_code: ServerZoneIpcType::PlayerSpawn,
|
2025-05-02 00:17:15 -04:00
|
|
|
option: 0,
|
2025-03-16 14:07:56 -04:00
|
|
|
timestamp: timestamp_secs(),
|
2025-03-16 17:43:29 -04:00
|
|
|
data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn {
|
2025-03-23 15:28:53 -04:00
|
|
|
account_id: 1000000,
|
|
|
|
content_id: 1000000,
|
2025-03-23 12:16:15 -04:00
|
|
|
current_world_id: config.world.world_id,
|
|
|
|
home_world_id: config.world.world_id,
|
2025-03-18 22:13:28 -04:00
|
|
|
common: CommonSpawn {
|
|
|
|
class_job: 35,
|
2025-03-22 17:06:16 -04:00
|
|
|
name: "Test Actor".to_string(),
|
2025-03-23 12:54:04 -04:00
|
|
|
hp_curr: 250,
|
|
|
|
hp_max: 250,
|
|
|
|
mp_curr: 10000,
|
|
|
|
mp_max: 10000,
|
|
|
|
level: 5,
|
2025-03-23 12:16:15 -04:00
|
|
|
object_kind: ObjectKind::Player(PlayerSubKind::Player),
|
2025-03-18 22:13:28 -04:00
|
|
|
spawn_index: connection.get_free_spawn_index(),
|
|
|
|
look: CUSTOMIZE_DATA,
|
2025-03-23 15:28:53 -04:00
|
|
|
display_flags: DisplayFlag::INVISIBLE
|
|
|
|
| DisplayFlag::HIDE_HEAD
|
|
|
|
| DisplayFlag::UNK,
|
2025-03-18 22:13:28 -04:00
|
|
|
models: [
|
|
|
|
0, // head
|
|
|
|
89, // body
|
|
|
|
89, // hands
|
|
|
|
89, // legs
|
|
|
|
89, // feet
|
|
|
|
0, // ears
|
|
|
|
0, // neck
|
|
|
|
0, // wrists
|
|
|
|
0, // left finger
|
|
|
|
0, // right finger
|
|
|
|
],
|
2025-03-29 00:15:29 -04:00
|
|
|
pos: connection.player_data.position,
|
2025-03-18 22:13:28 -04:00
|
|
|
..Default::default()
|
|
|
|
},
|
|
|
|
..Default::default()
|
|
|
|
}),
|
|
|
|
};
|
|
|
|
|
|
|
|
connection
|
|
|
|
.send_segment(PacketSegment {
|
|
|
|
source_actor: 0x106ad804,
|
2025-03-21 19:56:16 -04:00
|
|
|
target_actor: connection.player_data.actor_id,
|
2025-05-02 00:03:36 -04:00
|
|
|
segment_type: SegmentType::Ipc,
|
|
|
|
data: SegmentData::Ipc { data: ipc },
|
2025-03-18 22:13:28 -04:00
|
|
|
})
|
|
|
|
.await;
|
|
|
|
}
|
2025-03-18 23:30:59 -04:00
|
|
|
|
|
|
|
// zone in
|
|
|
|
{
|
|
|
|
let ipc = ServerZoneIpcSegment {
|
|
|
|
op_code: ServerZoneIpcType::ActorControl,
|
|
|
|
timestamp: timestamp_secs(),
|
|
|
|
data: ServerZoneIpcData::ActorControl(ActorControl {
|
2025-03-23 15:28:53 -04:00
|
|
|
category: ActorControlCategory::ZoneIn {
|
|
|
|
warp_finish_anim: 0x0,
|
|
|
|
raise_anim: 0x0,
|
|
|
|
},
|
2025-03-18 23:30:59 -04:00
|
|
|
}),
|
2025-05-02 23:38:44 -04:00
|
|
|
..Default::default()
|
2025-03-18 23:30:59 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
connection
|
|
|
|
.send_segment(PacketSegment {
|
|
|
|
source_actor: 0x106ad804,
|
2025-03-21 19:56:16 -04:00
|
|
|
target_actor: connection.player_data.actor_id,
|
2025-05-02 00:03:36 -04:00
|
|
|
segment_type: SegmentType::Ipc,
|
|
|
|
data: SegmentData::Ipc { data: ipc },
|
2025-03-18 23:30:59 -04:00
|
|
|
})
|
|
|
|
.await;
|
|
|
|
}
|
2025-03-18 22:13:28 -04:00
|
|
|
}
|
|
|
|
"!spawnnpc" => {
|
2025-04-01 19:15:08 -04:00
|
|
|
let ipc = ServerZoneIpcSegment {
|
|
|
|
op_code: ServerZoneIpcType::NpcSpawn,
|
|
|
|
timestamp: timestamp_secs(),
|
|
|
|
data: ServerZoneIpcData::NpcSpawn(NpcSpawn {
|
|
|
|
common: CommonSpawn {
|
|
|
|
hp_curr: 100,
|
|
|
|
hp_max: 100,
|
|
|
|
mp_curr: 100,
|
|
|
|
mp_max: 100,
|
|
|
|
look: CUSTOMIZE_DATA,
|
|
|
|
spawn_index: connection.get_free_spawn_index(),
|
|
|
|
bnpc_base: 13498,
|
|
|
|
bnpc_name: 10261,
|
|
|
|
object_kind: ObjectKind::BattleNpc(BattleNpcSubKind::Enemy),
|
|
|
|
target_id: ObjectTypeId {
|
|
|
|
object_id: ObjectId(connection.player_data.actor_id),
|
|
|
|
object_type: 0,
|
|
|
|
}, // target the player
|
|
|
|
level: 1,
|
|
|
|
models: [
|
|
|
|
0, // head
|
|
|
|
89, // body
|
|
|
|
89, // hands
|
|
|
|
89, // legs
|
|
|
|
89, // feet
|
|
|
|
0, // ears
|
|
|
|
0, // neck
|
|
|
|
0, // wrists
|
|
|
|
0, // left finger
|
|
|
|
0, // right finger
|
|
|
|
],
|
|
|
|
pos: connection.player_data.position,
|
2025-03-23 10:33:49 -04:00
|
|
|
..Default::default()
|
2025-04-01 19:15:08 -04:00
|
|
|
},
|
|
|
|
..Default::default()
|
|
|
|
}),
|
2025-05-02 23:38:44 -04:00
|
|
|
..Default::default()
|
2025-04-01 19:15:08 -04:00
|
|
|
};
|
2025-03-23 10:33:49 -04:00
|
|
|
|
2025-04-01 19:15:08 -04:00
|
|
|
connection
|
|
|
|
.send_segment(PacketSegment {
|
|
|
|
source_actor: 0x106ad804,
|
|
|
|
target_actor: connection.player_data.actor_id,
|
2025-05-02 00:03:36 -04:00
|
|
|
segment_type: SegmentType::Ipc,
|
|
|
|
data: SegmentData::Ipc { data: ipc },
|
2025-04-01 19:15:08 -04:00
|
|
|
})
|
|
|
|
.await;
|
2025-03-23 10:33:49 -04:00
|
|
|
}
|
|
|
|
"!spawnmonster" => {
|
2025-04-15 16:27:49 -04:00
|
|
|
let spawn_index = connection.get_free_spawn_index();
|
|
|
|
|
2025-03-23 10:33:49 -04:00
|
|
|
// spawn a tiny mandragora
|
|
|
|
{
|
|
|
|
let ipc = ServerZoneIpcSegment {
|
|
|
|
op_code: ServerZoneIpcType::NpcSpawn,
|
|
|
|
timestamp: timestamp_secs(),
|
|
|
|
data: ServerZoneIpcData::NpcSpawn(NpcSpawn {
|
2025-03-23 12:16:15 -04:00
|
|
|
aggression_mode: 1,
|
2025-03-23 10:33:49 -04:00
|
|
|
common: CommonSpawn {
|
|
|
|
hp_curr: 91,
|
|
|
|
hp_max: 91,
|
|
|
|
mp_curr: 100,
|
|
|
|
mp_max: 100,
|
2025-04-15 16:27:49 -04:00
|
|
|
spawn_index,
|
2025-03-23 10:33:49 -04:00
|
|
|
bnpc_base: 13498, // TODO: changing this prevents it from spawning...
|
|
|
|
bnpc_name: 405,
|
2025-03-23 12:16:15 -04:00
|
|
|
object_kind: ObjectKind::BattleNpc(BattleNpcSubKind::Enemy),
|
2025-03-23 10:33:49 -04:00
|
|
|
level: 1,
|
|
|
|
battalion: 4,
|
|
|
|
model_chara: 297,
|
2025-03-29 00:15:29 -04:00
|
|
|
pos: connection.player_data.position,
|
2025-03-18 22:13:28 -04:00
|
|
|
..Default::default()
|
|
|
|
},
|
2025-03-16 14:07:56 -04:00
|
|
|
..Default::default()
|
|
|
|
}),
|
2025-05-02 23:38:44 -04:00
|
|
|
..Default::default()
|
2025-03-16 14:07:56 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
connection
|
|
|
|
.send_segment(PacketSegment {
|
|
|
|
source_actor: 0x106ad804,
|
2025-03-21 19:56:16 -04:00
|
|
|
target_actor: connection.player_data.actor_id,
|
2025-05-02 00:03:36 -04:00
|
|
|
segment_type: SegmentType::Ipc,
|
|
|
|
data: SegmentData::Ipc { data: ipc },
|
2025-03-16 14:07:56 -04:00
|
|
|
})
|
|
|
|
.await;
|
|
|
|
}
|
2025-04-15 16:27:49 -04:00
|
|
|
|
|
|
|
connection.actors.push(Actor {
|
|
|
|
id: ObjectId(0x106ad804),
|
|
|
|
hp: 91,
|
|
|
|
spawn_index: spawn_index as u32,
|
|
|
|
});
|
2025-03-16 14:07:56 -04:00
|
|
|
}
|
2025-03-28 20:19:17 -04:00
|
|
|
"!playscene" => {
|
2025-03-28 22:52:21 -04:00
|
|
|
let parts: Vec<&str> = chat_message.message.split(' ').collect();
|
|
|
|
let event_id = parts[1].parse::<u32>().unwrap();
|
2025-03-28 20:19:17 -04:00
|
|
|
|
|
|
|
// Load the game script for this event on the client
|
|
|
|
{
|
|
|
|
let ipc = ServerZoneIpcSegment {
|
|
|
|
op_code: ServerZoneIpcType::EventStart,
|
|
|
|
timestamp: timestamp_secs(),
|
|
|
|
data: ServerZoneIpcData::EventStart(EventStart {
|
|
|
|
target_id: ObjectTypeId {
|
|
|
|
object_id: ObjectId(connection.player_data.actor_id),
|
|
|
|
object_type: 0,
|
|
|
|
},
|
|
|
|
event_type: 15,
|
2025-03-28 22:52:21 -04:00
|
|
|
event_id,
|
2025-03-28 20:19:17 -04:00
|
|
|
flags: 0,
|
2025-03-28 22:52:21 -04:00
|
|
|
event_arg: 182, // zone?
|
2025-03-28 20:19:17 -04:00
|
|
|
}),
|
2025-05-02 23:38:44 -04:00
|
|
|
..Default::default()
|
2025-03-28 20:19:17 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
connection
|
|
|
|
.send_segment(PacketSegment {
|
|
|
|
source_actor: connection.player_data.actor_id,
|
|
|
|
target_actor: connection.player_data.actor_id,
|
2025-05-02 00:03:36 -04:00
|
|
|
segment_type: SegmentType::Ipc,
|
|
|
|
data: SegmentData::Ipc { data: ipc },
|
2025-03-28 20:19:17 -04:00
|
|
|
})
|
|
|
|
.await;
|
|
|
|
}
|
|
|
|
|
|
|
|
// set our status icon to viewing cutscene
|
|
|
|
{
|
|
|
|
let ipc = ServerZoneIpcSegment {
|
|
|
|
op_code: ServerZoneIpcType::ActorControl,
|
|
|
|
timestamp: timestamp_secs(),
|
|
|
|
data: ServerZoneIpcData::ActorControl(ActorControl {
|
|
|
|
category: ActorControlCategory::SetStatusIcon {
|
|
|
|
icon: OnlineStatus::ViewingCutscene,
|
|
|
|
},
|
|
|
|
}),
|
2025-05-02 23:38:44 -04:00
|
|
|
..Default::default()
|
2025-03-28 20:19:17 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
connection
|
|
|
|
.send_segment(PacketSegment {
|
|
|
|
source_actor: connection.player_data.actor_id,
|
|
|
|
target_actor: connection.player_data.actor_id,
|
2025-05-02 00:03:36 -04:00
|
|
|
segment_type: SegmentType::Ipc,
|
|
|
|
data: SegmentData::Ipc { data: ipc },
|
2025-03-28 20:19:17 -04:00
|
|
|
})
|
|
|
|
.await;
|
|
|
|
}
|
|
|
|
|
2025-03-28 22:52:21 -04:00
|
|
|
let event = match event_id {
|
2025-05-05 23:30:36 -04:00
|
|
|
1245185 => Event::new(1245185, "opening/OpeningLimsaLominsa.lua"),
|
|
|
|
1245186 => Event::new(1245186, "opening/OpeningGridania.lua"),
|
|
|
|
1245187 => Event::new(1245187, "opening/OpeningUldah.lua"),
|
2025-03-28 22:52:21 -04:00
|
|
|
_ => panic!("Unsupported event!"),
|
|
|
|
};
|
|
|
|
|
|
|
|
connection.event = Some(event);
|
2025-03-28 21:28:30 -04:00
|
|
|
connection
|
|
|
|
.event
|
|
|
|
.as_mut()
|
|
|
|
.unwrap()
|
2025-03-28 22:34:34 -04:00
|
|
|
.enter_territory(lua_player, connection.zone.as_ref().unwrap());
|
2025-03-28 20:19:17 -04:00
|
|
|
}
|
2025-04-01 19:15:08 -04:00
|
|
|
"!spawnclone" => {
|
|
|
|
// spawn another one of us
|
2025-04-01 21:37:41 -04:00
|
|
|
let player = &connection.player_data;
|
2025-04-01 19:15:08 -04:00
|
|
|
|
|
|
|
let mut common = connection
|
|
|
|
.get_player_common_spawn(Some(player.position), Some(player.rotation));
|
|
|
|
common.spawn_index = connection.get_free_spawn_index();
|
|
|
|
|
|
|
|
let ipc = ServerZoneIpcSegment {
|
|
|
|
op_code: ServerZoneIpcType::NpcSpawn,
|
|
|
|
timestamp: timestamp_secs(),
|
|
|
|
data: ServerZoneIpcData::NpcSpawn(NpcSpawn {
|
|
|
|
common,
|
|
|
|
..Default::default()
|
|
|
|
}),
|
2025-05-02 23:38:44 -04:00
|
|
|
..Default::default()
|
2025-04-01 19:15:08 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
connection
|
|
|
|
.send_segment(PacketSegment {
|
|
|
|
source_actor: 0x106ad804,
|
|
|
|
target_actor: connection.player_data.actor_id,
|
2025-05-02 00:03:36 -04:00
|
|
|
segment_type: SegmentType::Ipc,
|
|
|
|
data: SegmentData::Ipc { data: ipc },
|
2025-04-01 19:15:08 -04:00
|
|
|
})
|
|
|
|
.await;
|
|
|
|
}
|
2025-04-01 21:53:10 -04:00
|
|
|
"!classjob" => {
|
|
|
|
let parts: Vec<&str> = chat_message.message.split(' ').collect();
|
|
|
|
|
|
|
|
connection.player_data.classjob_id = parts[1].parse::<u8>().unwrap();
|
|
|
|
connection.update_class_info().await;
|
|
|
|
}
|
2025-03-15 20:36:39 -04:00
|
|
|
_ => tracing::info!("Unrecognized debug command!"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|