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

Initial support for multiplayer

This is quite the architecture change, and I started working on the
first Tokio actor tutorial I could find. This actually works though,
and you can now chat between two characters on the server.

The next steps are to clean up my mess, and send actors over the wire.
This commit is contained in:
Joshua Goins 2025-03-30 16:41:06 -04:00
parent 7efbc5fd02
commit caaa9c9b13
3 changed files with 520 additions and 369 deletions

View file

@ -1,4 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}; use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType};
@ -24,112 +25,184 @@ use kawari::world::{
SocialList, SocialList,
}, },
}; };
use kawari::world::{EffectsBuilder, LuaPlayer, PlayerData, StatusEffects, WorldDatabase}; use kawari::world::{
ClientHandle, ClientId, EffectsBuilder, FromServer, LuaPlayer, PlayerData, ServerHandle,
StatusEffects, ToServer, WorldDatabase,
};
use mlua::{Function, Lua}; use mlua::{Function, Lua};
use std::net::SocketAddr;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::net::TcpListener; use tokio::join;
use tokio::net::tcp::WriteHalf;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc::{
Receiver, Sender, UnboundedReceiver, UnboundedSender, channel, unbounded_channel,
};
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
#[derive(Default)] #[derive(Default)]
struct ExtraLuaState { struct ExtraLuaState {
action_scripts: HashMap<u32, String>, action_scripts: HashMap<u32, String>,
} }
#[tokio::main] #[derive(Default, Debug)]
async fn main() { struct Data {
tracing_subscriber::fmt::init(); clients: HashMap<ClientId, ClientHandle>,
let config = get_config();
let addr = config.world.get_socketaddr();
let listener = TcpListener::bind(addr).await.unwrap();
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 lua = lua.lock().unwrap();
let register_action_func = lua
.create_function(|lua, (action_id, action_script): (u32, String)| {
tracing::info!("Registering {action_id} with {action_script}!");
let mut state = lua.app_data_mut::<ExtraLuaState>().unwrap();
let _ = state.action_scripts.insert(action_id, action_script);
Ok(())
})
.unwrap();
lua.set_app_data(ExtraLuaState::default());
lua.globals()
.set("registerAction", register_action_func)
.unwrap();
let effectsbuilder_constructor = lua
.create_function(|_, ()| Ok(EffectsBuilder::default()))
.unwrap();
lua.globals()
.set("EffectsBuilder", effectsbuilder_constructor)
.unwrap();
let file_name = format!("{}/Global.lua", &config.world.scripts_location);
lua.load(std::fs::read(&file_name).expect("Failed to locate scripts directory!"))
.set_name("@".to_string() + &file_name)
.exec()
.unwrap();
} }
loop { async fn main_loop(mut recv: Receiver<ToServer>) -> Result<(), std::io::Error> {
let (socket, _) = listener.accept().await.unwrap(); let mut data = Data::default();
let database = database.clone(); while let Some(msg) = recv.recv().await {
let lua = lua.clone(); match msg {
let game_data = game_data.clone(); ToServer::NewClient(handle) => {
data.clients.insert(handle.id, handle);
}
ToServer::Message(from_id, msg) => {
let mut to_remove = Vec::new();
let state = PacketState { for (id, handle) in data.clients.iter_mut() {
client_key: None, let id = *id;
clientbound_oodle: OodleNetwork::new(),
serverbound_oodle: OodleNetwork::new(), if id == from_id {
continue;
}
let msg = FromServer::Message(msg.clone());
if handle.send(msg).is_err() {
to_remove.push(id);
}
}
// Remove any clients that errored out
for id in to_remove {
data.clients.remove(&id);
}
}
ToServer::FatalError(err) => return Err(err),
}
}
Ok(())
}
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 = main_loop(recv).await;
match res {
Ok(()) => {}
Err(err) => {
tracing::error!("{}", err);
}
}
});
(handle, join)
}
struct ClientData {
id: ClientId,
handle: ServerHandle,
/// Socket for data recieved from the global server
recv: Receiver<FromServer>,
connection: ZoneConnection,
}
#[derive(Debug)]
enum InternalMsg {
Message(String),
}
/// Spawn a new client actor.
pub fn spawn_client(info: ZoneConnection) {
let (send, recv) = channel(64);
let id = &info.id.clone();
let ip = &info.ip.clone();
let data = ClientData {
id: info.id,
handle: info.handle.clone(),
recv,
connection: info,
};
// 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,
kill,
};
let _ = my_send.send(handle);
}
async fn start_client(my_handle: oneshot::Receiver<ClientHandle>, mut data: ClientData) {
// Recieve client information from global
let my_handle = match my_handle.await {
Ok(my_handle) => my_handle,
Err(_) => return,
};
data.handle.send(ToServer::NewClient(my_handle)).await;
let mut connection = data.connection;
let recv = data.recv;
let (_, write) = &connection.socket.split();
// communication channel between client_loop and client_server_loop
let (internal_send, internal_recv) = unbounded_channel();
join! {
client_loop(connection, internal_recv),
client_server_loop(recv, internal_send)
};
}
async fn client_server_loop(
mut data: Receiver<FromServer>,
internal_send: UnboundedSender<InternalMsg>,
) {
loop {
match data.recv().await {
Some(msg) => match msg {
FromServer::Message(msg) => internal_send.send(InternalMsg::Message(msg)).unwrap(),
},
None => break,
}
}
}
async fn client_loop(
mut connection: ZoneConnection,
mut internal_recv: UnboundedReceiver<InternalMsg>,
) {
let database = connection.database.clone();
let game_data = connection.gamedata.clone();
let lua = connection.lua.clone();
let config = get_config();
let mut exit_position = None; let mut exit_position = None;
let mut exit_rotation = None; let mut exit_rotation = None;
let mut connection = ZoneConnection {
socket,
state,
player_data: PlayerData::default(),
spawn_index: 0,
zone: None,
inventory: Inventory::new(),
status_effects: StatusEffects::default(),
event: None,
actors: Vec::new(),
};
let mut lua_player = LuaPlayer::default(); let mut lua_player = LuaPlayer::default();
/*let config = get_config();
let mut game_data =
GameData::from_existing(Platform::Win32, &config.game_location).unwrap();
let exh = game_data.read_excel_sheet_header("Action").unwrap();
let exd = game_data
.read_excel_sheet("Action", &exh, Language::English, 0)
.unwrap();*/
tokio::spawn(async move {
let mut buf = [0; 2056]; let mut buf = [0; 2056];
loop { loop {
let n = connection tokio::select! {
.socket Ok(n) = connection.socket.read(&mut buf) => {
.read(&mut buf)
.await
.expect("Failed to read data!");
if n != 0 { if n != 0 {
let (segments, connection_type) = connection.parse_packet(&buf[..n]).await; let (segments, connection_type) = connection.parse_packet(&buf[..n]).await;
for segment in &segments { for segment in &segments {
@ -173,8 +246,8 @@ async fn main() {
.await; .await;
} }
let chara_details = database let chara_details =
.find_chara_make(connection.player_data.content_id); database.find_chara_make(connection.player_data.content_id);
// fill inventory // fill inventory
connection.inventory.equip_racial_items( connection.inventory.equip_racial_items(
@ -188,8 +261,7 @@ async fn main() {
// set chara gear param // set chara gear param
connection connection
.actor_control_self(ActorControlSelf { .actor_control_self(ActorControlSelf {
category: category: ActorControlCategory::SetCharaGearParamUI {
ActorControlCategory::SetCharaGearParamUI {
unk1: 1, unk1: 1,
unk2: 1, unk2: 1,
}, },
@ -247,21 +319,12 @@ async fn main() {
name: chara_details.name, name: chara_details.name,
char_id: connection.player_data.actor_id, char_id: connection.player_data.actor_id,
race: chara_details.chara_make.customize.race, race: chara_details.chara_make.customize.race,
gender: chara_details gender: chara_details.chara_make.customize.gender,
.chara_make tribe: chara_details.chara_make.customize.subrace,
.customize
.gender,
tribe: chara_details
.chara_make
.customize
.subrace,
city_state: chara_details.city_state, city_state: chara_details.city_state,
nameday_month: chara_details nameday_month: chara_details.chara_make.birth_month
.chara_make
.birth_month
as u8,
nameday_day: chara_details.chara_make.birth_day
as u8, as u8,
nameday_day: chara_details.chara_make.birth_day as u8,
deity: chara_details.chara_make.guardian as u8, deity: chara_details.chara_make.guardian as u8,
..Default::default() ..Default::default()
}), }),
@ -281,12 +344,10 @@ async fn main() {
let lua = lua.lock().unwrap(); let lua = lua.lock().unwrap();
lua.scope(|scope| { lua.scope(|scope| {
let connection_data = scope let connection_data =
.create_userdata_ref_mut(&mut lua_player) scope.create_userdata_ref_mut(&mut lua_player).unwrap();
.unwrap();
let func: Function = let func: Function = lua.globals().get("onBeginLogin").unwrap();
lua.globals().get("onBeginLogin").unwrap();
func.call::<()>(connection_data).unwrap(); func.call::<()>(connection_data).unwrap();
@ -295,12 +356,8 @@ async fn main() {
.unwrap(); .unwrap();
} }
ClientZoneIpcData::FinishLoading { .. } => { ClientZoneIpcData::FinishLoading { .. } => {
tracing::info!( let chara_details =
"Client has finished loading... spawning in!" database.find_chara_make(connection.player_data.content_id);
);
let chara_details = database
.find_chara_make(connection.player_data.content_id);
// send player spawn // send player spawn
{ {
@ -312,72 +369,47 @@ async fn main() {
ipc = ServerZoneIpcSegment { ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::PlayerSpawn, op_code: ServerZoneIpcType::PlayerSpawn,
timestamp: timestamp_secs(), timestamp: timestamp_secs(),
data: ServerZoneIpcData::PlayerSpawn( data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn {
PlayerSpawn { account_id: connection.player_data.account_id,
account_id: connection content_id: connection.player_data.content_id,
.player_data
.account_id,
content_id: connection
.player_data
.content_id,
current_world_id: config.world.world_id, current_world_id: config.world.world_id,
home_world_id: config.world.world_id, home_world_id: config.world.world_id,
gm_rank: GameMasterRank::Debug, gm_rank: GameMasterRank::Debug,
online_status: online_status: OnlineStatus::GameMasterBlue,
OnlineStatus::GameMasterBlue,
common: CommonSpawn { common: CommonSpawn {
class_job: connection class_job: connection.player_data.classjob_id,
.player_data
.classjob_id,
name: chara_details.name, name: chara_details.name,
hp_curr: connection hp_curr: connection.player_data.curr_hp,
.player_data hp_max: connection.player_data.max_hp,
.curr_hp, mp_curr: connection.player_data.curr_mp,
hp_max: connection mp_max: connection.player_data.max_mp,
.player_data
.max_hp,
mp_curr: connection
.player_data
.curr_mp,
mp_max: connection
.player_data
.max_mp,
object_kind: ObjectKind::Player( object_kind: ObjectKind::Player(
PlayerSubKind::Player, PlayerSubKind::Player,
), ),
look: chara_details look: chara_details.chara_make.customize,
.chara_make
.customize,
fc_tag: "LOCAL".to_string(), fc_tag: "LOCAL".to_string(),
display_flags: DisplayFlag::UNK, display_flags: DisplayFlag::UNK,
models: [ models: [
game_data.get_primary_model_id( game_data
equipped.head.id, .get_primary_model_id(equipped.head.id)
)
as u32, as u32,
game_data.get_primary_model_id( game_data
equipped.body.id, .get_primary_model_id(equipped.body.id)
)
as u32, as u32,
game_data.get_primary_model_id( game_data
equipped.hands.id, .get_primary_model_id(equipped.hands.id)
)
as u32, as u32,
game_data.get_primary_model_id( game_data
equipped.legs.id, .get_primary_model_id(equipped.legs.id)
)
as u32, as u32,
game_data.get_primary_model_id( game_data
equipped.feet.id, .get_primary_model_id(equipped.feet.id)
)
as u32, as u32,
game_data.get_primary_model_id( game_data
equipped.ears.id, .get_primary_model_id(equipped.ears.id)
)
as u32, as u32,
game_data.get_primary_model_id( game_data
equipped.neck.id, .get_primary_model_id(equipped.neck.id)
)
as u32, as u32,
game_data.get_primary_model_id( game_data.get_primary_model_id(
equipped.wrists.id, equipped.wrists.id,
@ -394,13 +426,11 @@ async fn main() {
], ],
pos: exit_position pos: exit_position
.unwrap_or(Position::default()), .unwrap_or(Position::default()),
rotation: exit_rotation rotation: exit_rotation.unwrap_or(0.0),
.unwrap_or(0.0),
..Default::default() ..Default::default()
}, },
..Default::default() ..Default::default()
}, }),
),
..Default::default() ..Default::default()
}; };
} }
@ -474,20 +504,13 @@ async fn main() {
let ipc = ServerZoneIpcSegment { let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::SocialList, op_code: ServerZoneIpcType::SocialList,
timestamp: timestamp_secs(), timestamp: timestamp_secs(),
data: ServerZoneIpcData::SocialList( data: ServerZoneIpcData::SocialList(SocialList {
SocialList {
request_type: request.request_type, request_type: request.request_type,
sequence: request.count, sequence: request.count,
entries: vec![PlayerEntry { entries: vec![PlayerEntry {
// TODO: fill with actual player data, it also shows up wrong in game // TODO: fill with actual player data, it also shows up wrong in game
content_id: connection content_id: connection.player_data.content_id,
.player_data zone_id: connection.zone.as_ref().unwrap().id,
.content_id,
zone_id: connection
.zone
.as_ref()
.unwrap()
.id,
zone_id1: 0x0100, zone_id1: 0x0100,
class_job: 36, class_job: 36,
level: 100, level: 100,
@ -496,22 +519,15 @@ async fn main() {
fc_tag: "LOCAL".to_string(), fc_tag: "LOCAL".to_string(),
..Default::default() ..Default::default()
}], }],
}, }),
),
..Default::default() ..Default::default()
}; };
connection connection
.send_segment(PacketSegment { .send_segment(PacketSegment {
source_actor: connection source_actor: connection.player_data.actor_id,
.player_data target_actor: connection.player_data.actor_id,
.actor_id, segment_type: SegmentType::Ipc { data: ipc },
target_actor: connection
.player_data
.actor_id,
segment_type: SegmentType::Ipc {
data: ipc,
},
}) })
.await; .await;
} }
@ -519,27 +535,19 @@ async fn main() {
let ipc = ServerZoneIpcSegment { let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::SocialList, op_code: ServerZoneIpcType::SocialList,
timestamp: timestamp_secs(), timestamp: timestamp_secs(),
data: ServerZoneIpcData::SocialList( data: ServerZoneIpcData::SocialList(SocialList {
SocialList {
request_type: request.request_type, request_type: request.request_type,
sequence: request.count, sequence: request.count,
entries: Default::default(), entries: Default::default(),
}, }),
),
..Default::default() ..Default::default()
}; };
connection connection
.send_segment(PacketSegment { .send_segment(PacketSegment {
source_actor: connection source_actor: connection.player_data.actor_id,
.player_data target_actor: connection.player_data.actor_id,
.actor_id, segment_type: SegmentType::Ipc { data: ipc },
target_actor: connection
.player_data
.actor_id,
segment_type: SegmentType::Ipc {
data: ipc,
},
}) })
.await; .await;
} }
@ -571,10 +579,7 @@ async fn main() {
.await; .await;
} }
} }
ClientZoneIpcData::UpdatePositionHandler { ClientZoneIpcData::UpdatePositionHandler { position, rotation } => {
position,
rotation,
} => {
tracing::info!( tracing::info!(
"Character moved to {position:#?} {}", "Character moved to {position:#?} {}",
rotation.to_degrees() rotation.to_degrees()
@ -594,9 +599,7 @@ async fn main() {
let ipc = ServerZoneIpcSegment { let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::LogOutComplete, op_code: ServerZoneIpcType::LogOutComplete,
timestamp: timestamp_secs(), timestamp: timestamp_secs(),
data: ServerZoneIpcData::LogOutComplete { data: ServerZoneIpcData::LogOutComplete { unk: [0; 8] },
unk: [0; 8],
},
..Default::default() ..Default::default()
}; };
@ -613,6 +616,8 @@ async fn main() {
tracing::info!("Client disconnected!"); tracing::info!("Client disconnected!");
} }
ClientZoneIpcData::ChatMessage(chat_message) => { ClientZoneIpcData::ChatMessage(chat_message) => {
connection.handle.send(ToServer::Message(connection.id, chat_message.message.clone())).await;
ChatHandler::handle_chat_message( ChatHandler::handle_chat_message(
&mut connection, &mut connection,
&mut lua_player, &mut lua_player,
@ -620,9 +625,7 @@ async fn main() {
) )
.await .await
} }
ClientZoneIpcData::GameMasterCommand { ClientZoneIpcData::GameMasterCommand { command, arg, .. } => {
command, arg, ..
} => {
tracing::info!("Got a game master command!"); tracing::info!("Got a game master command!");
match &command { match &command {
@ -632,16 +635,22 @@ async fn main() {
GameMasterCommandType::ChangeTerritory => { GameMasterCommandType::ChangeTerritory => {
connection.change_zone(*arg as u16).await connection.change_zone(*arg as u16).await
} }
GameMasterCommandType::ToggleInvisibility => connection.actor_control_self(ActorControlSelf { GameMasterCommandType::ToggleInvisibility => {
connection
.actor_control_self(ActorControlSelf {
category: category:
ActorControlCategory::ToggleInvisibility { ActorControlCategory::ToggleInvisibility {
invisible: 1 invisible: 1,
}, },
}).await, })
GameMasterCommandType::ToggleWireframe => connection.actor_control_self(ActorControlSelf { .await
}
GameMasterCommandType::ToggleWireframe => connection
.actor_control_self(ActorControlSelf {
category: category:
ActorControlCategory::ToggleWireframeRendering(), ActorControlCategory::ToggleWireframeRendering(),
}).await })
.await,
} }
} }
ClientZoneIpcData::EnterZoneLine { ClientZoneIpcData::EnterZoneLine {
@ -681,20 +690,12 @@ async fn main() {
connection.change_zone(new_territory).await; connection.change_zone(new_territory).await;
} }
ClientZoneIpcData::ActionRequest(request) => { ClientZoneIpcData::ActionRequest(request) => {
tracing::info!("Recieved action request: {:#?}!", request);
/*let action_row =
&exd.read_row(&exh, request.action_id).unwrap()[0];
println!("Found action: {:#?}", action_row);*/
let mut effects_builder = None; let mut effects_builder = None;
// run action script // run action script
{ {
let lua = lua.lock().unwrap(); let lua = lua.lock().unwrap();
let state = let state = lua.app_data_ref::<ExtraLuaState>().unwrap();
lua.app_data_ref::<ExtraLuaState>().unwrap();
if let Some(action_script) = if let Some(action_script) =
state.action_scripts.get(&request.action_id) state.action_scripts.get(&request.action_id)
@ -708,12 +709,12 @@ async fn main() {
let file_name = format!( let file_name = format!(
"{}/{}", "{}/{}",
&config.world.scripts_location, &config.world.scripts_location, action_script
action_script
); );
lua.load(std::fs::read(&file_name).expect( lua.load(
"Failed to locate scripts directory!", std::fs::read(&file_name)
)) .expect("Failed to locate scripts directory!"),
)
.set_name("@".to_string() + &file_name) .set_name("@".to_string() + &file_name)
.exec() .exec()
.unwrap(); .unwrap();
@ -722,9 +723,7 @@ async fn main() {
lua.globals().get("doAction").unwrap(); lua.globals().get("doAction").unwrap();
effects_builder = Some( effects_builder = Some(
func.call::<EffectsBuilder>( func.call::<EffectsBuilder>(connection_data)
connection_data,
)
.unwrap(), .unwrap(),
); );
@ -752,30 +751,24 @@ async fn main() {
} }
let actor = actor.clone(); let actor = actor.clone();
connection connection.update_hp_mp(actor.id, actor.hp, 10000).await;
.update_hp_mp(actor.id, actor.hp, 10000)
.await;
} }
let ipc = ServerZoneIpcSegment { let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::ActionResult, op_code: ServerZoneIpcType::ActionResult,
timestamp: timestamp_secs(), timestamp: timestamp_secs(),
data: ServerZoneIpcData::ActionResult( data: ServerZoneIpcData::ActionResult(ActionResult {
ActionResult {
main_target: request.target, main_target: request.target,
target_id_again: request.target, target_id_again: request.target,
action_id: request.action_id, action_id: request.action_id,
animation_lock_time: 0.6, animation_lock_time: 0.6,
rotation: connection.player_data.rotation, rotation: connection.player_data.rotation,
action_animation_id: request.action_id action_animation_id: request.action_id as u16, // assuming action id == animation id
as u16, // assuming action id == animation id
flag: 1, flag: 1,
effect_count: effects_builder.effects.len() effect_count: effects_builder.effects.len() as u8,
as u8,
effects, effects,
..Default::default() ..Default::default()
}, }),
),
..Default::default() ..Default::default()
}; };
@ -835,9 +828,7 @@ async fn main() {
name, name,
chara_make_json, chara_make_json,
} => { } => {
tracing::info!( tracing::info!("creating character from: {name} {chara_make_json}");
"creating character from: {name} {chara_make_json}"
);
let chara_make = CharaMake::from_json(chara_make_json); let chara_make = CharaMake::from_json(chara_make_json);
@ -845,8 +836,8 @@ async fn main() {
{ {
let mut game_data = game_data.lock().unwrap(); let mut game_data = game_data.lock().unwrap();
city_state = game_data city_state =
.get_citystate(chara_make.classjob_id as u16); game_data.get_citystate(chara_make.classjob_id as u16);
} }
let (content_id, actor_id) = database.create_player_data( let (content_id, actor_id) = database.create_player_data(
@ -856,9 +847,7 @@ async fn main() {
determine_initial_starting_zone(city_state), determine_initial_starting_zone(city_state),
); );
tracing::info!( tracing::info!("Created new player: {content_id} {actor_id}");
"Created new player: {content_id} {actor_id}"
);
// send them the new actor and content id // send them the new actor and content id
{ {
@ -870,8 +859,7 @@ async fn main() {
data: CustomIpcSegment { data: CustomIpcSegment {
unk1: 0, unk1: 0,
unk2: 0, unk2: 0,
op_code: op_code: CustomIpcType::CharacterCreated,
CustomIpcType::CharacterCreated,
server_id: 0, server_id: 0,
timestamp: 0, timestamp: 0,
data: CustomIpcData::CharacterCreated { data: CustomIpcData::CharacterCreated {
@ -902,9 +890,7 @@ async fn main() {
op_code: CustomIpcType::ActorIdFound, op_code: CustomIpcType::ActorIdFound,
server_id: 0, server_id: 0,
timestamp: 0, timestamp: 0,
data: CustomIpcData::ActorIdFound { data: CustomIpcData::ActorIdFound { actor_id },
actor_id,
},
}, },
}, },
}) })
@ -943,8 +929,7 @@ async fn main() {
let world_name; let world_name;
{ {
let mut game_data = game_data.lock().unwrap(); let mut game_data = game_data.lock().unwrap();
world_name = world_name = game_data.get_world_name(config.world.world_id);
game_data.get_world_name(config.world.world_id);
} }
let characters = database.get_character_list( let characters = database.get_character_list(
@ -997,8 +982,7 @@ async fn main() {
data: CustomIpcSegment { data: CustomIpcSegment {
unk1: 0, unk1: 0,
unk2: 0, unk2: 0,
op_code: op_code: CustomIpcType::CharacterDeleted,
CustomIpcType::CharacterDeleted,
server_id: 0, server_id: 0,
timestamp: 0, timestamp: 0,
data: CustomIpcData::CharacterDeleted { data: CustomIpcData::CharacterDeleted {
@ -1011,9 +995,9 @@ async fn main() {
.await; .await;
} }
} }
_ => panic!( _ => {
"The server is recieving a response or unknown custom IPC!" panic!("The server is recieving a response or unknown custom IPC!")
), }
} }
} }
_ => { _ => {
@ -1037,6 +1021,93 @@ async fn main() {
lua_player.status_effects = connection.status_effects.clone(); lua_player.status_effects = connection.status_effects.clone();
} }
} }
msg = internal_recv.recv() => match msg {
Some(msg) => match msg {
InternalMsg::Message(msg) => connection.send_message(&msg).await,
},
None => break,
}
}
}
}
#[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();
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 lua = lua.lock().unwrap();
let register_action_func = lua
.create_function(|lua, (action_id, action_script): (u32, String)| {
tracing::info!("Registering {action_id} with {action_script}!");
let mut state = lua.app_data_mut::<ExtraLuaState>().unwrap();
let _ = state.action_scripts.insert(action_id, action_script);
Ok(())
})
.unwrap();
lua.set_app_data(ExtraLuaState::default());
lua.globals()
.set("registerAction", register_action_func)
.unwrap();
let effectsbuilder_constructor = lua
.create_function(|_, ()| Ok(EffectsBuilder::default()))
.unwrap();
lua.globals()
.set("EffectsBuilder", effectsbuilder_constructor)
.unwrap();
let file_name = format!("{}/Global.lua", &config.world.scripts_location);
lua.load(std::fs::read(&file_name).expect("Failed to locate scripts directory!"))
.set_name("@".to_string() + &file_name)
.exec()
.unwrap();
}
let (handle, join) = spawn_main_loop();
loop {
let (socket, ip) = listener.accept().await.unwrap();
let id = handle.next_id();
let state = PacketState {
client_key: None,
clientbound_oodle: OodleNetwork::new(),
serverbound_oodle: OodleNetwork::new(),
};
spawn_client(ZoneConnection {
socket,
state,
player_data: PlayerData::default(),
spawn_index: 0,
zone: None,
inventory: Inventory::new(),
status_effects: StatusEffects::default(),
event: None,
actors: Vec::new(),
ip,
id,
handle: handle.clone(),
database: database.clone(),
lua: lua.clone(),
gamedata: game_data.clone(),
}); });
} }
join.await.unwrap();
} }

View file

@ -1,7 +1,15 @@
use tokio::net::TcpStream; use std::{
net::SocketAddr,
sync::{
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
},
};
use tokio::{net::TcpStream, sync::mpsc::Sender, task::JoinHandle};
use crate::{ use crate::{
common::{ObjectId, Position, timestamp_secs}, common::{GameData, ObjectId, Position, timestamp_secs},
opcodes::ServerZoneIpcType, opcodes::ServerZoneIpcType,
packet::{ packet::{
CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, parse_packet, CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, parse_packet,
@ -10,7 +18,7 @@ use crate::{
}; };
use super::{ use super::{
Actor, Event, Inventory, Item, LuaPlayer, StatusEffects, Zone, Actor, Event, Inventory, Item, LuaPlayer, StatusEffects, WorldDatabase, Zone,
ipc::{ ipc::{
ActorControlSelf, ActorSetPos, ClientZoneIpcSegment, ContainerInfo, ContainerType, ActorControlSelf, ActorSetPos, ClientZoneIpcSegment, ContainerInfo, ContainerType,
InitZone, ItemInfo, ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect, InitZone, ItemInfo, ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect,
@ -39,6 +47,68 @@ pub struct PlayerData {
pub zone_id: u16, pub zone_id: u16,
} }
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct ClientId(usize);
pub enum FromServer {
/// A chat message.
Message(String),
}
#[derive(Debug)]
pub struct ClientHandle {
pub id: ClientId,
pub ip: SocketAddr,
pub channel: Sender<FromServer>,
pub kill: JoinHandle<()>,
}
impl ClientHandle {
/// Send a message to this client actor. Will emit an error if sending does
/// not succeed immediately, as this means that forwarding messages to the
/// tcp connection cannot keep up.
pub fn send(&mut self, msg: FromServer) -> Result<(), std::io::Error> {
if self.channel.try_send(msg).is_err() {
Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"Can't keep up or dead",
))
} else {
Ok(())
}
}
/// Kill the actor.
pub fn kill(self) {
// run the destructor
drop(self);
}
}
pub enum ToServer {
NewClient(ClientHandle),
Message(ClientId, String),
FatalError(std::io::Error),
}
#[derive(Clone, Debug)]
pub struct ServerHandle {
pub chan: Sender<ToServer>,
pub next_id: Arc<AtomicUsize>,
}
impl ServerHandle {
pub async fn send(&mut self, msg: ToServer) {
if self.chan.send(msg).await.is_err() {
panic!("Main loop has shut down.");
}
}
pub fn next_id(&self) -> ClientId {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
ClientId(id)
}
}
/// Represents a single connection between an instance of the client and the world server /// Represents a single connection between an instance of the client and the world server
pub struct ZoneConnection { pub struct ZoneConnection {
pub socket: TcpStream, pub socket: TcpStream,
@ -54,6 +124,14 @@ pub struct ZoneConnection {
pub event: Option<Event>, pub event: Option<Event>,
pub actors: Vec<Actor>, pub actors: Vec<Actor>,
pub ip: SocketAddr,
pub id: ClientId,
pub handle: ServerHandle,
pub database: Arc<WorldDatabase>,
pub lua: Arc<Mutex<mlua::Lua>>,
pub gamedata: Arc<Mutex<GameData>>,
} }
impl ZoneConnection { impl ZoneConnection {

View file

@ -7,7 +7,9 @@ mod chat_handler;
pub use chat_handler::ChatHandler; pub use chat_handler::ChatHandler;
mod connection; mod connection;
pub use connection::{PlayerData, ZoneConnection}; pub use connection::{
ClientHandle, ClientId, FromServer, PlayerData, ServerHandle, ToServer, ZoneConnection,
};
mod database; mod database;
pub use database::{CharacterData, WorldDatabase}; pub use database::{CharacterData, WorldDatabase};