mirror of
https://github.com/redstrate/Kawari.git
synced 2025-06-30 11:47:45 +00:00
I figured out a few of these while figuring out the crystal bell event, although their purpose is still generally unknown. It doesn't really affect anything functionally as the event still works as well as it did before. I also fixed the crystal bell scenes so they don't finish the event prematurely, although it looks weird without the hairstyle menu. But that still isn't figured out yet.
1123 lines
70 KiB
Rust
1123 lines
70 KiB
Rust
use std::net::SocketAddr;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::{Duration, Instant};
|
|
|
|
use kawari::RECEIVE_BUFFER_SIZE;
|
|
use kawari::common::Position;
|
|
use kawari::common::{GameData, TerritoryNameKind, timestamp_secs};
|
|
use kawari::config::get_config;
|
|
use kawari::inventory::{Item, Storage};
|
|
use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment};
|
|
use kawari::ipc::zone::{
|
|
ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList,
|
|
};
|
|
use kawari::ipc::zone::{
|
|
ClientTriggerCommand, ClientZoneIpcData, CommonSpawn, EventStart, GameMasterCommandType,
|
|
GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
|
|
};
|
|
use kawari::opcodes::{ServerChatIpcType, ServerZoneIpcType};
|
|
use kawari::packet::oodle::OodleNetwork;
|
|
use kawari::packet::{
|
|
ConnectionType, PacketSegment, PacketState, SegmentData, SegmentType, send_keep_alive,
|
|
};
|
|
use kawari::world::{ChatHandler, ExtraLuaState, Zone, ZoneConnection, load_init_script};
|
|
use kawari::world::{
|
|
ClientHandle, Event, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer,
|
|
WorldDatabase, handle_custom_ipc, server_main_loop,
|
|
};
|
|
|
|
use mlua::{Function, Lua};
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::join;
|
|
use tokio::net::{TcpListener, TcpStream};
|
|
use tokio::sync::mpsc::{Receiver, UnboundedReceiver, UnboundedSender, channel, unbounded_channel};
|
|
use tokio::sync::oneshot;
|
|
use tokio::task::JoinHandle;
|
|
|
|
fn spawn_main_loop() -> (ServerHandle, JoinHandle<()>) {
|
|
let (send, recv) = channel(64);
|
|
|
|
let handle = ServerHandle {
|
|
chan: send,
|
|
next_id: Default::default(),
|
|
};
|
|
|
|
let join = tokio::spawn(async move {
|
|
let res = server_main_loop(recv).await;
|
|
match res {
|
|
Ok(()) => {}
|
|
Err(err) => {
|
|
tracing::error!("{}", err);
|
|
}
|
|
}
|
|
});
|
|
|
|
(handle, join)
|
|
}
|
|
|
|
struct ClientData {
|
|
/// Socket for data recieved from the global server
|
|
recv: Receiver<FromServer>,
|
|
connection: ZoneConnection,
|
|
}
|
|
|
|
/// Spawn a new client actor.
|
|
pub fn spawn_client(connection: ZoneConnection) {
|
|
let (send, recv) = channel(64);
|
|
|
|
let id = &connection.id.clone();
|
|
let ip = &connection.ip.clone();
|
|
|
|
let data = ClientData { recv, connection };
|
|
|
|
// Spawn a new client task
|
|
let (my_send, my_recv) = oneshot::channel();
|
|
let _kill = tokio::spawn(start_client(my_recv, data));
|
|
|
|
// Send client information to said task
|
|
let handle = ClientHandle {
|
|
id: *id,
|
|
ip: *ip,
|
|
channel: send,
|
|
actor_id: 0,
|
|
common: CommonSpawn::default(),
|
|
};
|
|
let _ = my_send.send(handle);
|
|
}
|
|
|
|
async fn start_client(my_handle: oneshot::Receiver<ClientHandle>, data: ClientData) {
|
|
// Recieve client information from global
|
|
let my_handle = match my_handle.await {
|
|
Ok(my_handle) => my_handle,
|
|
Err(_) => return,
|
|
};
|
|
|
|
let connection = data.connection;
|
|
let recv = data.recv;
|
|
|
|
// communication channel between client_loop and client_server_loop
|
|
let (internal_send, internal_recv) = unbounded_channel();
|
|
|
|
let _ = join!(
|
|
tokio::spawn(client_loop(connection, internal_recv, my_handle)),
|
|
tokio::spawn(client_server_loop(recv, internal_send))
|
|
);
|
|
}
|
|
|
|
async fn client_server_loop(
|
|
mut data: Receiver<FromServer>,
|
|
internal_send: UnboundedSender<FromServer>,
|
|
) {
|
|
while let Some(msg) = data.recv().await {
|
|
internal_send.send(msg).unwrap()
|
|
}
|
|
}
|
|
|
|
async fn client_loop(
|
|
mut connection: ZoneConnection,
|
|
mut internal_recv: UnboundedReceiver<FromServer>,
|
|
client_handle: ClientHandle,
|
|
) {
|
|
let database = connection.database.clone();
|
|
let game_data = connection.gamedata.clone();
|
|
let lua = connection.lua.clone();
|
|
let config = get_config();
|
|
|
|
let mut lua_player = LuaPlayer::default();
|
|
|
|
// TODO: this is terrible, just have a separate zone/chat connection
|
|
let mut is_zone_connection = false;
|
|
|
|
let mut buf = vec![0; RECEIVE_BUFFER_SIZE];
|
|
loop {
|
|
tokio::select! {
|
|
biased; // client data should always be prioritized
|
|
n = connection.socket.read(&mut buf) => {
|
|
match n {
|
|
Ok(n) => {
|
|
// if the last response was over >5 seconds, the client is probably gone
|
|
if n == 0 {
|
|
let now = Instant::now();
|
|
if now.duration_since(connection.last_keep_alive) > Duration::from_secs(5) {
|
|
tracing::info!("Connection {:#?} was killed because of timeout", client_handle.id);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if n > 0 {
|
|
connection.last_keep_alive = Instant::now();
|
|
|
|
let (segments, connection_type) = connection.parse_packet(&buf[..n]).await;
|
|
for segment in &segments {
|
|
match &segment.data {
|
|
SegmentData::None() => {},
|
|
SegmentData::Setup { ticket } => {
|
|
// for some reason they send a string representation
|
|
let actor_id = ticket.parse::<u32>().unwrap();
|
|
|
|
// initialize player data if it doesn't exist'
|
|
if connection.player_data.actor_id == 0 {
|
|
connection.player_data = database.find_player_data(actor_id);
|
|
}
|
|
|
|
if connection_type == ConnectionType::Zone {
|
|
is_zone_connection = true;
|
|
|
|
// collect actor data
|
|
connection.initialize(actor_id).await;
|
|
|
|
connection.exit_position = Some(connection.player_data.position);
|
|
connection.exit_rotation = Some(connection.player_data.rotation);
|
|
|
|
let mut client_handle = client_handle.clone();
|
|
client_handle.actor_id = actor_id;
|
|
client_handle.common = connection.get_player_common_spawn(connection.exit_position, connection.exit_rotation);
|
|
|
|
// tell the server we exist, now that we confirmed we are a legitimate connection
|
|
connection.handle.send(ToServer::NewClient(client_handle)).await;
|
|
} else if connection_type == ConnectionType::Chat {
|
|
// We have send THEM a keep alive
|
|
connection.send_chat_segment(PacketSegment {
|
|
segment_type: SegmentType::KeepAliveRequest,
|
|
data: SegmentData::KeepAliveRequest {
|
|
id: 0xE0037603u32,
|
|
timestamp: timestamp_secs(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.await;
|
|
|
|
// initialize connection
|
|
connection.send_chat_segment(PacketSegment {
|
|
segment_type: SegmentType::Initialize,
|
|
data: SegmentData::Initialize {
|
|
player_id: connection.player_data.actor_id,
|
|
timestamp: timestamp_secs(),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.await;
|
|
|
|
// we need the actor id at this point!
|
|
assert!(connection.player_data.actor_id != 0);
|
|
|
|
// send login reply
|
|
{
|
|
let ipc = ServerChatIpcSegment {
|
|
op_code: ServerChatIpcType::LoginReply,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerChatIpcData::LoginReply {
|
|
timestamp: 0,
|
|
sid: 0,
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
connection.send_chat_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
SegmentData::Ipc { data } => {
|
|
match &data.data {
|
|
ClientZoneIpcData::InitRequest { .. } => {
|
|
tracing::info!(
|
|
"Client is now requesting zone information. Sending!"
|
|
);
|
|
|
|
// IPC Init(?)
|
|
{
|
|
let ipc = ServerZoneIpcSegment {
|
|
op_code: ServerZoneIpcType::InitResponse,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerZoneIpcData::InitResponse {
|
|
unk1: 0,
|
|
character_id: connection.player_data.actor_id,
|
|
unk2: 0,
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
connection
|
|
.send_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
|
|
let chara_details =
|
|
database.find_chara_make(connection.player_data.content_id);
|
|
|
|
// Send inventory
|
|
connection.send_inventory(false).await;
|
|
|
|
// set chara gear param
|
|
connection
|
|
.actor_control_self(ActorControlSelf {
|
|
category: ActorControlCategory::SetCharaGearParamUI {
|
|
unk1: 1,
|
|
unk2: 1,
|
|
},
|
|
})
|
|
.await;
|
|
|
|
// Stats
|
|
connection.send_stats(&chara_details).await;
|
|
|
|
// Player Setup
|
|
{
|
|
let ipc = ServerZoneIpcSegment {
|
|
op_code: ServerZoneIpcType::PlayerStatus,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerZoneIpcData::PlayerStatus(PlayerStatus {
|
|
content_id: connection.player_data.content_id,
|
|
exp: connection.player_data.classjob_exp,
|
|
name: chara_details.name,
|
|
char_id: connection.player_data.actor_id,
|
|
race: chara_details.chara_make.customize.race,
|
|
gender: chara_details.chara_make.customize.gender,
|
|
tribe: chara_details.chara_make.customize.subrace,
|
|
city_state: chara_details.city_state,
|
|
nameday_month: chara_details.chara_make.birth_month
|
|
as u8,
|
|
nameday_day: chara_details.chara_make.birth_day as u8,
|
|
deity: chara_details.chara_make.guardian as u8,
|
|
current_class: connection.player_data.classjob_id,
|
|
current_job: connection.player_data.classjob_id,
|
|
levels: connection.player_data.classjob_levels.map(|x| x as u16),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
connection
|
|
.send_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
|
|
let zone_id = connection.player_data.zone_id;
|
|
connection.change_zone(zone_id).await;
|
|
|
|
let lua = lua.lock().unwrap();
|
|
lua.scope(|scope| {
|
|
let connection_data =
|
|
scope.create_userdata_ref_mut(&mut lua_player).unwrap();
|
|
|
|
let func: Function = lua.globals().get("onBeginLogin").unwrap();
|
|
|
|
func.call::<()>(connection_data).unwrap();
|
|
|
|
Ok(())
|
|
})
|
|
.unwrap();
|
|
}
|
|
ClientZoneIpcData::FinishLoading { .. } => {
|
|
// tell the server we loaded into the zone, so it can start sending us acors
|
|
connection.handle.send(ToServer::ZoneLoaded(connection.id, connection.zone.as_ref().unwrap().id)).await;
|
|
|
|
let common = connection.get_player_common_spawn(connection.exit_position, connection.exit_rotation);
|
|
|
|
let chara_details = database.find_chara_make(connection.player_data.content_id);
|
|
|
|
connection.send_inventory(false).await;
|
|
connection.send_stats(&chara_details).await;
|
|
|
|
let online_status = if connection.player_data.gm_rank == GameMasterRank::NormalUser {
|
|
OnlineStatus::Online
|
|
} else {
|
|
OnlineStatus::GameMasterBlue
|
|
};
|
|
|
|
// send player spawn
|
|
{
|
|
let ipc = ServerZoneIpcSegment {
|
|
op_code: ServerZoneIpcType::PlayerSpawn,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn {
|
|
account_id: connection.player_data.account_id,
|
|
content_id: connection.player_data.content_id,
|
|
current_world_id: config.world.world_id,
|
|
home_world_id: config.world.world_id,
|
|
gm_rank: connection.player_data.gm_rank,
|
|
online_status,
|
|
common: common.clone(),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
connection
|
|
.send_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
|
|
// fade in?
|
|
{
|
|
let ipc = ServerZoneIpcSegment {
|
|
op_code: ServerZoneIpcType::PrepareZoning,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerZoneIpcData::PrepareZoning {
|
|
unk: [0, 0, 0, 0],
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
connection
|
|
.send_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
|
|
// wipe any exit position so it isn't accidentally reused
|
|
connection.exit_position = None;
|
|
connection.exit_rotation = None;
|
|
}
|
|
ClientZoneIpcData::ClientTrigger(trigger) => {
|
|
// store the query for scripts
|
|
if let ClientTriggerCommand::TeleportQuery { aetheryte_id } = trigger.trigger {
|
|
connection.player_data.teleport_query.aetheryte_id = aetheryte_id as u16;
|
|
}
|
|
|
|
// inform the server of our trigger, it will handle sending it to other clients
|
|
connection.handle.send(ToServer::ClientTrigger(connection.id, connection.player_data.actor_id, trigger.clone())).await;
|
|
}
|
|
ClientZoneIpcData::Unk2 { .. } => {
|
|
// no-op
|
|
}
|
|
ClientZoneIpcData::Unk3 { .. } => {
|
|
// no-op
|
|
}
|
|
ClientZoneIpcData::Unk4 { .. } => {
|
|
// no-op
|
|
}
|
|
ClientZoneIpcData::SetSearchInfoHandler { .. } => {
|
|
tracing::info!("Recieved SetSearchInfoHandler!");
|
|
}
|
|
ClientZoneIpcData::Unk5 { .. } => {
|
|
// no-op
|
|
}
|
|
ClientZoneIpcData::SocialListRequest(request) => {
|
|
tracing::info!("Recieved social list request!");
|
|
|
|
match &request.request_type {
|
|
SocialListRequestType::Party => {
|
|
let ipc = ServerZoneIpcSegment {
|
|
op_code: ServerZoneIpcType::SocialList,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerZoneIpcData::SocialList(SocialList {
|
|
request_type: request.request_type,
|
|
sequence: request.count,
|
|
entries: vec![PlayerEntry {
|
|
// TODO: fill with actual player data, it also shows up wrong in game
|
|
content_id: connection.player_data.content_id,
|
|
zone_id: connection.zone.as_ref().unwrap().id,
|
|
zone_id1: 0x0100,
|
|
class_job: 36,
|
|
level: 100,
|
|
one: 1,
|
|
name: "INVALID".to_string(),
|
|
..Default::default()
|
|
}],
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
connection
|
|
.send_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
SocialListRequestType::Friends => {
|
|
let ipc = ServerZoneIpcSegment {
|
|
op_code: ServerZoneIpcType::SocialList,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerZoneIpcData::SocialList(SocialList {
|
|
request_type: request.request_type,
|
|
sequence: request.count,
|
|
entries: Default::default(),
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
connection
|
|
.send_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
ClientZoneIpcData::UpdatePositionHandler { position, rotation } => {
|
|
connection.player_data.rotation = *rotation;
|
|
connection.player_data.position = *position;
|
|
|
|
connection.handle.send(ToServer::ActorMoved(connection.id, connection.player_data.actor_id, *position, *rotation)).await;
|
|
}
|
|
ClientZoneIpcData::LogOut { .. } => {
|
|
tracing::info!("Recieved log out from client!");
|
|
|
|
connection.begin_log_out().await;
|
|
}
|
|
ClientZoneIpcData::Disconnected { .. } => {
|
|
tracing::info!("Client disconnected!");
|
|
|
|
connection.handle.send(ToServer::Disconnected(connection.id)).await;
|
|
|
|
break;
|
|
}
|
|
ClientZoneIpcData::ChatMessage(chat_message) => {
|
|
connection.handle.send(ToServer::Message(connection.id, chat_message.message.clone())).await;
|
|
|
|
let mut handled = false;
|
|
let command_trigger: char = '!';
|
|
if chat_message.message.starts_with(command_trigger)
|
|
{
|
|
let parts: Vec<&str> = chat_message.message.split(' ').collect();
|
|
let command_name = &parts[0][1..];
|
|
|
|
{
|
|
let lua = lua.lock().unwrap();
|
|
let state = lua.app_data_ref::<ExtraLuaState>().unwrap();
|
|
|
|
// If a Lua command exists, try using that first
|
|
if let Some(command_script) =
|
|
state.command_scripts.get(command_name)
|
|
{
|
|
handled = true;
|
|
|
|
let file_name = format!(
|
|
"{}/{}",
|
|
&config.world.scripts_location, command_script
|
|
);
|
|
|
|
let mut run_script = || -> mlua::Result<()> {
|
|
lua.scope(|scope| {
|
|
let connection_data = scope
|
|
.create_userdata_ref_mut(&mut lua_player)?;
|
|
|
|
lua.load(
|
|
std::fs::read(&file_name)
|
|
.expect(format!("Failed to load script file {}!", &file_name).as_str()),
|
|
)
|
|
.set_name("@".to_string() + &file_name)
|
|
.exec()?;
|
|
|
|
let required_rank = lua.globals().get("required_rank");
|
|
if let Err(error) = required_rank {
|
|
tracing::info!("Script is missing required_rank! Unable to run command, sending error to user. Additional information: {}", error);
|
|
let func: Function =
|
|
lua.globals().get("onCommandRequiredRankMissingError")?;
|
|
func.call::<()>((error.to_string(), connection_data))?;
|
|
return Ok(());
|
|
}
|
|
|
|
/* Reset state for future commands. Without this it'll stay set to the last value
|
|
* and allow other commands that omit required_rank to run, which is undesirable. */
|
|
lua.globals().set("required_rank", mlua::Value::Nil)?;
|
|
|
|
if connection.player_data.gm_rank as u8 >= required_rank? {
|
|
let mut func_args = "";
|
|
if parts.len() > 1 {
|
|
func_args = &chat_message.message[command_name.len() + 2..];
|
|
tracing::info!("Args passed to Lua command {}: {}", command_name, func_args);
|
|
} else {
|
|
tracing::info!("No additional args passed to Lua command {}.", command_name);
|
|
}
|
|
let func: Function =
|
|
lua.globals().get("onCommand")?;
|
|
func.call::<()>((func_args, connection_data))?;
|
|
Ok(())
|
|
} else {
|
|
tracing::info!("User with account_id {} tried to invoke GM command {} with insufficient privileges!",
|
|
connection.player_data.account_id, command_name);
|
|
let func: Function =
|
|
lua.globals().get("onCommandRequiredRankInsufficientError")?;
|
|
func.call::<()>(connection_data)?;
|
|
Ok(())
|
|
}
|
|
})
|
|
};
|
|
|
|
if let Err(err) = run_script() {
|
|
tracing::warn!("Lua error in {file_name}: {:?}", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to Rust implemented commands
|
|
if !handled {
|
|
handled = ChatHandler::handle_chat_message(
|
|
&mut connection,
|
|
chat_message,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
// If it's truly not existent:
|
|
if !handled {
|
|
tracing::info!("Unknown command {command_name}");
|
|
|
|
let lua = lua.lock().unwrap();
|
|
|
|
let mut call_func = || {
|
|
lua.scope(|scope| {
|
|
let connection_data = scope
|
|
.create_userdata_ref_mut(&mut lua_player)?;
|
|
let func: Function =
|
|
lua.globals().get("onUnknownCommandError")?;
|
|
func.call::<()>((command_name, connection_data))?;
|
|
Ok(())
|
|
})
|
|
};
|
|
|
|
if let Err(err) = call_func() {
|
|
tracing::warn!("Lua error in Global.lua: {:?}", err);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ClientZoneIpcData::GMCommand { command, arg0, arg1, .. } => {
|
|
tracing::info!("Got a game master command!");
|
|
|
|
match &command {
|
|
GameMasterCommandType::SetLevel => {
|
|
connection.player_data.set_current_level(*arg0 as i32);
|
|
connection.update_class_info().await;
|
|
}
|
|
GameMasterCommandType::ChangeWeather => {
|
|
connection.change_weather(*arg0 as u16).await
|
|
}
|
|
GameMasterCommandType::Speed => {
|
|
// TODO: Maybe allow setting the speed of a targeted player too?
|
|
connection
|
|
.actor_control_self(ActorControlSelf {
|
|
category:
|
|
ActorControlCategory::Flee {
|
|
speed: *arg0 as u16,
|
|
},
|
|
})
|
|
.await
|
|
}
|
|
GameMasterCommandType::ChangeTerritory => {
|
|
connection.change_zone(*arg0 as u16).await
|
|
}
|
|
GameMasterCommandType::ToggleInvisibility => {
|
|
connection.player_data.gm_invisible = !connection.player_data.gm_invisible;
|
|
connection
|
|
.actor_control_self(ActorControlSelf {
|
|
category:
|
|
ActorControlCategory::ToggleInvisibility {
|
|
invisible: connection.player_data.gm_invisible,
|
|
},
|
|
})
|
|
.await
|
|
}
|
|
GameMasterCommandType::ToggleWireframe => connection
|
|
.actor_control_self(ActorControlSelf {
|
|
category:
|
|
ActorControlCategory::ToggleWireframeRendering(),
|
|
})
|
|
.await,
|
|
GameMasterCommandType::GiveItem => {
|
|
connection.player_data.inventory.add_in_next_free_slot(Item { id: *arg0, quantity: 1 });
|
|
connection.send_inventory(false).await;
|
|
}
|
|
GameMasterCommandType::Orchestrion => {
|
|
let on = *arg0 == 1; // This command uses 1 for on, 2 for off.
|
|
let id = *arg1 as u16;
|
|
|
|
// id == 0 means "all"
|
|
if id == 0 {
|
|
/* Currently 792 songs ingame.
|
|
* Commented out because this learns literally zero songs
|
|
* for some unknown reason. */
|
|
/*for i in 1..793 {
|
|
let idd = i as u16;
|
|
connection.send_message("test!").await;
|
|
connection.actor_control_self(ActorControlSelf {
|
|
category: ActorControlCategory::ToggleOrchestrionUnlock { song_id: id, unlocked: on } }).await;
|
|
}*/
|
|
} else {
|
|
connection.actor_control_self(ActorControlSelf {
|
|
category: ActorControlCategory::ToggleOrchestrionUnlock { song_id: id, unlocked: on }
|
|
}).await;
|
|
}
|
|
}
|
|
GameMasterCommandType::Aetheryte => {
|
|
let on = *arg0 == 0;
|
|
let id = *arg1;
|
|
|
|
// id == 0 means "all"
|
|
if id == 0 {
|
|
for i in 1..239 {
|
|
connection.actor_control_self(ActorControlSelf {
|
|
category: ActorControlCategory::LearnTeleport { id: i, unlocked: on } }).await;
|
|
}
|
|
} else {
|
|
connection.actor_control_self(ActorControlSelf {
|
|
category: ActorControlCategory::LearnTeleport { id, unlocked: on } }).await;
|
|
}
|
|
}
|
|
GameMasterCommandType::EXP => {
|
|
let amount = *arg0;
|
|
connection.player_data.set_current_exp(connection.player_data.current_exp() + amount);
|
|
connection.update_class_info().await;
|
|
}
|
|
|
|
GameMasterCommandType::TerritoryInfo => {
|
|
let id: u32 = connection.zone.as_ref().unwrap().id.into();
|
|
let weather_id;
|
|
let internal_name;
|
|
let place_name;
|
|
let region_name;
|
|
{
|
|
let mut game_data = connection.gamedata.lock().unwrap();
|
|
// TODO: Maybe the current weather should be cached somewhere like the zone id?
|
|
weather_id = game_data
|
|
.get_weather(id)
|
|
.unwrap_or(1) as u16;
|
|
let fallback = "<Unable to load name!>";
|
|
internal_name = game_data.get_territory_name(id, TerritoryNameKind::Internal).unwrap_or(fallback.to_string());
|
|
region_name = game_data.get_territory_name(id, TerritoryNameKind::Region).unwrap_or(fallback.to_string());
|
|
place_name = game_data.get_territory_name(id, TerritoryNameKind::Place).unwrap_or(fallback.to_string());
|
|
}
|
|
|
|
connection.send_message(format!(concat!("Territory Info for zone {}:\n",
|
|
"Current weather: {}\n",
|
|
"Internal name: {}\n",
|
|
"Region name: {}\n",
|
|
"Place name: {}"), id, weather_id, internal_name, region_name, place_name).as_str()).await;
|
|
},
|
|
GameMasterCommandType::Gil => {
|
|
let amount = *arg0;
|
|
connection.player_data.inventory.currency.get_slot_mut(0).quantity += amount;
|
|
connection.send_inventory(false).await;
|
|
},
|
|
}
|
|
}
|
|
ClientZoneIpcData::ZoneJump {
|
|
exit_box,
|
|
position,
|
|
..
|
|
} => {
|
|
tracing::info!(
|
|
"Character entered {exit_box} with a position of {position:#?}!"
|
|
);
|
|
|
|
// find the exit box id
|
|
let new_territory;
|
|
{
|
|
let (_, exit_box) = connection
|
|
.zone
|
|
.as_ref()
|
|
.unwrap()
|
|
.find_exit_box(*exit_box)
|
|
.unwrap();
|
|
|
|
// find the pop range on the other side
|
|
let mut game_data = game_data.lock().unwrap();
|
|
let new_zone = Zone::load(&mut game_data.game_data, exit_box.territory_type);
|
|
let (destination_object, _) = new_zone
|
|
.find_pop_range(exit_box.destination_instance_id)
|
|
.unwrap();
|
|
|
|
// set the exit position
|
|
connection.exit_position = Some(Position {
|
|
x: destination_object.transform.translation[0],
|
|
y: destination_object.transform.translation[1],
|
|
z: destination_object.transform.translation[2],
|
|
});
|
|
new_territory = exit_box.territory_type;
|
|
}
|
|
|
|
connection.change_zone(new_territory).await;
|
|
}
|
|
ClientZoneIpcData::ActionRequest(request) => {
|
|
connection
|
|
.handle
|
|
.send(ToServer::ActionRequest(
|
|
connection.id,
|
|
connection.player_data.actor_id,
|
|
request.clone(),
|
|
))
|
|
.await;
|
|
}
|
|
ClientZoneIpcData::Unk16 { .. } => {
|
|
// no-op
|
|
}
|
|
ClientZoneIpcData::Unk17 { unk1, .. } => {
|
|
// this is *usually* sent in response, but not always
|
|
let ipc = ServerZoneIpcSegment {
|
|
op_code: ServerZoneIpcType::UnkCall,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerZoneIpcData::UnkCall {
|
|
unk1: *unk1, // copied from here
|
|
unk2: 333, // always this for some reason
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
connection
|
|
.send_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
ClientZoneIpcData::Unk18 { .. } => {
|
|
// no-op
|
|
}
|
|
ClientZoneIpcData::EventRelatedUnk {
|
|
unk1,
|
|
unk2,
|
|
unk3,
|
|
unk4,
|
|
} => {
|
|
tracing::info!(
|
|
"Recieved EventRelatedUnk! {unk1} {unk2} {unk3} {unk4}"
|
|
);
|
|
|
|
if let Some(event) = connection.event.as_mut() {
|
|
event.scene_finished(&mut lua_player, *unk2);
|
|
}
|
|
}
|
|
ClientZoneIpcData::Unk19 { .. } => {
|
|
// no-op
|
|
}
|
|
ClientZoneIpcData::ItemOperation(action) => {
|
|
tracing::info!("Client is modifying inventory! {action:#?}");
|
|
|
|
connection.player_data.inventory.process_action(action);
|
|
connection.send_inventory(true).await;
|
|
}
|
|
ClientZoneIpcData::StartTalkEvent { actor_id, event_id } => {
|
|
// load event
|
|
{
|
|
let ipc = ServerZoneIpcSegment {
|
|
op_code: ServerZoneIpcType::EventStart,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerZoneIpcData::EventStart(EventStart {
|
|
target_id: *actor_id,
|
|
event_id: *event_id,
|
|
event_type: 1, // talk?
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
};
|
|
|
|
connection
|
|
.send_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
|
|
let mut should_cancel = false;
|
|
{
|
|
let lua = lua.lock().unwrap();
|
|
let state = lua.app_data_ref::<ExtraLuaState>().unwrap();
|
|
|
|
if let Some(event_script) =
|
|
state.event_scripts.get(event_id)
|
|
{
|
|
connection.event = Some(Event::new(*event_id, &event_script));
|
|
connection
|
|
.event
|
|
.as_mut()
|
|
.unwrap()
|
|
.talk(*actor_id, &mut lua_player);
|
|
} else {
|
|
tracing::warn!("Event {event_id} isn't scripted yet! Ignoring...");
|
|
|
|
should_cancel = true;
|
|
}
|
|
}
|
|
|
|
if should_cancel {
|
|
// give control back to the player so they aren't stuck
|
|
connection.event_finish(*event_id).await;
|
|
connection.send_message(&*format!("Event {event_id} tried to start, but it doesn't have a script associated with it!")).await;
|
|
}
|
|
}
|
|
ClientZoneIpcData::EventHandlerReturn { handler_id, scene, error_code, num_results, params } => {
|
|
tracing::info!("Finishing this event... {handler_id} {error_code} {scene} {params:#?}");
|
|
|
|
connection
|
|
.event
|
|
.as_mut()
|
|
.unwrap()
|
|
.finish(*scene, ¶ms[..*num_results as usize], &mut lua_player);
|
|
}
|
|
ClientZoneIpcData::Config(config) => {
|
|
connection
|
|
.handle
|
|
.send(ToServer::Config(
|
|
connection.id,
|
|
connection.player_data.actor_id,
|
|
config.clone(),
|
|
))
|
|
.await;
|
|
}
|
|
ClientZoneIpcData::EventUnkRequest { event_id, unk1, unk2, unk3 } => {
|
|
let ipc = ServerZoneIpcSegment {
|
|
op_code: ServerZoneIpcType::EventUnkReply,
|
|
timestamp: timestamp_secs(),
|
|
data: ServerZoneIpcData::EventUnkReply {
|
|
event_id: *event_id,
|
|
unk1: *unk1,
|
|
unk2: *unk2,
|
|
unk3: *unk3 + 1,
|
|
},
|
|
..Default::default()
|
|
};
|
|
|
|
connection
|
|
.send_segment(PacketSegment {
|
|
source_actor: connection.player_data.actor_id,
|
|
target_actor: connection.player_data.actor_id,
|
|
segment_type: SegmentType::Ipc,
|
|
data: SegmentData::Ipc { data: ipc },
|
|
})
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
SegmentData::KeepAliveRequest { id, timestamp } => {
|
|
send_keep_alive::<ServerZoneIpcSegment>(
|
|
&mut connection.socket,
|
|
&mut connection.state,
|
|
ConnectionType::Zone,
|
|
*id,
|
|
*timestamp,
|
|
)
|
|
.await
|
|
}
|
|
SegmentData::KeepAliveResponse { .. } => {
|
|
tracing::info!("Got keep alive response from client... cool...");
|
|
}
|
|
SegmentData::KawariIpc { data } => handle_custom_ipc(&mut connection, data).await,
|
|
_ => {
|
|
panic!("The server is recieving a response or unknown packet: {segment:#?}")
|
|
}
|
|
}
|
|
}
|
|
|
|
// copy from lua player state, as they modify the status effects list
|
|
// TODO: i dunno?
|
|
connection.status_effects = lua_player.status_effects.clone();
|
|
|
|
// Process any queued packets from scripts and whatnot
|
|
connection.process_lua_player(&mut lua_player).await;
|
|
|
|
// check if status effects need sending
|
|
connection.process_effects_list().await;
|
|
|
|
// update lua player
|
|
lua_player.player_data = connection.player_data.clone();
|
|
lua_player.status_effects = connection.status_effects.clone();
|
|
}
|
|
},
|
|
Err(_) => {
|
|
tracing::info!("Connection {:#?} was killed because of a network error!", client_handle.id);
|
|
break;
|
|
},
|
|
}
|
|
}
|
|
msg = internal_recv.recv() => match msg {
|
|
Some(msg) => match msg {
|
|
FromServer::Message(msg) => connection.send_message(&msg).await,
|
|
FromServer::ActorSpawn(actor, spawn) => connection.spawn_actor(actor, spawn).await,
|
|
FromServer::ActorMove(actor_id, position, rotation) => connection.set_actor_position(actor_id, position, rotation).await,
|
|
FromServer::ActorDespawn(actor_id) => connection.remove_actor(actor_id).await,
|
|
FromServer::ActorControl(actor_id, actor_control) => connection.actor_control(actor_id, actor_control).await,
|
|
FromServer::ActorControlTarget(actor_id, actor_control) => connection.actor_control_target(actor_id, actor_control).await,
|
|
FromServer::ActorControlSelf(actor_control) => connection.actor_control_self(actor_control).await,
|
|
FromServer::ActionComplete(request) => connection.execute_action(request, &mut lua_player).await,
|
|
FromServer::ActionCancelled() => connection.cancel_action().await,
|
|
FromServer::UpdateConfig(actor_id, config) => connection.update_config(actor_id, config).await,
|
|
},
|
|
None => break,
|
|
}
|
|
}
|
|
}
|
|
|
|
// forcefully log out the player if they weren't logging out but force D/C'd
|
|
if connection.player_data.actor_id != 0
|
|
&& !connection.gracefully_logged_out
|
|
&& is_zone_connection
|
|
{
|
|
tracing::info!(
|
|
"Forcefully logging out connection {:#?}...",
|
|
client_handle.id
|
|
);
|
|
connection.begin_log_out().await;
|
|
connection
|
|
.handle
|
|
.send(ToServer::Disconnected(connection.id))
|
|
.await;
|
|
}
|
|
}
|
|
|
|
async fn handle_rcon(listener: &Option<TcpListener>) -> Option<(TcpStream, SocketAddr)> {
|
|
match listener {
|
|
Some(listener) => Some(listener.accept().await.ok()?),
|
|
None => None,
|
|
}
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
tracing_subscriber::fmt::init();
|
|
|
|
let config = get_config();
|
|
|
|
let addr = config.world.get_socketaddr();
|
|
|
|
let listener = TcpListener::bind(addr).await.unwrap();
|
|
|
|
let rcon_listener = if !config.world.rcon_password.is_empty() {
|
|
Some(
|
|
TcpListener::bind(config.world.get_rcon_socketaddr())
|
|
.await
|
|
.unwrap(),
|
|
)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
tracing::info!("Server started on {addr}");
|
|
|
|
let database = Arc::new(WorldDatabase::new());
|
|
let lua = Arc::new(Mutex::new(Lua::new()));
|
|
let game_data = Arc::new(Mutex::new(GameData::new()));
|
|
|
|
{
|
|
let mut lua = lua.lock().unwrap();
|
|
if let Err(err) = load_init_script(&mut lua) {
|
|
tracing::warn!("Failed to load Init.lua: {:?}", err);
|
|
}
|
|
}
|
|
|
|
let (handle, _) = spawn_main_loop();
|
|
|
|
loop {
|
|
tokio::select! {
|
|
Ok((socket, ip)) = listener.accept() => {
|
|
let id = handle.next_id();
|
|
|
|
let state = PacketState {
|
|
client_key: None,
|
|
clientbound_oodle: OodleNetwork::new(),
|
|
serverbound_oodle: OodleNetwork::new(),
|
|
};
|
|
|
|
spawn_client(ZoneConnection {
|
|
config: get_config().world,
|
|
socket,
|
|
state,
|
|
player_data: PlayerData::default(),
|
|
spawn_index: 0,
|
|
zone: None,
|
|
status_effects: StatusEffects::default(),
|
|
event: None,
|
|
actors: Vec::new(),
|
|
ip,
|
|
id,
|
|
handle: handle.clone(),
|
|
database: database.clone(),
|
|
lua: lua.clone(),
|
|
gamedata: game_data.clone(),
|
|
exit_position: None,
|
|
exit_rotation: None,
|
|
last_keep_alive: Instant::now(),
|
|
gracefully_logged_out: false
|
|
});
|
|
}
|
|
Some((mut socket, _)) = handle_rcon(&rcon_listener) => {
|
|
let mut authenticated = false;
|
|
|
|
loop {
|
|
// read from client
|
|
let mut resp_bytes = [0u8; rkon::MAX_PACKET_SIZE];
|
|
let n = socket.read(&mut resp_bytes).await.unwrap();
|
|
if n > 0 {
|
|
let request = rkon::Packet::decode(&resp_bytes).unwrap();
|
|
|
|
match request.packet_type {
|
|
rkon::PacketType::Command => {
|
|
if authenticated {
|
|
let response = rkon::Packet {
|
|
request_id: request.request_id,
|
|
packet_type: rkon::PacketType::Command,
|
|
body: "hello world!".to_string()
|
|
};
|
|
let encoded = response.encode();
|
|
socket.write_all(&encoded).await.unwrap();
|
|
}
|
|
},
|
|
rkon::PacketType::Login => {
|
|
let config = get_config();
|
|
if request.body == config.world.rcon_password {
|
|
authenticated = true;
|
|
|
|
let response = rkon::Packet {
|
|
request_id: request.request_id,
|
|
packet_type: rkon::PacketType::Command,
|
|
body: String::default()
|
|
};
|
|
let encoded = response.encode();
|
|
socket.write_all(&encoded).await.unwrap();
|
|
} else {
|
|
authenticated = false;
|
|
|
|
let response = rkon::Packet {
|
|
request_id: -1,
|
|
packet_type: rkon::PacketType::Command,
|
|
body: String::default()
|
|
};
|
|
let encoded = response.encode();
|
|
socket.write_all(&encoded).await.unwrap();
|
|
}
|
|
},
|
|
_ => tracing::warn!("Ignoring unknown RCON packet")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|