diff --git a/Cargo.lock b/Cargo.lock
index b0dbc96..a0d83ad 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "addr2line"
@@ -1755,6 +1755,32 @@ dependencies = [
"zerocopy 0.8.14",
]
+[[package]]
+name = "rc_chat"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "env_logger",
+ "log",
+ "polariton",
+ "polariton_auth",
+ "polariton_server",
+ "tokio",
+]
+
+[[package]]
+name = "rc_chat_room"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "env_logger",
+ "log",
+ "polariton",
+ "polariton_auth",
+ "polariton_server",
+ "tokio",
+]
+
[[package]]
name = "rc_services"
version = "0.1.0"
@@ -1782,6 +1808,32 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "rc_social"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "env_logger",
+ "log",
+ "polariton",
+ "polariton_auth",
+ "polariton_server",
+ "tokio",
+]
+
+[[package]]
+name = "rc_social_room"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "env_logger",
+ "log",
+ "polariton",
+ "polariton_auth",
+ "polariton_server",
+ "tokio",
+]
+
[[package]]
name = "rc_static_data"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index a3644dd..8cefe9d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,7 +5,12 @@ edition = "2021"
[workspace]
members = [
- "auth", "polariton_auth", "rc_services", "rc_static_data", "rc_services_room"
+ "auth",
+ "polariton_auth",
+ "rc_services", "rc_services_room",
+ "rc_static_data",
+ "rc_social", "rc_social_room",
+ "rc_chat", "rc_chat_room"
]
[workspace.dependencies]
diff --git a/assets/robocraft/servenvmulti.config b/assets/robocraft/servenvmulti.config
index 47eb7c7..008ad64 100644
--- a/assets/robocraft/servenvmulti.config
+++ b/assets/robocraft/servenvmulti.config
@@ -4,9 +4,11 @@
127.0.0.1:4532
- not.used.hopefully:4534
- chat.server.not.a.valid.tld:4534
+ 127.0.0.1:4533
+ 127.0.0.1:4534
+ 127.0.0.1:4535
http://127.0.0.1:8001/
+ http://127.0.0.1:8010/live/data.json
diff --git a/rc_chat/Cargo.toml b/rc_chat/Cargo.toml
new file mode 100644
index 0000000..d935953
--- /dev/null
+++ b/rc_chat/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "rc_chat"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+log.workspace = true
+env_logger.workspace = true
+tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] }
+clap.workspace = true
+polariton.workspace = true
+polariton_auth = { version = "*", path = "../polariton_auth" }
+polariton_server.workspace = true
diff --git a/rc_chat/build_arm64.sh b/rc_chat/build_arm64.sh
new file mode 100755
index 0000000..7010ff6
--- /dev/null
+++ b/rc_chat/build_arm64.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+cargo build --release --target aarch64-unknown-linux-musl
diff --git a/rc_chat/run_debug.sh b/rc_chat/run_debug.sh
new file mode 100755
index 0000000..eedcd2c
--- /dev/null
+++ b/rc_chat/run_debug.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+RUST_BACKTRACE=1 RUST_LOG=debug cargo run
diff --git a/rc_chat/src/cli.rs b/rc_chat/src/cli.rs
new file mode 100644
index 0000000..47cc79d
--- /dev/null
+++ b/rc_chat/src/cli.rs
@@ -0,0 +1,31 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+pub struct CliArgs {
+ /// TCP port on which to accept connections
+ #[arg(short, long, default_value_t = 4534)]
+ pub port: u16,
+
+ /// IP Address on which to accept connections
+ #[arg(long, default_value_t = {"127.0.0.1".to_string()})]
+ pub ip: String,
+
+ /// Socket read tries before giving up (0 to never give up)
+ #[arg(long, default_value_t = 5)]
+ pub retries: usize,
+
+ /// Domain and port of the game server to send new connections
+ #[arg(long, default_value_t = {"127.0.0.1:4535".to_string()})]
+ pub redirect: String,
+
+ /// Name of game server to send new connections
+ #[arg(long, default_value_t = {"ngram_is_ngnius".to_string()})]
+ pub room_name: String,
+}
+
+impl CliArgs {
+ pub fn get() -> Self {
+ Self::parse()
+ }
+}
diff --git a/rc_chat/src/main.rs b/rc_chat/src/main.rs
new file mode 100644
index 0000000..2d221af
--- /dev/null
+++ b/rc_chat/src/main.rs
@@ -0,0 +1,356 @@
+mod cli;
+mod state;
+
+use std::num::NonZero;
+use std::sync::Arc;
+
+use polariton_auth::Handshake;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::net;
+
+use polariton::packet::{Cryptographer, Data, Message, Packet, Ping, StandardMessage, StandardPacket};
+use polariton::operation::{OperationResponse, Typed};
+
+#[tokio::main]
+async fn main() -> std::io::Result<()> {
+ env_logger::init();
+ let args = cli::CliArgs::get();
+ log::debug!("Got cli args {:?}", args);
+
+ let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address");
+
+ // memory leak, but only once (so not a big deal)
+ let redirect_static = Box::leak(Box::new(args.redirect.clone()));
+ let room_name_static = Box::leak(Box::new(args.room_name.clone()));
+
+ let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?;
+
+ #[cfg(not(debug_assertions))]
+ loop {
+ let (socket, address) = listener.accept().await?;
+ tokio::spawn(process_socket(socket, address, NonZero::new(args.retries), op_handler.clone(), redirect_static, room_name_static));
+ }
+ #[cfg(debug_assertions)]
+ {
+ let (socket, address) = listener.accept().await?;
+ process_socket(socket, address, NonZero::new(args.retries), redirect_static, room_name_static).await;
+ Ok(())
+ }
+}
+
+async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, retries: Option>, redirect_url: &str, lobby_name: &str) {
+ log::debug!("Accepting connection from address {}", address);
+
+ let mut buf = Vec::new();
+ let enc = match do_connect_handshake(&mut buf, &mut socket, retries, lobby_name, redirect_url).await {
+ Some(x) => x,
+ None => {
+ log::error!("Failed to do connect handshake with {}", address);
+ return;
+ }
+ };
+ let sock_state = state::State::new(enc);
+ while let Ok(packet) = receive_packet(&mut buf, &mut socket, retries, sock_state.binrw_args()).await {
+ match packet {
+ Packet::Ping(ping) => {
+ handle_ping(ping, &mut buf, &mut socket).await;
+ },
+ Packet::Packet(packet) => log::warn!("Not handling packet {:?}", packet),
+ }
+ }
+ log::debug!("Goodbye connection from address {}", address);
+}
+
+async fn handle_ping(ping: Ping, buf: &mut Vec, socket: &mut net::TcpStream) {
+ buf.clear();
+ let resp = Packet::Ping(polariton_auth::ping_pong(ping));
+ resp.to_buf(buf, None).unwrap();
+ let write_count = socket.write(buf).await.unwrap();
+ log::debug!("(ping) Write {} bytes to socket: {:?}", write_count, buf);
+ buf.clear();
+}
+
+fn buf_likely_valid(buf: &[u8]) -> bool {
+ buf.is_empty() || buf[0] == Packet::PING_MAGIC || buf[0] == Packet::FRAMED_MAGIC
+}
+
+async fn read_more(buf: &mut Vec, socket: &mut net::TcpStream) -> Result {
+ let read_count = socket.read_buf(buf).await?;
+ log::debug!("Read {} bytes from socket: {:?}", read_count, buf);
+ Ok(read_count)
+}
+
+async fn receive_packet(buf: &mut Vec, socket: &mut net::TcpStream, max_retries: Option>, args: Option>>) -> Result {
+ if buf.is_empty() {
+ let read_count = read_more(buf, socket).await?;
+ if read_count == 0 { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "socket did not read any bytes")); } // bad packet
+ }
+
+ let mut last_err = None;
+ let mut must_succeed_next = false;
+ if let Some(max_retries) = max_retries {
+ for _ in 0..max_retries.get() {
+ match Packet::from_buf(&buf, args.clone()) {
+ Ok(packet) => {
+ log::debug!("Received packet {:?}", packet);
+ return Ok(packet);
+ },
+ Err(e) => last_err = Some(e),
+ }
+ if must_succeed_next {
+ break;
+ }
+ must_succeed_next = read_more(buf, socket).await? == 0;
+ }
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap()));
+ } else {
+ while buf_likely_valid(buf.as_slice()) {
+ match Packet::from_buf(&buf, args.clone()) {
+ Ok(packet) => {
+ log::debug!("Received packet {:?}", packet);
+ return Ok(packet);
+ },
+ Err(e) => last_err = Some(e),
+ }
+ if must_succeed_next {
+ break;
+ }
+ must_succeed_next = read_more(buf, socket).await? == 0;
+ }
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap()));
+ }
+}
+
+async fn send_packet(packet: Packet, buf: &mut Vec, socket: &mut net::TcpStream, args: Option>>) -> Result<(), std::io::Error> {
+ log::debug!("Sending packet {:?}", packet);
+ buf.clear();
+ packet.to_buf(buf, args).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
+ let write_count = socket.write(buf).await?;
+ log::debug!("Write {} bytes to socket: {:?}", write_count, buf);
+ #[cfg(debug_assertions)]
+ {
+ // print out unencrypted packet too
+ if let Packet::Packet(standard_p) = packet {
+ if let Message::Standard(standard_m) = standard_p.message {
+ if standard_m.is_encrypted() {
+ let standard_m = standard_m.encrypt(false);
+ let packet = Packet::Packet(StandardPacket { header: standard_p.header, message: Message::Standard(standard_m) });
+ packet.to_buf(buf, None).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
+ log::debug!("Unencrypted bytes of packet: {:?} (len: {})", buf, buf.len());
+ }
+
+ }
+ }
+ }
+ buf.clear();
+ Ok(())
+}
+
+const APP_ID: &str = "SocialServer";
+
+struct AuthImpl;
+
+const TOKEN_KEY: u8 = 216; // token;refresh_token
+//const UNKNOWN_BYTE_KEY: u8 = 217;
+const SERVICE_KEY: u8 = 224;
+const USERNAME_KEY: u8 = 225;
+
+//const CCU_KEY: u8 = 245;
+
+#[derive(Debug)]
+enum AuthError {
+ WrongService { expected: String, actual: String },
+ MissingService,
+ MissingToken,
+ MissingUsername,
+}
+
+impl AuthError {
+ fn log_err(&self) {
+ match self {
+ Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected),
+ Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY),
+ Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY),
+ Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY),
+ }
+ }
+}
+
+impl polariton_auth::AuthProvider for AuthImpl {
+ fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> {
+ if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) {
+ if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) {
+ if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) {
+ if service.string == APP_ID {
+ let params_resp = std::collections::HashMap::::new();
+ //params_resp.insert(CCU_KEY, Typed::Byte(0));
+ log::debug!("Auth success for {} (token: {})", user.string, token.string);
+ Ok(params_resp)
+ } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) }
+ } else { Err(AuthError::MissingUsername) }
+ } else { Err(AuthError::MissingService) }
+ } else { Err(AuthError::MissingToken) }
+ }
+}
+
+async fn do_connect_handshake(
+ buf: &mut Vec,
+ socket: &mut net::TcpStream,
+ max_retries: Option>,
+ game_server_name: &str,
+ game_server_url: &str,
+) -> Option>> {
+ let handshake = Handshake::new(APP_ID);
+ // connect
+ log::debug!("(connect) Handling first packet");
+ let packet1 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read connect packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ let (handshake, to_send) = match handshake.connect(&packet1) {
+ Ok(x) => (x.handshake, x.extra),
+ Err(e) => {
+ log::error!("Failed to handle connect handshake: {:?}", e.extra);
+ return None;
+ }
+ };
+ match send_packet(to_send, buf, socket, None).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send connect ack packet: {}", e);
+ return None;
+ }
+ }
+ // encrypt
+ log::debug!("(connect) Handling second packet");
+ let mut packet2 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) public key packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet2 {
+ handle_ping(ping, buf, socket).await;
+ packet2 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) public key packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) {
+ Ok(x) => (x.handshake, x.extra.0, x.extra.1),
+ Err(e) => {
+ log::error!("Failed to handle encryption handshake: {:?}", e.extra);
+ return None;
+ }
+ };
+ match send_packet(to_send, buf, socket, None).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send encryption ack packet: {}", e);
+ return None;
+ }
+ }
+ // pre-auth
+ let handshake = handshake.with_auth(AuthImpl);
+ // authenticate
+ log::debug!("(connect) Handling third packet");
+ let mut packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) auth packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet3 {
+ handle_ping(ping, buf, socket).await;
+ packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) auth packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ let to_send = match handshake.authenticate(&packet3, crypto.clone()) {
+ Ok(x) => x,
+ Err(h) => match h.extra {
+ polariton_auth::AuthError::Validation(e) => {
+ e.log_err();
+ return None;
+ },
+ e => {
+ log::error!("Failed to handle auth handshake: {:?}", e);
+ return None;
+ },
+ },
+ };
+ match send_packet(to_send, buf, socket, Some(crypto.clone())).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send auth ack packet: {}", e);
+ return None;
+ }
+ }
+
+ // redirect to lobby
+ let mut packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) join packet: {}", e);
+ return None;
+ }
+ };
+ while let Packet::Ping(ping) = packet_j {
+ handle_ping(ping, buf, socket).await;
+ packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) join packet: {}", e);
+ return None;
+ }
+ };
+ }
+ if let Packet::Packet(msg) = &packet_j {
+ if let Message::Standard(st) = &msg.message {
+ if let Data::OpReq(req) = &st.data {
+ if req.code == 225 { // join lobby
+ log::debug!("Max players from lobby join request: {:?}", req.params.to_owned().to_dict().get(&255));
+ let mut params = std::collections::HashMap::::new();
+ params.insert(230 /* game server address */, Typed::Str(game_server_url.into()));
+ params.insert(255 /* room name */, Typed::Str(game_server_name.into()));
+ let resp = Packet::from_message(
+ Message::Standard(
+ StandardMessage { flags: 0,
+ data: Data::OpResp(OperationResponse {
+ code: req.code,
+ return_code: 0,
+ message: Typed::Null,
+ params: params.into(),
+ }),
+ }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap();
+ match send_packet(resp, buf, socket, Some(crypto.clone())).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send lobby ack packet: {}", e);
+ return None;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Some(crypto)
+}
diff --git a/rc_chat/src/operations/mod.rs b/rc_chat/src/operations/mod.rs
new file mode 100644
index 0000000..caae20b
--- /dev/null
+++ b/rc_chat/src/operations/mod.rs
@@ -0,0 +1,3 @@
+pub fn handler() -> polariton_server::operations::OperationsHandler {
+ polariton_server::operations::OperationsHandler::new()
+}
diff --git a/rc_chat/src/state.rs b/rc_chat/src/state.rs
new file mode 100644
index 0000000..2bd18ab
--- /dev/null
+++ b/rc_chat/src/state.rs
@@ -0,0 +1,17 @@
+use std::sync::Arc;
+
+pub struct State {
+ pub crypto: Box>,
+}
+
+impl State {
+ pub fn new(c: Box>) -> Self {
+ Self {
+ crypto: c,
+ }
+ }
+
+ pub fn binrw_args(&self) -> polariton::packet::WriteArgs {
+ Some(self.crypto.clone())
+ }
+}
diff --git a/rc_chat_room/Cargo.toml b/rc_chat_room/Cargo.toml
new file mode 100644
index 0000000..a415aaf
--- /dev/null
+++ b/rc_chat_room/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "rc_chat_room"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+log.workspace = true
+env_logger.workspace = true
+tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] }
+clap.workspace = true
+polariton.workspace = true
+polariton_auth = { version = "*", path = "../polariton_auth" }
+polariton_server.workspace = true
diff --git a/rc_chat_room/build_arm64.sh b/rc_chat_room/build_arm64.sh
new file mode 100755
index 0000000..7010ff6
--- /dev/null
+++ b/rc_chat_room/build_arm64.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+cargo build --release --target aarch64-unknown-linux-musl
diff --git a/rc_chat_room/run_debug.sh b/rc_chat_room/run_debug.sh
new file mode 100755
index 0000000..eedcd2c
--- /dev/null
+++ b/rc_chat_room/run_debug.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+RUST_BACKTRACE=1 RUST_LOG=debug cargo run
diff --git a/rc_chat_room/src/cli.rs b/rc_chat_room/src/cli.rs
new file mode 100644
index 0000000..11819ce
--- /dev/null
+++ b/rc_chat_room/src/cli.rs
@@ -0,0 +1,23 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+pub struct CliArgs {
+ /// TCP port on which to accept connections
+ #[arg(short, long, default_value_t = 4535)]
+ pub port: u16,
+
+ /// IP Address on which to accept connections
+ #[arg(long, default_value_t = {"127.0.0.1".to_string()})]
+ pub ip: String,
+
+ /// Socket read tries before giving up (0 to never give up)
+ #[arg(long, default_value_t = 5)]
+ pub retries: usize,
+}
+
+impl CliArgs {
+ pub fn get() -> Self {
+ Self::parse()
+ }
+}
diff --git a/rc_chat_room/src/data/clan_invite.rs b/rc_chat_room/src/data/clan_invite.rs
new file mode 100644
index 0000000..abbed9d
--- /dev/null
+++ b/rc_chat_room/src/data/clan_invite.rs
@@ -0,0 +1,23 @@
+use polariton::operation::Typed;
+
+pub struct ClanInviteInfo {
+ pub username: String,
+ pub display_name: String,
+ pub clan_name: String,
+ pub clan_size: i32,
+ pub use_custom_avatar: bool,
+ pub avatar_id: i32,
+}
+
+impl ClanInviteInfo {
+ pub fn as_transmissible(&self) -> Typed {
+ Typed::HashMap(vec![
+ (Typed::Str("userName".into()), Typed::Str(self.username.clone().into())),
+ (Typed::Str("displayName".into()), Typed::Str(self.display_name.clone().into())),
+ (Typed::Str("clanName".into()), Typed::Str(self.clan_name.clone().into())),
+ (Typed::Str("clanSize".into()), Typed::Int(self.clan_size)),
+ (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.use_custom_avatar.into())),
+ (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id)),
+ ].into())
+ }
+}
diff --git a/rc_chat_room/src/data/friend.rs b/rc_chat_room/src/data/friend.rs
new file mode 100644
index 0000000..724116e
--- /dev/null
+++ b/rc_chat_room/src/data/friend.rs
@@ -0,0 +1,19 @@
+use polariton::operation::Typed;
+
+pub struct AvatarInfo {
+ pub name: String,
+ pub use_custom_avatar: bool,
+ pub avatar_id: i32,
+}
+
+impl AvatarInfo {
+ pub fn as_transmissible(&self) -> Typed {
+ Typed::HashMap(vec![
+ (Typed::Str("name".into()), Typed::Str(self.name.clone().into())),
+ (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.use_custom_avatar.into())),
+ (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id)),
+ ].into())
+ }
+}
+
+// TODO pub struct FriendInfo {}
diff --git a/rc_chat_room/src/data/mod.rs b/rc_chat_room/src/data/mod.rs
new file mode 100644
index 0000000..4de3fde
--- /dev/null
+++ b/rc_chat_room/src/data/mod.rs
@@ -0,0 +1,2 @@
+pub mod friend;
+pub mod clan_invite;
diff --git a/rc_chat_room/src/main.rs b/rc_chat_room/src/main.rs
new file mode 100644
index 0000000..dcddc27
--- /dev/null
+++ b/rc_chat_room/src/main.rs
@@ -0,0 +1,404 @@
+mod cli;
+mod state;
+
+mod data;
+mod operations;
+
+use std::num::NonZero;
+use std::sync::Arc;
+
+use polariton_auth::Handshake;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::net;
+
+use polariton::packet::{Cryptographer, Data, Message, Packet, Ping, StandardMessage, StandardPacket};
+use polariton::operation::{OperationResponse, Typed};
+
+pub type UserTy = std::sync::RwLock;
+
+#[tokio::main]
+async fn main() -> std::io::Result<()> {
+ env_logger::init();
+ let args = cli::CliArgs::get();
+ log::debug!("Got cli args {:?}", args);
+
+ let op_handler = Arc::new(operations::handler());
+
+ let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address");
+
+ let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?;
+
+ #[cfg(not(debug_assertions))]
+ loop {
+ let (socket, address) = listener.accept().await?;
+ tokio::spawn(process_socket(socket, address, NonZero::new(args.retries), op_handler.clone()));
+ }
+ #[cfg(debug_assertions)]
+ {
+ let (socket, address) = listener.accept().await?;
+ process_socket(socket, address, NonZero::new(args.retries), op_handler.clone()).await;
+ Ok(())
+ }
+}
+
+async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, retries: Option>, op_handler: Arc>) {
+ log::debug!("Accepting connection from address {}", address);
+
+ let mut read_buf = Vec::new();
+ let mut write_buf = Vec::new();
+ let enc = match do_connect_handshake(&mut read_buf, &mut socket, retries).await {
+ Some(x) => x,
+ None => {
+ log::error!("Failed to do connect handshake with {}", address);
+ return;
+ }
+ };
+ let sock_state = state::State::new(enc);
+ let user_state = sock_state.user();
+ while let Ok(packet) = receive_packet(&mut read_buf, &mut socket, retries, sock_state.binrw_args()).await {
+ match packet {
+ Packet::Ping(ping) => {
+ handle_ping(ping, &mut write_buf, &mut socket).await;
+ for _ in 0..5 {
+ read_buf.remove(0);
+ }
+ },
+ Packet::Packet(packet) => {
+ // remove packet's advertised size from the buffer
+ for _ in 0..packet.header.len {
+ read_buf.remove(0);
+ }
+ match packet.message {
+ Message::Ping(ping) => {
+ handle_ping(ping, &mut write_buf, &mut socket).await;
+ },
+ Message::Standard(msg) => {
+
+ let is_encrypted = msg.is_encrypted();
+ match msg.data {
+ Data::OpReq(req) => {
+ let resp = op_handler.handle_op(&user_state, req);
+ let result = send_packet(
+ Packet::from_message(
+ Message::Standard(StandardMessage {
+ flags: 0,
+ data: Data::OpResp(resp),
+ }.encrypt(is_encrypted)),
+ packet.header.channel,
+ packet.header.is_reliable(),
+ sock_state.binrw_args()).unwrap(),
+ &mut write_buf, &mut socket, sock_state.binrw_args()).await;
+ match result {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send operation response packet: {}", e);
+ }
+ }
+ },
+ data => log::warn!("Failed to handle packet with message data {:?}", data),
+ }
+ }
+ }
+ }
+ //log::warn!("Not handling packet {:?}", packet),
+ }
+ }
+ log::debug!("Goodbye connection from address {}", address);
+}
+
+async fn handle_ping(ping: Ping, buf: &mut Vec, socket: &mut net::TcpStream) {
+ buf.clear();
+ let resp = Packet::Ping(polariton_auth::ping_pong(ping));
+ resp.to_buf(buf, None).unwrap();
+ let write_count = socket.write(buf).await.unwrap();
+ log::debug!("(ping) Write {} bytes to socket: {:?}", write_count, buf);
+ buf.clear();
+}
+
+fn buf_likely_valid(buf: &[u8]) -> bool {
+ buf.is_empty() || buf[0] == Packet::PING_MAGIC || buf[0] == Packet::FRAMED_MAGIC
+}
+
+async fn read_more(buf: &mut Vec, socket: &mut net::TcpStream) -> Result {
+ let read_count = socket.read_buf(buf).await?;
+ log::debug!("Read {} bytes from socket: {:?}", read_count, buf);
+ Ok(read_count)
+}
+
+async fn receive_packet(buf: &mut Vec, socket: &mut net::TcpStream, max_retries: Option>, args: Option>>) -> Result {
+ if buf.is_empty() {
+ let read_count = read_more(buf, socket).await?;
+ if read_count == 0 { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "socket did not read any bytes")); } // bad packet
+ }
+
+ let mut last_err = None;
+ let mut must_succeed_next = false;
+ if let Some(max_retries) = max_retries {
+ for _ in 0..max_retries.get() {
+ match Packet::from_buf(&buf, args.clone()) {
+ Ok(packet) => {
+ log::debug!("Received packet {:?}", packet);
+ return Ok(packet);
+ },
+ Err(e) => last_err = Some(e),
+ }
+ if must_succeed_next {
+ break;
+ }
+ must_succeed_next = read_more(buf, socket).await? == 0;
+ }
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap()));
+ } else {
+ while buf_likely_valid(buf.as_slice()) {
+ match Packet::from_buf(&buf, args.clone()) {
+ Ok(packet) => {
+ log::debug!("Received packet {:?}", packet);
+ return Ok(packet);
+ },
+ Err(e) => last_err = Some(e),
+ }
+ if must_succeed_next {
+ break;
+ }
+ must_succeed_next = read_more(buf, socket).await? == 0;
+ }
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap()));
+ }
+}
+
+async fn send_packet(packet: Packet, buf: &mut Vec, socket: &mut net::TcpStream, args: Option>>) -> Result<(), std::io::Error> {
+ log::debug!("Sending packet {:?}", packet);
+ buf.clear();
+ packet.to_buf(buf, args).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
+ let write_count = socket.write(buf).await?;
+ log::debug!("Write {} bytes to socket: {:?}", write_count, buf);
+ #[cfg(debug_assertions)]
+ {
+ // print out unencrypted packet too
+ if let Packet::Packet(standard_p) = packet {
+ if let Message::Standard(standard_m) = standard_p.message {
+ if standard_m.is_encrypted() {
+ let standard_m = standard_m.encrypt(false);
+ let packet = Packet::Packet(StandardPacket { header: standard_p.header, message: Message::Standard(standard_m) });
+ packet.to_buf(buf, None).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
+ log::debug!("Unencrypted bytes of packet: {:?} (len: {})", buf, buf.len());
+ }
+
+ }
+ }
+ }
+ buf.clear();
+ Ok(())
+}
+
+const APP_ID: &str = "SocialServer";
+
+struct AuthImpl;
+
+const TOKEN_KEY: u8 = 216; // token;refresh_token
+//const UNKNOWN_BYTE_KEY: u8 = 217;
+const SERVICE_KEY: u8 = 224;
+const USERNAME_KEY: u8 = 225;
+
+//const CCU_KEY: u8 = 245;
+
+#[derive(Debug)]
+enum AuthError {
+ WrongService { expected: String, actual: String },
+ MissingService,
+ MissingToken,
+ MissingUsername,
+}
+
+impl AuthError {
+ fn log_err(&self) {
+ match self {
+ Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected),
+ Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY),
+ Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY),
+ Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY),
+ }
+ }
+}
+
+impl polariton_auth::AuthProvider for AuthImpl {
+ fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> {
+ if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) {
+ if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) {
+ if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) {
+ if service.string == APP_ID {
+ let params_resp = std::collections::HashMap::::new();
+ //params_resp.insert(CCU_KEY, Typed::Byte(0));
+ log::debug!("Auth success for {} (token: {})", user.string, token.string);
+ Ok(params_resp)
+ } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) }
+ } else { Err(AuthError::MissingUsername) }
+ } else { Err(AuthError::MissingService) }
+ } else { Err(AuthError::MissingToken) }
+ }
+}
+
+async fn do_connect_handshake(
+ buf: &mut Vec,
+ socket: &mut net::TcpStream,
+ max_retries: Option>,
+) -> Option>> {
+ let handshake = Handshake::new(APP_ID);
+ // connect
+ log::debug!("(connect) Handling first packet");
+ let packet1 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read connect packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ let (handshake, to_send) = match handshake.connect(&packet1) {
+ Ok(x) => (x.handshake, x.extra),
+ Err(e) => {
+ log::error!("Failed to handle connect handshake: {:?}", e.extra);
+ return None;
+ }
+ };
+ match send_packet(to_send, buf, socket, None).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send connect ack packet: {}", e);
+ return None;
+ }
+ }
+ // encrypt
+ log::debug!("(connect) Handling second packet");
+ let mut packet2 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) public key packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet2 {
+ handle_ping(ping, buf, socket).await;
+ packet2 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) public key packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) {
+ Ok(x) => (x.handshake, x.extra.0, x.extra.1),
+ Err(e) => {
+ log::error!("Failed to handle encryption handshake: {:?}", e.extra);
+ return None;
+ }
+ };
+ match send_packet(to_send, buf, socket, None).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send encryption ack packet: {}", e);
+ return None;
+ }
+ }
+ // pre-auth
+ let handshake = handshake.with_auth(AuthImpl);
+ // authenticate
+ log::debug!("(connect) Handling third packet");
+ let mut packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) auth packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet3 {
+ handle_ping(ping, buf, socket).await;
+ packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) auth packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ let to_send = match handshake.authenticate(&packet3, crypto.clone()) {
+ Ok(x) => x,
+ Err(h) => match h.extra {
+ polariton_auth::AuthError::Validation(e) => {
+ e.log_err();
+ return None;
+ },
+ e => {
+ log::error!("Failed to handle auth handshake: {:?}", e);
+ return None;
+ },
+ },
+ };
+ match send_packet(to_send, buf, socket, Some(crypto.clone())).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send auth ack packet: {}", e);
+ return None;
+ }
+ }
+
+ // join lobby
+ log::debug!("(join lobby) Handling fourth packet");
+ let mut packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) join packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet_j {
+ handle_ping(ping, buf, socket).await;
+ packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) join packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ if let Packet::Packet(msg) = &packet_j {
+ if let Message::Standard(st) = &msg.message {
+ if let Data::OpReq(req) = &st.data {
+ if req.code == 226 { // join lobby (but for real this time)
+ let mut params = std::collections::HashMap::::new();
+ //params.insert(252 /* actors in game */, Typed::Str(game_server_url.into()));
+ params.insert(254 /* game server address */, Typed::Int(42));
+ params.insert(249 /* actor properties */, Typed::HashMap(Vec::new().into()));
+ params.insert(248 /* game properties */, Typed::HashMap(Vec::new().into()));
+ let resp = Packet::from_message(
+ Message::Standard(
+ StandardMessage { flags: 0,
+ data: Data::OpResp(OperationResponse {
+ code: req.code,
+ return_code: 0,
+ message: Typed::Null,
+ params: params.into(),
+ }),
+ }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap();
+ match send_packet(resp, buf, socket, Some(crypto.clone())).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send lobby ack packet: {}", e);
+ return None;
+ }
+ }
+ }
+ }
+ }
+ }
+ buf.clear();
+
+ Some(crypto)
+}
diff --git a/rc_chat_room/src/operations/clan_invite.rs b/rc_chat_room/src/operations/clan_invite.rs
new file mode 100644
index 0000000..f779b9c
--- /dev/null
+++ b/rc_chat_room/src/operations/clan_invite.rs
@@ -0,0 +1,26 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Arr};
+
+use crate::data::clan_invite::*;
+
+const PARAM_KEY: u8 = 42;
+
+pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Arr(Arr {
+ ty: 104, // hashmap
+ items: vec![
+ ClanInviteInfo {
+ username: "RE_user1".to_owned(),
+ display_name: "RE_user1".to_owned(),
+ clan_name: "RE_clan1".to_owned(),
+ clan_size: 42,
+ use_custom_avatar: false,
+ avatar_id: 0,
+ }.as_transmissible()
+ ],
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_chat_room/src/operations/friend_list.rs b/rc_chat_room/src/operations/friend_list.rs
new file mode 100644
index 0000000..2669c8a
--- /dev/null
+++ b/rc_chat_room/src/operations/friend_list.rs
@@ -0,0 +1,29 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Arr};
+
+use crate::data::friend::*;
+
+const FRIENDS_PARAM_KEY: u8 = 5;
+const AVATAR_PARAM_KEY: u8 = 76;
+
+pub(super) fn friends_provider() -> SimpleFunc<4, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(FRIENDS_PARAM_KEY, Typed::Arr(Arr {
+ ty: 99, // custom
+ items: vec![Typed::Custom(vec![ // FIXME don't manually serialize
+ 0u8, // byte custom type
+ 0u8, 5u8, // short custom object size
+ 3u8, 0u8, 0u8, 0u8, 0u8, // content
+ ].into())] }));
+ params.insert(AVATAR_PARAM_KEY, Typed::Arr(Arr {
+ ty: 104, // hashmap
+ items: vec![AvatarInfo {
+ name: "".to_string(),
+ use_custom_avatar: false,
+ avatar_id: 1,
+ }.as_transmissible()],
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_chat_room/src/operations/mod.rs b/rc_chat_room/src/operations/mod.rs
new file mode 100644
index 0000000..47bd1d3
--- /dev/null
+++ b/rc_chat_room/src/operations/mod.rs
@@ -0,0 +1,17 @@
+mod more_auth;
+mod friend_list;
+mod settings;
+mod clan_invite;
+
+use polariton_server::operations::OperationsHandler;
+
+pub fn handler() -> OperationsHandler {
+ OperationsHandler::new()
+ .without_state(more_auth::MoreLobbyAuth)
+ .without_state(polariton_server::operations::Ack::<33, _>::default()) // get user clan info (this is equivalent to not being in a clan)
+ .without_state(friend_list::friends_provider()) // TODO friend object parsing Token: 0x0200169C RID: 5788
+ .without_state(settings::settings_provider()) // TODO save settings persistently
+ .without_state(polariton_server::operations::Ack::<43, _>::default()) // get my clan info (this is equivalent to not being in a clan)
+ .without_state(clan_invite::clan_invites_provider())
+ .without_state(polariton_server::operations::Ack::<19, _>::default()) // get pending platoon invite (this is equivalent to having no pending invite)
+}
diff --git a/rc_chat_room/src/operations/more_auth.rs b/rc_chat_room/src/operations/more_auth.rs
new file mode 100644
index 0000000..b154590
--- /dev/null
+++ b/rc_chat_room/src/operations/more_auth.rs
@@ -0,0 +1,42 @@
+use polariton::operation::Typed;
+use polariton_server::operations::{Operation, OperationCode};
+
+pub struct MoreLobbyAuth;
+
+impl MoreLobbyAuth {
+ const AUTH_PAYLOAD_KEY: u8 = 245;
+}
+
+impl Operation for MoreLobbyAuth {
+ type State = ();
+ type User = crate::UserTy;
+
+ fn handle(&self, params: polariton::operation::ParameterTable, _: &mut Self::State, user: &Self::User) -> polariton::operation::OperationResponse {
+ let params_dict = params.to_dict();
+ if let Some(Typed::Str(auth_payload)) = params_dict.get(&Self::AUTH_PAYLOAD_KEY) {
+ let mut write_lock = user.write().unwrap();
+ if write_lock.update_with_auth(&auth_payload.string) {
+ let mut resp_params = std::collections::HashMap::new();
+ resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0));
+ return polariton::operation::OperationResponse {
+ code: 230,
+ return_code: 0,
+ message: polariton::operation::Typed::Null,
+ params: resp_params.into(),
+ }
+ }
+ }
+ polariton::operation::OperationResponse {
+ code: 230,
+ return_code: 120,
+ message: polariton::operation::Typed::Null,
+ params: std::collections::HashMap::new().into(),
+ }
+ }
+}
+
+impl OperationCode for MoreLobbyAuth {
+ fn op_code() -> u8 {
+ 230
+ }
+}
diff --git a/rc_chat_room/src/operations/platoon_invite.rs b/rc_chat_room/src/operations/platoon_invite.rs
new file mode 100644
index 0000000..8a6664d
--- /dev/null
+++ b/rc_chat_room/src/operations/platoon_invite.rs
@@ -0,0 +1,20 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Arr};
+
+use crate::data::friend::*;
+
+const INVITER_NAME_PARAM_KEY: u8 = 19;
+const INVITER_DISPLAY_NAME_PARAM_KEY: u8 = 75;
+const INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY: u8 = 13;
+const INVITER_AVATAR_ID_NAME_PARAM_KEY: u8 = 14;
+
+pub(super) fn platoon_pending_provider() -> SimpleFunc<19, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(INVITER_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter".into())));
+ params.insert(INVITER_DISPLAY_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter_display".into())));
+ params.insert(INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY, Typed::Bool(false.into())));
+ params.insert(INVITER_AVATAR_ID_NAME_PARAM_KEY, Typed::Int(1)));
+ Ok(params.into())
+ })
+}
diff --git a/rc_chat_room/src/operations/settings.rs b/rc_chat_room/src/operations/settings.rs
new file mode 100644
index 0000000..469d81d
--- /dev/null
+++ b/rc_chat_room/src/operations/settings.rs
@@ -0,0 +1,16 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Dict};
+
+const PARAM_KEY: u8 = 30;
+
+pub(super) fn settings_provider() -> SimpleFunc<24, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Dict(Dict {
+ key_ty: 115,
+ val_ty: 42,
+ items: Vec::default(),
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_chat_room/src/state.rs b/rc_chat_room/src/state.rs
new file mode 100644
index 0000000..7adeeb0
--- /dev/null
+++ b/rc_chat_room/src/state.rs
@@ -0,0 +1,43 @@
+use std::sync::{Arc, RwLock};
+
+pub struct State {
+ pub crypto: Box>,
+}
+
+impl State {
+ pub fn new(c: Box>) -> Self {
+ Self {
+ crypto: c,
+ }
+ }
+
+ pub fn binrw_args(&self) -> polariton::packet::WriteArgs {
+ Some(self.crypto.clone())
+ }
+
+ pub fn user(&self) -> crate::UserTy {
+ RwLock::new(UserState::default())
+ }
+}
+
+#[derive(Default, Debug)]
+pub struct UserState {
+ pub uuid: String,
+ pub token: String,
+ pub refresh_token: String,
+}
+
+impl UserState {
+ pub fn update_with_auth(&mut self, auth_str: &str) -> bool {
+ let splits: Vec<&str> = auth_str.split(';').collect();
+ if splits.len() != 3 {
+ log::warn!("Invalid auth payload: {}", auth_str);
+ false
+ } else {
+ self.uuid = splits[0].to_owned();
+ self.token = splits[1].to_owned();
+ self.refresh_token = splits[2].to_owned();
+ true
+ }
+ }
+}
diff --git a/rc_services_room/src/data/custom_games.rs b/rc_services_room/src/data/custom_games.rs
new file mode 100644
index 0000000..2a79437
--- /dev/null
+++ b/rc_services_room/src/data/custom_games.rs
@@ -0,0 +1,33 @@
+#![allow(dead_code)]
+
+#[repr(u8)]
+pub enum GameMode {
+ BattleArena = 0,
+ SuddenDeath = 1,
+ Pit = 2,
+ TestMode = 3,
+ SinglePlayer = 4,
+ TeamDeathmatch = 5,
+ Campaign = 6,
+}
+
+impl GameMode {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ GameMode::BattleArena => "BattleArena",
+ GameMode::SuddenDeath => "SuddenDeath",
+ GameMode::Pit => "Pit",
+ GameMode::TestMode => "TestMode",
+ GameMode::SinglePlayer => "SinglePlayerTDM",
+ GameMode::TeamDeathmatch => "TeamDeathmatch",
+ GameMode::Campaign => "Campaign",
+ }
+ }
+}
+
+#[repr(u8)]
+pub enum MapVisibility {
+ Good = 0,
+ Poor = 1,
+ Bad = 2, // VeryPoor
+}
diff --git a/rc_services_room/src/data/garage_bay.rs b/rc_services_room/src/data/garage_bay.rs
new file mode 100644
index 0000000..3a68723
--- /dev/null
+++ b/rc_services_room/src/data/garage_bay.rs
@@ -0,0 +1,93 @@
+use polariton::operation::{Typed, Arr};
+
+#[allow(dead_code)]
+#[derive(Copy, Clone)]
+pub enum MovementCategory {
+ NotAFunctionalItem = 0,
+ Wheel = 1,
+ Hover = 2,
+ Wing = 3,
+ Rudder = 4,
+ Thruster = 5,
+ InsectLeg = 6,
+ MechLeg = 7,
+ Ski = 8,
+ TankTrack = 9,
+ Rotor = 10,
+ SprinterLeg = 11,
+ Propeller = 12,
+ Laser = 100,
+ Plasma = 200,
+ Mortar = 250,
+ Rail = 300,
+ Nano = 400,
+ Tesla = 500,
+ Aeroflak = 600,
+ Ion = 650,
+ Seeker = 701,
+ Chaingun = 750,
+ ShieldModule = 800,
+ GhostModule = 801,
+ BlinkModule = 802,
+ EmpModule = 803,
+ WindowmakerModule = 804,
+ EnergyModule = 900,
+}
+
+pub struct GarageSlotInfo {
+ pub name: String,
+ pub cubes: u32,
+ pub crf_id: u32, // 0 means not uploaded
+ pub was_rated: bool, // ignored when not on CRF
+ pub movement_categories: Vec,
+ pub uuid: (u32, u32),
+ pub thumbnail_version: u32,
+ pub total_robot_cpu: u32,
+ pub total_cosmetic_cpu: u32,
+ pub total_robot_ranking: u32,
+ pub bay_cpu: u32,
+ pub tutorial_robot: bool, // assumed to be false (when omitted)
+ pub starter_robot_index: i32, // assumed to be -1 (whem omitted)
+ pub control_type: i32, // enum???
+ pub control_options: Vec,
+ pub mastery_level: i32,
+ pub bay_skin_id: String,
+ pub weapon_order: Vec,
+}
+
+impl GarageSlotInfo {
+ pub fn as_transmissible(&self) -> Typed {
+ Typed::HashMap(vec![
+ (Typed::Str("name".into()), Typed::Str(self.name.clone().into())),
+ (Typed::Str("numberCubes".into()), Typed::Int(self.cubes as i32)),
+ (Typed::Str("crfId".into()), Typed::Int(self.crf_id as i32)),
+ (Typed::Str("wasRated".into()), Typed::Bool(self.was_rated.into())),
+ (Typed::Str("movementCategories".into()), Typed::Arr(Arr {
+ ty: 105, // int
+ items: self.movement_categories.iter().map(|x| Typed::Int((*x as i32) * 100_000)).collect(),
+ })),
+ (Typed::Str("uniqueId1".into()), Typed::Int(self.uuid.0 as i32)),
+ (Typed::Str("uniqueId2".into()), Typed::Int(self.uuid.1 as i32)),
+ (Typed::Str("thumbnailVersion".into()), Typed::Int(self.thumbnail_version as i32)),
+ (Typed::Str("totalRobotCPU".into()), Typed::Int(self.total_robot_cpu as i32)),
+ (Typed::Str("totalCosmeticCPU".into()), Typed::Int(self.total_cosmetic_cpu as i32)),
+ (Typed::Str("totalRobotRanking".into()), Typed::Int(self.total_robot_ranking as i32)),
+ (Typed::Str("bayCpu".into()), Typed::Int(self.bay_cpu as i32)),
+ (Typed::Str("tutorialRobot".into()), Typed::Bool(self.tutorial_robot.into())),
+ (Typed::Str("starterRobotIndex".into()), Typed::Int(self.starter_robot_index)),
+ (Typed::Str("controlType".into()), Typed::Int(self.control_type)),
+ (Typed::Str("controlOptions".into()), Typed::Arr(Arr {
+ ty: 111, // bool
+ items: self.control_options.iter().map(|&x| Typed::Bool(x.into())).collect(),
+ })),
+ (Typed::Str("masteryLevel".into()), Typed::Int(self.mastery_level)),
+ (Typed::Str("baySkinId".into()), Typed::Str(self.bay_skin_id.clone().into())),
+ (Typed::Str("weaponOrder".into()), Typed::Arr(Arr {
+ ty: 105, // int
+ items: self.weapon_order.iter().map(|x| Typed::Int(*x)).collect(),
+ })),
+ ].into())
+ }
+}
+
+
diff --git a/rc_services_room/src/data/item_shop_bundle.rs b/rc_services_room/src/data/item_shop_bundle.rs
new file mode 100644
index 0000000..c267c34
--- /dev/null
+++ b/rc_services_room/src/data/item_shop_bundle.rs
@@ -0,0 +1,124 @@
+#![allow(dead_code)]
+
+use std::io::Write;
+
+use polariton::operation::Typed;
+
+pub struct ItemShopBundle {
+ pub sku: String,
+ pub bundle_name_key: String,
+ pub sprite: String,
+ pub is_sprite_full_size: bool,
+ pub category: ItemShopCategory,
+ pub currency: CurrencyType, // str
+ pub price: i32,
+ pub discount_time: i64, // seconds since unix epoch
+ pub discount_price: i32,
+ pub recurrence: ItemShopRecurrence,
+ pub owns_required_cube: bool,
+ //pub is_discounted: bool,
+ pub is_limited_edition: bool,
+}
+
+impl ItemShopBundle {
+ pub fn as_transmissible(&self) -> Typed {
+ let mut buf = Vec::new();
+ let mut writer = std::io::Cursor::new(&mut buf);
+ self.dump(&mut writer).unwrap();
+ Typed::Bytes(buf.into())
+ }
+
+ fn dump(&self, writer: &mut dyn Write) -> std::io::Result {
+ let sku_bytes = self.sku.as_bytes();
+ let mut total_len = writer.write(&encode_7_bit_i32(sku_bytes.len() as i32))?;
+ total_len += writer.write(sku_bytes)?;
+
+ let bundle_name_key_bytes = self.bundle_name_key.as_bytes();
+ total_len += writer.write(&encode_7_bit_i32(bundle_name_key_bytes.len() as i32))?;
+ total_len += writer.write(bundle_name_key_bytes)?;
+
+ let sprite_bytes = self.sprite.as_bytes();
+ total_len += writer.write(&encode_7_bit_i32(sprite_bytes.len() as i32))?;
+ total_len += writer.write(sprite_bytes)?;
+
+ total_len += writer.write(&[self.is_sprite_full_size as u8])?;
+
+ let currency_bytes = self.currency.as_str().as_bytes();
+ total_len += writer.write(&encode_7_bit_i32(currency_bytes.len() as i32))?;
+ total_len += writer.write(currency_bytes)?;
+
+ total_len += writer.write(&self.price.to_le_bytes())?;
+
+ total_len += writer.write(&self.discount_time.to_le_bytes())?;
+
+ total_len += writer.write(&self.discount_price.to_le_bytes())?;
+
+ total_len += writer.write(&(self.recurrence as i32).to_le_bytes())?;
+
+ total_len += writer.write(&[self.owns_required_cube as u8])?;
+
+ total_len += writer.write(&(self.category as i32).to_le_bytes())?;
+
+ total_len += writer.write(&[self.is_limited_edition as u8])?;
+
+ Ok(total_len)
+ }
+
+ pub fn as_transmissible_vec(items: Vec) -> Typed {
+ let mut buf = Vec::new();
+ let mut writer = std::io::Cursor::new(&mut buf);
+ writer.write(&(items.len() as i32).to_le_bytes()).unwrap();
+ for item in items.iter() {
+ item.dump(&mut writer).unwrap();
+ }
+ Typed::Bytes(buf.into())
+ }
+}
+
+#[repr(i32)]
+#[derive(Copy, Clone)]
+pub enum ItemShopCategory {
+ Cube = 0,
+ GarageBaySkin = 1,
+ Bundle = 2,
+ DeathEffect = 3,
+ SpawnEffect = 4,
+ Emotigram = 5,
+}
+
+#[repr(i32)]
+#[derive(Copy, Clone)]
+pub enum ItemShopRecurrence {
+ Daily = 0,
+ Weekly = 1,
+}
+
+#[repr(i32)]
+#[derive(Copy, Clone)]
+pub enum CurrencyType {
+ Robits = 0,
+ CosmeticCredits = 1,
+}
+
+impl CurrencyType {
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Self::Robits => "Robits",
+ Self::CosmeticCredits => "CosmeticCredits",
+ }
+ }
+}
+
+fn encode_7_bit_i32(mut src: i32) -> Vec {
+ let mut out = Vec::with_capacity(5);
+ while src != 0 {
+ let last_7 = (src & 0x7F) as u8;
+ src = src >> 7;
+ if src != 0 {
+ out.push(last_7 | 0x80);
+ } else {
+ out.push(last_7);
+ }
+ }
+ out
+}
diff --git a/rc_services_room/src/data/mod.rs b/rc_services_room/src/data/mod.rs
index 5d19c43..4bb7a7a 100644
--- a/rc_services_room/src/data/mod.rs
+++ b/rc_services_room/src/data/mod.rs
@@ -12,3 +12,7 @@ pub mod cpu_limits;
pub mod cosmetic_limits;
pub mod taunts_config;
pub mod customisation_info;
+pub mod garage_bay;
+pub mod custom_games;
+pub mod tech_tree;
+pub mod item_shop_bundle;
diff --git a/rc_services_room/src/data/tech_tree.rs b/rc_services_room/src/data/tech_tree.rs
new file mode 100644
index 0000000..832f6a1
--- /dev/null
+++ b/rc_services_room/src/data/tech_tree.rs
@@ -0,0 +1,28 @@
+use polariton::operation::{Typed, Arr};
+
+pub struct TechTreeNode {
+ pub main_cube_id: i32, // hex
+ pub position_x: i32,
+ pub position_y: i32,
+ pub is_unlocked: bool,
+ pub is_unlockable: bool,
+ pub tech_points: u32,
+ pub neighbours: Vec, // cube IDs, hex
+}
+
+impl TechTreeNode {
+ pub fn as_transmissible(&self) -> Typed {
+ Typed::HashMap(vec![
+ (Typed::Str("mainCubeId".into()), Typed::Str(hex::encode(self.main_cube_id.to_le_bytes()).into())),
+ (Typed::Str("positionX".into()), Typed::Int(self.position_x)),
+ (Typed::Str("positionY".into()), Typed::Int(self.position_y)),
+ (Typed::Str("isUnlocked".into()), Typed::Bool(self.is_unlocked.into())),
+ (Typed::Str("isUnlockable".into()), Typed::Bool(self.is_unlockable.into())),
+ (Typed::Str("tp".into()), Typed::Int(self.tech_points as i32)),
+ (Typed::Str("neighbours".into()), Typed::Arr(Arr {
+ ty: 115, // str
+ items: self.neighbours.iter().map(|cube_id| Typed::Str(hex::encode(cube_id.to_le_bytes()).into())).collect(),
+ })),
+ ].into())
+ }
+}
diff --git a/rc_services_room/src/main.rs b/rc_services_room/src/main.rs
index 52b2619..381a9a2 100644
--- a/rc_services_room/src/main.rs
+++ b/rc_services_room/src/main.rs
@@ -60,6 +60,9 @@ async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAdd
match packet {
Packet::Ping(ping) => {
handle_ping(ping, &mut write_buf, &mut socket).await;
+ for _ in 0..5 {
+ read_buf.remove(0);
+ }
},
Packet::Packet(packet) => {
// remove packet's advertised size from the buffer
diff --git a/rc_services_room/src/operations/avatar_info.rs b/rc_services_room/src/operations/avatar_info.rs
new file mode 100644
index 0000000..0d5dd99
--- /dev/null
+++ b/rc_services_room/src/operations/avatar_info.rs
@@ -0,0 +1,14 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const IS_CUSTOM_PARAM_KEY: u8 = 130;
+const AVATAR_ID_PARAM_KEY: u8 = 129;
+
+pub(super) fn get_avatar_provider() -> SimpleFunc<110, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(IS_CUSTOM_PARAM_KEY, Typed::Bool(false.into()));
+ params.insert(AVATAR_ID_PARAM_KEY, Typed::Int(1));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/balance_info.rs b/rc_services_room/src/operations/balance_info.rs
new file mode 100644
index 0000000..303134e
--- /dev/null
+++ b/rc_services_room/src/operations/balance_info.rs
@@ -0,0 +1,14 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const FREE_BALANCE_PARAM_KEY: u8 = 74;
+const PAID_BALANCE_PARAM_KEY: u8 = 87;
+
+pub(super) fn balance_wallet_provider() -> SimpleFunc<66, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(FREE_BALANCE_PARAM_KEY, Typed::Long(31337_000));
+ params.insert(PAID_BALANCE_PARAM_KEY, Typed::Long(1));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/cube_inventory.rs b/rc_services_room/src/operations/cube_inventory.rs
new file mode 100644
index 0000000..1ba0f38
--- /dev/null
+++ b/rc_services_room/src/operations/cube_inventory.rs
@@ -0,0 +1,17 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Dict};
+
+const PARAM_KEY: u8 = 16;
+
+pub(super) fn cube_inv_provider() -> SimpleFunc<16, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Dict(Dict {
+ key_ty: 105, // int
+ val_ty: 105, // int
+ items: vec![
+ (Typed::Int(0), Typed::Int(99)),
+ ] }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/custom_game_session.rs b/rc_services_room/src/operations/custom_game_session.rs
new file mode 100644
index 0000000..316b1de
--- /dev/null
+++ b/rc_services_room/src/operations/custom_game_session.rs
@@ -0,0 +1,13 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const RESPONSE_CODE_PARAM_KEY: u8 = 168;
+//const CUSTOM_GAME_DATA_PARAM_KEY: u8 = 169;
+
+pub(super) fn get_custom_session_provider() -> SimpleFunc<144, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(0 /* Not in any session */));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/custom_games_maps.rs b/rc_services_room/src/operations/custom_games_maps.rs
new file mode 100644
index 0000000..b2e30cf
--- /dev/null
+++ b/rc_services_room/src/operations/custom_games_maps.rs
@@ -0,0 +1,53 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Dict, Arr};
+
+use crate::data::custom_games::*;
+
+const MODE_MAP_PARAM_KEY: u8 = 170;
+const MAP_NAMES_PARAM_KEY: u8 = 178;
+
+pub(super) fn allowed_maps_provider() -> SimpleFunc<146, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(MODE_MAP_PARAM_KEY, Typed::Dict(Dict {
+ key_ty: 115, // str
+ val_ty: 121, // arr
+ items: vec![
+ (Typed::Str(GameMode::BattleArena.as_str().into()), Typed::Arr(Arr {
+ ty: 115, // str
+ items: vec![
+ Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_02_BA".into()),
+ Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_03_BA".into()),
+ Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_02_BA".into()),
+ Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_02_BA".into()),
+ Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_01_BA".into()),
+ Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_03_BA".into()),
+ ]
+ })),
+ (Typed::Str(GameMode::TeamDeathmatch.as_str().into()), Typed::Arr(Arr {
+ ty: 115, // str
+ items: vec![
+ Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_01_CTF".into()),
+ Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_01_CTF".into()),
+ ]
+ })),
+ ],
+ }));
+ params.insert(MAP_NAMES_PARAM_KEY, Typed::Dict(Dict {
+ key_ty: 115, // str
+ val_ty: 115, // str
+ items: vec![
+ (Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_02_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Neptune_02_BA".into())),
+ (Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_01_CTF".into()), Typed::Str("strCustomGameMapNameRC_Planet_Neptune_01_CTF".into())),
+ (Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_03_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Mars_03_BA".into())),
+ (Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_02_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Mars_02_BA".into())),
+ (Typed::Str("Assets/Scenes/Planet_Mars/RC_Planet_Mars_01_CTF".into()), Typed::Str("strCustomGameMapNameRC_Planet_Mars_01_CTF".into())),
+ (Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_02_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Earth_02_BA".into())),
+ (Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_01_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Earth_01_BA".into())),
+ (Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_03_BA".into()), Typed::Str("strCustomGameMapNameRC_Planet_Neptune_03_BA".into())),
+ (Typed::Str("Assets/Scenes/Planet_Test/TestRobot".into()), Typed::Str("TestRobot".into())),
+ ],
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/dev_message.rs b/rc_services_room/src/operations/dev_message.rs
new file mode 100644
index 0000000..c87898b
--- /dev/null
+++ b/rc_services_room/src/operations/dev_message.rs
@@ -0,0 +1,12 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const PARAM_KEY: u8 = 2;
+
+pub(super) fn dev_message_provider() -> SimpleFunc<8, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Bytes(Vec::from("No jam was harmed in the reverse-engineering of this game".as_bytes()).into()));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/game_event_params.rs b/rc_services_room/src/operations/game_event_params.rs
new file mode 100644
index 0000000..89d02cb
--- /dev/null
+++ b/rc_services_room/src/operations/game_event_params.rs
@@ -0,0 +1,46 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Arr};
+
+use crate::data::custom_games::*;
+
+const MAP_NAMES_PARAM_KEY: u8 = 78;
+const VISIBILITY_PARAM_KEY: u8 = 66;
+const MODE_PARAM_KEY: u8 = 136;
+const AUTO_HEAL_PARAM_KEY: u8 = 37;
+const REMAINING_TICKS_PARAM_KEY: u8 = 145;
+
+pub(super) fn event_system_params_provider() -> SimpleFunc<24, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(MAP_NAMES_PARAM_KEY, Typed::Arr(Arr {
+ ty: 115, // str
+ items: vec![
+ Typed::Str("Assets/Scenes/Planet_Neptune/RC_Planet_Neptune_03_BA".into()),
+ Typed::Str("Assets/Scenes/Planet_Earth/RC_Planet_Earth_01_BA".into()),
+ ],
+ }));
+ params.insert(VISIBILITY_PARAM_KEY, Typed::Arr(Arr {
+ ty: 105, // int
+ items: vec![
+ Typed::Int(GameMode::BattleArena as _),
+ Typed::Int(GameMode::BattleArena as _),
+ ],
+ }));
+ params.insert(MODE_PARAM_KEY, Typed::Arr(Arr {
+ ty: 105, // int
+ items: vec![
+ Typed::Int(MapVisibility::Good as _),
+ Typed::Int(MapVisibility::Bad as _),
+ ],
+ }));
+ params.insert(AUTO_HEAL_PARAM_KEY, Typed::Arr(Arr {
+ ty: 111, // bool
+ items: vec![
+ Typed::Bool(true.into()),
+ Typed::Bool(false.into()),
+ ],
+ }));
+ params.insert(REMAINING_TICKS_PARAM_KEY, Typed::Long(1_000_000));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/garage_bay_uuid.rs b/rc_services_room/src/operations/garage_bay_uuid.rs
new file mode 100644
index 0000000..fbe9495
--- /dev/null
+++ b/rc_services_room/src/operations/garage_bay_uuid.rs
@@ -0,0 +1,12 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const PARAM_KEY: u8 = 54;
+
+pub(super) fn garage_id_provider() -> SimpleFunc<177, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Str(format!("{}_{}", 12345, 54321).into()));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/garage_slots.rs b/rc_services_room/src/operations/garage_slots.rs
new file mode 100644
index 0000000..9309a8c
--- /dev/null
+++ b/rc_services_room/src/operations/garage_slots.rs
@@ -0,0 +1,46 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Dict, Arr};
+
+use crate::data::garage_bay::*;
+
+const SLOTS_PARAM_KEY: u8 = 44;
+const SELECTED_SLOT_PARAM_KEY: u8 = 43;
+const SLOT_ORDER_PARAM_KEY: u8 = 58;
+
+pub(super) fn garage_slot_provider() -> SimpleFunc<40, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(SLOTS_PARAM_KEY, Typed::Dict(Dict {
+ key_ty: 105, // int
+ val_ty: 104, // hashmap
+ items: vec![
+ (Typed::Int(0), GarageSlotInfo {
+ name: "Reverse-engineer great success!".to_owned(),
+ cubes: 1,
+ crf_id: 0,
+ was_rated: false,
+ movement_categories: vec![MovementCategory::Wheel],
+ uuid: (2,4),
+ thumbnail_version: 0,
+ total_robot_cpu: 1,
+ total_cosmetic_cpu: 0,
+ total_robot_ranking: 1,
+ bay_cpu: 2_000,
+ tutorial_robot: false,
+ starter_robot_index: -1,
+ control_type: 0,
+ control_options: vec![false, false],
+ mastery_level: 1,
+ bay_skin_id: "".to_owned(), // TODO
+ weapon_order: vec![0],
+ }.as_transmissible())
+ ],
+ }));
+ params.insert(SELECTED_SLOT_PARAM_KEY, Typed::Int(0));
+ params.insert(SLOT_ORDER_PARAM_KEY, Typed::Arr(Arr {
+ ty: 105, // int
+ items: vec![Typed::Int(0)],
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/garage_upgrades.rs b/rc_services_room/src/operations/garage_upgrades.rs
new file mode 100644
index 0000000..23ecba0
--- /dev/null
+++ b/rc_services_room/src/operations/garage_upgrades.rs
@@ -0,0 +1,25 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Dict};
+
+const PARAM_KEY: u8 = 1;
+
+pub(super) fn garage_upgrades_provider() -> SimpleFunc<1, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::HashMap(vec![
+ (Typed::Str("cpuIncreaseCost".into()), Typed::Dict(Dict {
+ key_ty: 110, // int
+ val_ty: 110, // int
+ items: vec![
+ // (CPU limit, upgrade cost)
+ (Typed::Int(100), Typed::Int(100)),
+ (Typed::Int(200), Typed::Int(200)),
+ (Typed::Int(1_000), Typed::Int(1_000)),
+ (Typed::Int(2_000), Typed::Int(2_000)), // max regular bot CPU
+ (Typed::Int(10_000), Typed::Int(10_000)), // max mega bot cpu
+ ],
+ }))
+ ].into()));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/item_shop_bundles.rs b/rc_services_room/src/operations/item_shop_bundles.rs
new file mode 100644
index 0000000..5147cd8
--- /dev/null
+++ b/rc_services_room/src/operations/item_shop_bundles.rs
@@ -0,0 +1,29 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::ParameterTable;
+
+use crate::data::item_shop_bundle::*;
+
+const PARAM_KEY: u8 = 65;
+
+pub(super) fn item_bundle_provider() -> SimpleFunc<188, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, ItemShopBundle::as_transmissible_vec(vec![
+ ItemShopBundle {
+ sku: "12345".to_owned(),
+ bundle_name_key: "RE_todo_item_shop_bundle_name_key".to_owned(),
+ sprite: "RE_todo_item_shop_sprite_name".to_owned(),
+ is_sprite_full_size: true,
+ category: ItemShopCategory::Cube,
+ currency: CurrencyType::Robits,
+ price: 10_000,
+ discount_time: 1,
+ discount_price: 5_000,
+ recurrence: ItemShopRecurrence::Daily,
+ owns_required_cube: true,
+ is_limited_edition: true,
+ }
+ ]));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs
index 9ac2fc3..b540daf 100644
--- a/rc_services_room/src/operations/mod.rs
+++ b/rc_services_room/src/operations/mod.rs
@@ -22,6 +22,27 @@ mod cpu_limits_config;
mod cosmetic_config;
mod taunts_config;
mod all_customisations_info;
+// some social requests must complete here
+mod tech_points;
+mod cube_inventory;
+mod player_level;
+mod balance_info;
+mod premium_duration;
+mod tutorial_status;
+mod user_perms;
+mod garage_slots;
+mod robopass_season;
+mod owned_cosmetics;
+mod dev_message;
+mod custom_games_maps;
+mod avatar_info;
+mod custom_game_session;
+mod user_xp;
+mod garage_upgrades;
+mod game_event_params;
+mod garage_bay_uuid;
+mod tech_tree_data;
+mod item_shop_bundles;
use polariton_server::operations::OperationsHandler;
@@ -53,5 +74,26 @@ pub fn handler() -> OperationsHandler {
.without_state(cosmetic_config::cosmetic_limits_config_provider())
.without_state(taunts_config::taunts_config_provider())
.without_state(all_customisations_info::all_customisations_provider())
+ .without_state(tech_points::tech_points_provider())
+ .without_state(cube_inventory::cube_inv_provider())
+ .without_state(player_level::player_level_info_provider())
+ .without_state(balance_info::balance_wallet_provider())
+ .without_state(premium_duration::premium_remaining_provider())
+ .without_state(tutorial_status::tutorial_info_provider())
+ .without_state(user_perms::user_rights_provider())
+ .without_state(garage_slots::garage_slot_provider())
+ .without_state(robopass_season::robopass_season_provider())
+ .without_state(owned_cosmetics::owned_cosmetics_provider())
+ .without_state(owned_cosmetics::selected_cosmetics_provider())
+ .without_state(dev_message::dev_message_provider())
+ .without_state(custom_games_maps::allowed_maps_provider())
+ .without_state(avatar_info::get_avatar_provider())
+ .without_state(custom_game_session::get_custom_session_provider())
+ .without_state(user_xp::get_user_xp_provider())
+ .without_state(garage_upgrades::garage_upgrades_provider())
+ .without_state(game_event_params::event_system_params_provider())
+ .without_state(garage_bay_uuid::garage_id_provider())
+ .without_state(tech_tree_data::tech_tree_layout_provider())
+ .without_state(item_shop_bundles::item_bundle_provider())
//.without_state(polariton_server::operations::Ack::<70, _>::default())
}
diff --git a/rc_services_room/src/operations/owned_cosmetics.rs b/rc_services_room/src/operations/owned_cosmetics.rs
new file mode 100644
index 0000000..95d82d8
--- /dev/null
+++ b/rc_services_room/src/operations/owned_cosmetics.rs
@@ -0,0 +1,26 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Arr};
+
+const PARAM_KEY: u8 = 50;
+
+pub(super) fn owned_cosmetics_provider() -> SimpleFunc<23, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Arr(Arr {
+ ty: 115, // str
+ items: vec![Typed::Str("1".into())],
+ }));
+ Ok(params.into())
+ })
+}
+
+pub(super) fn selected_cosmetics_provider() -> SimpleFunc<21, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Arr(Arr {
+ ty: 115, // str
+ items: vec![Typed::Str("1".into())],
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/player_level.rs b/rc_services_room/src/operations/player_level.rs
new file mode 100644
index 0000000..9e8fd6f
--- /dev/null
+++ b/rc_services_room/src/operations/player_level.rs
@@ -0,0 +1,18 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Dict};
+
+const PARAM_KEY: u8 = 1;
+
+pub(super) fn player_level_info_provider() -> SimpleFunc<3, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Dict(Dict {
+ key_ty: 105, // int
+ val_ty: 105, // int
+ items: vec![
+ (Typed::Int(0), Typed::Int(99)),
+ (Typed::Int(10_000), Typed::Int(99_000)),
+ ] }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/premium_duration.rs b/rc_services_room/src/operations/premium_duration.rs
new file mode 100644
index 0000000..a2ff27c
--- /dev/null
+++ b/rc_services_room/src/operations/premium_duration.rs
@@ -0,0 +1,20 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const DAYS_PARAM_KEY: u8 = 8;
+const HOURS_PARAM_KEY: u8 = 13;
+const MINUTES_PARAM_KEY: u8 = 14;
+const SECONDS_PARAM_KEY: u8 = 15;
+const LIFETIME_PARAM_KEY: u8 = 150;
+
+pub(super) fn premium_remaining_provider() -> SimpleFunc<15, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(DAYS_PARAM_KEY, Typed::Int(0));
+ params.insert(HOURS_PARAM_KEY, Typed::Int(0));
+ params.insert(MINUTES_PARAM_KEY, Typed::Int(0));
+ params.insert(SECONDS_PARAM_KEY, Typed::Int(0));
+ params.insert(LIFETIME_PARAM_KEY, Typed::Bool(false.into()));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/robopass_season.rs b/rc_services_room/src/operations/robopass_season.rs
new file mode 100644
index 0000000..cb7729e
--- /dev/null
+++ b/rc_services_room/src/operations/robopass_season.rs
@@ -0,0 +1,12 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const PARAM_KEY: u8 = 1;
+
+pub(super) fn robopass_season_provider() -> SimpleFunc<108, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Null);
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/tech_points.rs b/rc_services_room/src/operations/tech_points.rs
new file mode 100644
index 0000000..251a3dc
--- /dev/null
+++ b/rc_services_room/src/operations/tech_points.rs
@@ -0,0 +1,12 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const PARAM_KEY: u8 = 214;
+
+pub(super) fn tech_points_provider() -> SimpleFunc<187, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Int(1337));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/tech_tree_data.rs b/rc_services_room/src/operations/tech_tree_data.rs
new file mode 100644
index 0000000..ccbb6a6
--- /dev/null
+++ b/rc_services_room/src/operations/tech_tree_data.rs
@@ -0,0 +1,28 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Dict};
+
+use crate::data::tech_tree::*;
+
+const PARAM_KEY: u8 = 210;
+
+pub(super) fn tech_tree_layout_provider() -> SimpleFunc<183, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Dict(Dict {
+ key_ty: 115, // str
+ val_ty: 104, // hashmap
+ items: vec![
+ (Typed::Str("1".into()), TechTreeNode {
+ main_cube_id: 1,
+ position_x: 0,
+ position_y: 0,
+ is_unlocked: true,
+ is_unlockable: true,
+ tech_points: 1,
+ neighbours: Vec::default(),
+ }.as_transmissible())
+ ],
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/tutorial_status.rs b/rc_services_room/src/operations/tutorial_status.rs
new file mode 100644
index 0000000..3ebb65b
--- /dev/null
+++ b/rc_services_room/src/operations/tutorial_status.rs
@@ -0,0 +1,16 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const IN_PROGRESS_PARAM_KEY: u8 = 140;
+const COMPLETED_PARAM_KEY: u8 = 141;
+const SKIPPED_PARAM_KEY: u8 = 142;
+
+pub(super) fn tutorial_info_provider() -> SimpleFunc<122, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(IN_PROGRESS_PARAM_KEY, Typed::Bool(false.into()));
+ params.insert(COMPLETED_PARAM_KEY, Typed::Bool(true.into()));
+ params.insert(SKIPPED_PARAM_KEY, Typed::Bool(true.into()));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/user_perms.rs b/rc_services_room/src/operations/user_perms.rs
new file mode 100644
index 0000000..d4a4ff9
--- /dev/null
+++ b/rc_services_room/src/operations/user_perms.rs
@@ -0,0 +1,16 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const MOD_PARAM_KEY: u8 = 10;
+const DEV_PARAM_KEY: u8 = 11;
+const ADM_PARAM_KEY: u8 = 12;
+
+pub(super) fn user_rights_provider() -> SimpleFunc<14, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(MOD_PARAM_KEY, Typed::Bool(false.into()));
+ params.insert(DEV_PARAM_KEY, Typed::Bool(false.into()));
+ params.insert(ADM_PARAM_KEY, Typed::Bool(false.into()));
+ Ok(params.into())
+ })
+}
diff --git a/rc_services_room/src/operations/user_xp.rs b/rc_services_room/src/operations/user_xp.rs
new file mode 100644
index 0000000..3b356fa
--- /dev/null
+++ b/rc_services_room/src/operations/user_xp.rs
@@ -0,0 +1,12 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed};
+
+const PARAM_KEY: u8 = 8;
+
+pub(super) fn get_user_xp_provider() -> SimpleFunc<83, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Int(31337));
+ Ok(params.into())
+ })
+}
diff --git a/rc_social/Cargo.toml b/rc_social/Cargo.toml
new file mode 100644
index 0000000..aed827b
--- /dev/null
+++ b/rc_social/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "rc_social"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+log.workspace = true
+env_logger.workspace = true
+tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] }
+clap.workspace = true
+polariton.workspace = true
+polariton_auth = { version = "*", path = "../polariton_auth" }
+polariton_server.workspace = true
diff --git a/rc_social/build_arm64.sh b/rc_social/build_arm64.sh
new file mode 100755
index 0000000..7010ff6
--- /dev/null
+++ b/rc_social/build_arm64.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+cargo build --release --target aarch64-unknown-linux-musl
diff --git a/rc_social/run_debug.sh b/rc_social/run_debug.sh
new file mode 100755
index 0000000..eedcd2c
--- /dev/null
+++ b/rc_social/run_debug.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+RUST_BACKTRACE=1 RUST_LOG=debug cargo run
diff --git a/rc_social/src/cli.rs b/rc_social/src/cli.rs
new file mode 100644
index 0000000..47cc79d
--- /dev/null
+++ b/rc_social/src/cli.rs
@@ -0,0 +1,31 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+pub struct CliArgs {
+ /// TCP port on which to accept connections
+ #[arg(short, long, default_value_t = 4534)]
+ pub port: u16,
+
+ /// IP Address on which to accept connections
+ #[arg(long, default_value_t = {"127.0.0.1".to_string()})]
+ pub ip: String,
+
+ /// Socket read tries before giving up (0 to never give up)
+ #[arg(long, default_value_t = 5)]
+ pub retries: usize,
+
+ /// Domain and port of the game server to send new connections
+ #[arg(long, default_value_t = {"127.0.0.1:4535".to_string()})]
+ pub redirect: String,
+
+ /// Name of game server to send new connections
+ #[arg(long, default_value_t = {"ngram_is_ngnius".to_string()})]
+ pub room_name: String,
+}
+
+impl CliArgs {
+ pub fn get() -> Self {
+ Self::parse()
+ }
+}
diff --git a/rc_social/src/main.rs b/rc_social/src/main.rs
new file mode 100644
index 0000000..2d221af
--- /dev/null
+++ b/rc_social/src/main.rs
@@ -0,0 +1,356 @@
+mod cli;
+mod state;
+
+use std::num::NonZero;
+use std::sync::Arc;
+
+use polariton_auth::Handshake;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::net;
+
+use polariton::packet::{Cryptographer, Data, Message, Packet, Ping, StandardMessage, StandardPacket};
+use polariton::operation::{OperationResponse, Typed};
+
+#[tokio::main]
+async fn main() -> std::io::Result<()> {
+ env_logger::init();
+ let args = cli::CliArgs::get();
+ log::debug!("Got cli args {:?}", args);
+
+ let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address");
+
+ // memory leak, but only once (so not a big deal)
+ let redirect_static = Box::leak(Box::new(args.redirect.clone()));
+ let room_name_static = Box::leak(Box::new(args.room_name.clone()));
+
+ let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?;
+
+ #[cfg(not(debug_assertions))]
+ loop {
+ let (socket, address) = listener.accept().await?;
+ tokio::spawn(process_socket(socket, address, NonZero::new(args.retries), op_handler.clone(), redirect_static, room_name_static));
+ }
+ #[cfg(debug_assertions)]
+ {
+ let (socket, address) = listener.accept().await?;
+ process_socket(socket, address, NonZero::new(args.retries), redirect_static, room_name_static).await;
+ Ok(())
+ }
+}
+
+async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, retries: Option>, redirect_url: &str, lobby_name: &str) {
+ log::debug!("Accepting connection from address {}", address);
+
+ let mut buf = Vec::new();
+ let enc = match do_connect_handshake(&mut buf, &mut socket, retries, lobby_name, redirect_url).await {
+ Some(x) => x,
+ None => {
+ log::error!("Failed to do connect handshake with {}", address);
+ return;
+ }
+ };
+ let sock_state = state::State::new(enc);
+ while let Ok(packet) = receive_packet(&mut buf, &mut socket, retries, sock_state.binrw_args()).await {
+ match packet {
+ Packet::Ping(ping) => {
+ handle_ping(ping, &mut buf, &mut socket).await;
+ },
+ Packet::Packet(packet) => log::warn!("Not handling packet {:?}", packet),
+ }
+ }
+ log::debug!("Goodbye connection from address {}", address);
+}
+
+async fn handle_ping(ping: Ping, buf: &mut Vec, socket: &mut net::TcpStream) {
+ buf.clear();
+ let resp = Packet::Ping(polariton_auth::ping_pong(ping));
+ resp.to_buf(buf, None).unwrap();
+ let write_count = socket.write(buf).await.unwrap();
+ log::debug!("(ping) Write {} bytes to socket: {:?}", write_count, buf);
+ buf.clear();
+}
+
+fn buf_likely_valid(buf: &[u8]) -> bool {
+ buf.is_empty() || buf[0] == Packet::PING_MAGIC || buf[0] == Packet::FRAMED_MAGIC
+}
+
+async fn read_more(buf: &mut Vec, socket: &mut net::TcpStream) -> Result {
+ let read_count = socket.read_buf(buf).await?;
+ log::debug!("Read {} bytes from socket: {:?}", read_count, buf);
+ Ok(read_count)
+}
+
+async fn receive_packet(buf: &mut Vec, socket: &mut net::TcpStream, max_retries: Option>, args: Option>>) -> Result {
+ if buf.is_empty() {
+ let read_count = read_more(buf, socket).await?;
+ if read_count == 0 { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "socket did not read any bytes")); } // bad packet
+ }
+
+ let mut last_err = None;
+ let mut must_succeed_next = false;
+ if let Some(max_retries) = max_retries {
+ for _ in 0..max_retries.get() {
+ match Packet::from_buf(&buf, args.clone()) {
+ Ok(packet) => {
+ log::debug!("Received packet {:?}", packet);
+ return Ok(packet);
+ },
+ Err(e) => last_err = Some(e),
+ }
+ if must_succeed_next {
+ break;
+ }
+ must_succeed_next = read_more(buf, socket).await? == 0;
+ }
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap()));
+ } else {
+ while buf_likely_valid(buf.as_slice()) {
+ match Packet::from_buf(&buf, args.clone()) {
+ Ok(packet) => {
+ log::debug!("Received packet {:?}", packet);
+ return Ok(packet);
+ },
+ Err(e) => last_err = Some(e),
+ }
+ if must_succeed_next {
+ break;
+ }
+ must_succeed_next = read_more(buf, socket).await? == 0;
+ }
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap()));
+ }
+}
+
+async fn send_packet(packet: Packet, buf: &mut Vec, socket: &mut net::TcpStream, args: Option>>) -> Result<(), std::io::Error> {
+ log::debug!("Sending packet {:?}", packet);
+ buf.clear();
+ packet.to_buf(buf, args).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
+ let write_count = socket.write(buf).await?;
+ log::debug!("Write {} bytes to socket: {:?}", write_count, buf);
+ #[cfg(debug_assertions)]
+ {
+ // print out unencrypted packet too
+ if let Packet::Packet(standard_p) = packet {
+ if let Message::Standard(standard_m) = standard_p.message {
+ if standard_m.is_encrypted() {
+ let standard_m = standard_m.encrypt(false);
+ let packet = Packet::Packet(StandardPacket { header: standard_p.header, message: Message::Standard(standard_m) });
+ packet.to_buf(buf, None).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
+ log::debug!("Unencrypted bytes of packet: {:?} (len: {})", buf, buf.len());
+ }
+
+ }
+ }
+ }
+ buf.clear();
+ Ok(())
+}
+
+const APP_ID: &str = "SocialServer";
+
+struct AuthImpl;
+
+const TOKEN_KEY: u8 = 216; // token;refresh_token
+//const UNKNOWN_BYTE_KEY: u8 = 217;
+const SERVICE_KEY: u8 = 224;
+const USERNAME_KEY: u8 = 225;
+
+//const CCU_KEY: u8 = 245;
+
+#[derive(Debug)]
+enum AuthError {
+ WrongService { expected: String, actual: String },
+ MissingService,
+ MissingToken,
+ MissingUsername,
+}
+
+impl AuthError {
+ fn log_err(&self) {
+ match self {
+ Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected),
+ Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY),
+ Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY),
+ Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY),
+ }
+ }
+}
+
+impl polariton_auth::AuthProvider for AuthImpl {
+ fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> {
+ if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) {
+ if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) {
+ if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) {
+ if service.string == APP_ID {
+ let params_resp = std::collections::HashMap::::new();
+ //params_resp.insert(CCU_KEY, Typed::Byte(0));
+ log::debug!("Auth success for {} (token: {})", user.string, token.string);
+ Ok(params_resp)
+ } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) }
+ } else { Err(AuthError::MissingUsername) }
+ } else { Err(AuthError::MissingService) }
+ } else { Err(AuthError::MissingToken) }
+ }
+}
+
+async fn do_connect_handshake(
+ buf: &mut Vec,
+ socket: &mut net::TcpStream,
+ max_retries: Option>,
+ game_server_name: &str,
+ game_server_url: &str,
+) -> Option>> {
+ let handshake = Handshake::new(APP_ID);
+ // connect
+ log::debug!("(connect) Handling first packet");
+ let packet1 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read connect packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ let (handshake, to_send) = match handshake.connect(&packet1) {
+ Ok(x) => (x.handshake, x.extra),
+ Err(e) => {
+ log::error!("Failed to handle connect handshake: {:?}", e.extra);
+ return None;
+ }
+ };
+ match send_packet(to_send, buf, socket, None).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send connect ack packet: {}", e);
+ return None;
+ }
+ }
+ // encrypt
+ log::debug!("(connect) Handling second packet");
+ let mut packet2 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) public key packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet2 {
+ handle_ping(ping, buf, socket).await;
+ packet2 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) public key packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) {
+ Ok(x) => (x.handshake, x.extra.0, x.extra.1),
+ Err(e) => {
+ log::error!("Failed to handle encryption handshake: {:?}", e.extra);
+ return None;
+ }
+ };
+ match send_packet(to_send, buf, socket, None).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send encryption ack packet: {}", e);
+ return None;
+ }
+ }
+ // pre-auth
+ let handshake = handshake.with_auth(AuthImpl);
+ // authenticate
+ log::debug!("(connect) Handling third packet");
+ let mut packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) auth packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet3 {
+ handle_ping(ping, buf, socket).await;
+ packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) auth packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ let to_send = match handshake.authenticate(&packet3, crypto.clone()) {
+ Ok(x) => x,
+ Err(h) => match h.extra {
+ polariton_auth::AuthError::Validation(e) => {
+ e.log_err();
+ return None;
+ },
+ e => {
+ log::error!("Failed to handle auth handshake: {:?}", e);
+ return None;
+ },
+ },
+ };
+ match send_packet(to_send, buf, socket, Some(crypto.clone())).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send auth ack packet: {}", e);
+ return None;
+ }
+ }
+
+ // redirect to lobby
+ let mut packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) join packet: {}", e);
+ return None;
+ }
+ };
+ while let Packet::Ping(ping) = packet_j {
+ handle_ping(ping, buf, socket).await;
+ packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) join packet: {}", e);
+ return None;
+ }
+ };
+ }
+ if let Packet::Packet(msg) = &packet_j {
+ if let Message::Standard(st) = &msg.message {
+ if let Data::OpReq(req) = &st.data {
+ if req.code == 225 { // join lobby
+ log::debug!("Max players from lobby join request: {:?}", req.params.to_owned().to_dict().get(&255));
+ let mut params = std::collections::HashMap::::new();
+ params.insert(230 /* game server address */, Typed::Str(game_server_url.into()));
+ params.insert(255 /* room name */, Typed::Str(game_server_name.into()));
+ let resp = Packet::from_message(
+ Message::Standard(
+ StandardMessage { flags: 0,
+ data: Data::OpResp(OperationResponse {
+ code: req.code,
+ return_code: 0,
+ message: Typed::Null,
+ params: params.into(),
+ }),
+ }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap();
+ match send_packet(resp, buf, socket, Some(crypto.clone())).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send lobby ack packet: {}", e);
+ return None;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Some(crypto)
+}
diff --git a/rc_social/src/operations/mod.rs b/rc_social/src/operations/mod.rs
new file mode 100644
index 0000000..caae20b
--- /dev/null
+++ b/rc_social/src/operations/mod.rs
@@ -0,0 +1,3 @@
+pub fn handler() -> polariton_server::operations::OperationsHandler {
+ polariton_server::operations::OperationsHandler::new()
+}
diff --git a/rc_social/src/state.rs b/rc_social/src/state.rs
new file mode 100644
index 0000000..2bd18ab
--- /dev/null
+++ b/rc_social/src/state.rs
@@ -0,0 +1,17 @@
+use std::sync::Arc;
+
+pub struct State {
+ pub crypto: Box>,
+}
+
+impl State {
+ pub fn new(c: Box>) -> Self {
+ Self {
+ crypto: c,
+ }
+ }
+
+ pub fn binrw_args(&self) -> polariton::packet::WriteArgs {
+ Some(self.crypto.clone())
+ }
+}
diff --git a/rc_social_room/Cargo.toml b/rc_social_room/Cargo.toml
new file mode 100644
index 0000000..009f7fa
--- /dev/null
+++ b/rc_social_room/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "rc_social_room"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+log.workspace = true
+env_logger.workspace = true
+tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] }
+clap.workspace = true
+polariton.workspace = true
+polariton_auth = { version = "*", path = "../polariton_auth" }
+polariton_server.workspace = true
diff --git a/rc_social_room/build_arm64.sh b/rc_social_room/build_arm64.sh
new file mode 100755
index 0000000..7010ff6
--- /dev/null
+++ b/rc_social_room/build_arm64.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+cargo build --release --target aarch64-unknown-linux-musl
diff --git a/rc_social_room/run_debug.sh b/rc_social_room/run_debug.sh
new file mode 100755
index 0000000..eedcd2c
--- /dev/null
+++ b/rc_social_room/run_debug.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+RUST_BACKTRACE=1 RUST_LOG=debug cargo run
diff --git a/rc_social_room/src/cli.rs b/rc_social_room/src/cli.rs
new file mode 100644
index 0000000..11819ce
--- /dev/null
+++ b/rc_social_room/src/cli.rs
@@ -0,0 +1,23 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+pub struct CliArgs {
+ /// TCP port on which to accept connections
+ #[arg(short, long, default_value_t = 4535)]
+ pub port: u16,
+
+ /// IP Address on which to accept connections
+ #[arg(long, default_value_t = {"127.0.0.1".to_string()})]
+ pub ip: String,
+
+ /// Socket read tries before giving up (0 to never give up)
+ #[arg(long, default_value_t = 5)]
+ pub retries: usize,
+}
+
+impl CliArgs {
+ pub fn get() -> Self {
+ Self::parse()
+ }
+}
diff --git a/rc_social_room/src/data/clan_invite.rs b/rc_social_room/src/data/clan_invite.rs
new file mode 100644
index 0000000..abbed9d
--- /dev/null
+++ b/rc_social_room/src/data/clan_invite.rs
@@ -0,0 +1,23 @@
+use polariton::operation::Typed;
+
+pub struct ClanInviteInfo {
+ pub username: String,
+ pub display_name: String,
+ pub clan_name: String,
+ pub clan_size: i32,
+ pub use_custom_avatar: bool,
+ pub avatar_id: i32,
+}
+
+impl ClanInviteInfo {
+ pub fn as_transmissible(&self) -> Typed {
+ Typed::HashMap(vec![
+ (Typed::Str("userName".into()), Typed::Str(self.username.clone().into())),
+ (Typed::Str("displayName".into()), Typed::Str(self.display_name.clone().into())),
+ (Typed::Str("clanName".into()), Typed::Str(self.clan_name.clone().into())),
+ (Typed::Str("clanSize".into()), Typed::Int(self.clan_size)),
+ (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.use_custom_avatar.into())),
+ (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id)),
+ ].into())
+ }
+}
diff --git a/rc_social_room/src/data/friend.rs b/rc_social_room/src/data/friend.rs
new file mode 100644
index 0000000..724116e
--- /dev/null
+++ b/rc_social_room/src/data/friend.rs
@@ -0,0 +1,19 @@
+use polariton::operation::Typed;
+
+pub struct AvatarInfo {
+ pub name: String,
+ pub use_custom_avatar: bool,
+ pub avatar_id: i32,
+}
+
+impl AvatarInfo {
+ pub fn as_transmissible(&self) -> Typed {
+ Typed::HashMap(vec![
+ (Typed::Str("name".into()), Typed::Str(self.name.clone().into())),
+ (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.use_custom_avatar.into())),
+ (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id)),
+ ].into())
+ }
+}
+
+// TODO pub struct FriendInfo {}
diff --git a/rc_social_room/src/data/mod.rs b/rc_social_room/src/data/mod.rs
new file mode 100644
index 0000000..4de3fde
--- /dev/null
+++ b/rc_social_room/src/data/mod.rs
@@ -0,0 +1,2 @@
+pub mod friend;
+pub mod clan_invite;
diff --git a/rc_social_room/src/main.rs b/rc_social_room/src/main.rs
new file mode 100644
index 0000000..dcddc27
--- /dev/null
+++ b/rc_social_room/src/main.rs
@@ -0,0 +1,404 @@
+mod cli;
+mod state;
+
+mod data;
+mod operations;
+
+use std::num::NonZero;
+use std::sync::Arc;
+
+use polariton_auth::Handshake;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::net;
+
+use polariton::packet::{Cryptographer, Data, Message, Packet, Ping, StandardMessage, StandardPacket};
+use polariton::operation::{OperationResponse, Typed};
+
+pub type UserTy = std::sync::RwLock;
+
+#[tokio::main]
+async fn main() -> std::io::Result<()> {
+ env_logger::init();
+ let args = cli::CliArgs::get();
+ log::debug!("Got cli args {:?}", args);
+
+ let op_handler = Arc::new(operations::handler());
+
+ let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address");
+
+ let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?;
+
+ #[cfg(not(debug_assertions))]
+ loop {
+ let (socket, address) = listener.accept().await?;
+ tokio::spawn(process_socket(socket, address, NonZero::new(args.retries), op_handler.clone()));
+ }
+ #[cfg(debug_assertions)]
+ {
+ let (socket, address) = listener.accept().await?;
+ process_socket(socket, address, NonZero::new(args.retries), op_handler.clone()).await;
+ Ok(())
+ }
+}
+
+async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, retries: Option>, op_handler: Arc>) {
+ log::debug!("Accepting connection from address {}", address);
+
+ let mut read_buf = Vec::new();
+ let mut write_buf = Vec::new();
+ let enc = match do_connect_handshake(&mut read_buf, &mut socket, retries).await {
+ Some(x) => x,
+ None => {
+ log::error!("Failed to do connect handshake with {}", address);
+ return;
+ }
+ };
+ let sock_state = state::State::new(enc);
+ let user_state = sock_state.user();
+ while let Ok(packet) = receive_packet(&mut read_buf, &mut socket, retries, sock_state.binrw_args()).await {
+ match packet {
+ Packet::Ping(ping) => {
+ handle_ping(ping, &mut write_buf, &mut socket).await;
+ for _ in 0..5 {
+ read_buf.remove(0);
+ }
+ },
+ Packet::Packet(packet) => {
+ // remove packet's advertised size from the buffer
+ for _ in 0..packet.header.len {
+ read_buf.remove(0);
+ }
+ match packet.message {
+ Message::Ping(ping) => {
+ handle_ping(ping, &mut write_buf, &mut socket).await;
+ },
+ Message::Standard(msg) => {
+
+ let is_encrypted = msg.is_encrypted();
+ match msg.data {
+ Data::OpReq(req) => {
+ let resp = op_handler.handle_op(&user_state, req);
+ let result = send_packet(
+ Packet::from_message(
+ Message::Standard(StandardMessage {
+ flags: 0,
+ data: Data::OpResp(resp),
+ }.encrypt(is_encrypted)),
+ packet.header.channel,
+ packet.header.is_reliable(),
+ sock_state.binrw_args()).unwrap(),
+ &mut write_buf, &mut socket, sock_state.binrw_args()).await;
+ match result {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send operation response packet: {}", e);
+ }
+ }
+ },
+ data => log::warn!("Failed to handle packet with message data {:?}", data),
+ }
+ }
+ }
+ }
+ //log::warn!("Not handling packet {:?}", packet),
+ }
+ }
+ log::debug!("Goodbye connection from address {}", address);
+}
+
+async fn handle_ping(ping: Ping, buf: &mut Vec, socket: &mut net::TcpStream) {
+ buf.clear();
+ let resp = Packet::Ping(polariton_auth::ping_pong(ping));
+ resp.to_buf(buf, None).unwrap();
+ let write_count = socket.write(buf).await.unwrap();
+ log::debug!("(ping) Write {} bytes to socket: {:?}", write_count, buf);
+ buf.clear();
+}
+
+fn buf_likely_valid(buf: &[u8]) -> bool {
+ buf.is_empty() || buf[0] == Packet::PING_MAGIC || buf[0] == Packet::FRAMED_MAGIC
+}
+
+async fn read_more(buf: &mut Vec, socket: &mut net::TcpStream) -> Result {
+ let read_count = socket.read_buf(buf).await?;
+ log::debug!("Read {} bytes from socket: {:?}", read_count, buf);
+ Ok(read_count)
+}
+
+async fn receive_packet(buf: &mut Vec, socket: &mut net::TcpStream, max_retries: Option>, args: Option>>) -> Result {
+ if buf.is_empty() {
+ let read_count = read_more(buf, socket).await?;
+ if read_count == 0 { return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "socket did not read any bytes")); } // bad packet
+ }
+
+ let mut last_err = None;
+ let mut must_succeed_next = false;
+ if let Some(max_retries) = max_retries {
+ for _ in 0..max_retries.get() {
+ match Packet::from_buf(&buf, args.clone()) {
+ Ok(packet) => {
+ log::debug!("Received packet {:?}", packet);
+ return Ok(packet);
+ },
+ Err(e) => last_err = Some(e),
+ }
+ if must_succeed_next {
+ break;
+ }
+ must_succeed_next = read_more(buf, socket).await? == 0;
+ }
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap()));
+ } else {
+ while buf_likely_valid(buf.as_slice()) {
+ match Packet::from_buf(&buf, args.clone()) {
+ Ok(packet) => {
+ log::debug!("Received packet {:?}", packet);
+ return Ok(packet);
+ },
+ Err(e) => last_err = Some(e),
+ }
+ if must_succeed_next {
+ break;
+ }
+ must_succeed_next = read_more(buf, socket).await? == 0;
+ }
+ return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, last_err.unwrap()));
+ }
+}
+
+async fn send_packet(packet: Packet, buf: &mut Vec, socket: &mut net::TcpStream, args: Option>>) -> Result<(), std::io::Error> {
+ log::debug!("Sending packet {:?}", packet);
+ buf.clear();
+ packet.to_buf(buf, args).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
+ let write_count = socket.write(buf).await?;
+ log::debug!("Write {} bytes to socket: {:?}", write_count, buf);
+ #[cfg(debug_assertions)]
+ {
+ // print out unencrypted packet too
+ if let Packet::Packet(standard_p) = packet {
+ if let Message::Standard(standard_m) = standard_p.message {
+ if standard_m.is_encrypted() {
+ let standard_m = standard_m.encrypt(false);
+ let packet = Packet::Packet(StandardPacket { header: standard_p.header, message: Message::Standard(standard_m) });
+ packet.to_buf(buf, None).map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?;
+ log::debug!("Unencrypted bytes of packet: {:?} (len: {})", buf, buf.len());
+ }
+
+ }
+ }
+ }
+ buf.clear();
+ Ok(())
+}
+
+const APP_ID: &str = "SocialServer";
+
+struct AuthImpl;
+
+const TOKEN_KEY: u8 = 216; // token;refresh_token
+//const UNKNOWN_BYTE_KEY: u8 = 217;
+const SERVICE_KEY: u8 = 224;
+const USERNAME_KEY: u8 = 225;
+
+//const CCU_KEY: u8 = 245;
+
+#[derive(Debug)]
+enum AuthError {
+ WrongService { expected: String, actual: String },
+ MissingService,
+ MissingToken,
+ MissingUsername,
+}
+
+impl AuthError {
+ fn log_err(&self) {
+ match self {
+ Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected),
+ Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY),
+ Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY),
+ Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY),
+ }
+ }
+}
+
+impl polariton_auth::AuthProvider for AuthImpl {
+ fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> {
+ if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) {
+ if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) {
+ if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) {
+ if service.string == APP_ID {
+ let params_resp = std::collections::HashMap::::new();
+ //params_resp.insert(CCU_KEY, Typed::Byte(0));
+ log::debug!("Auth success for {} (token: {})", user.string, token.string);
+ Ok(params_resp)
+ } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) }
+ } else { Err(AuthError::MissingUsername) }
+ } else { Err(AuthError::MissingService) }
+ } else { Err(AuthError::MissingToken) }
+ }
+}
+
+async fn do_connect_handshake(
+ buf: &mut Vec,
+ socket: &mut net::TcpStream,
+ max_retries: Option>,
+) -> Option>> {
+ let handshake = Handshake::new(APP_ID);
+ // connect
+ log::debug!("(connect) Handling first packet");
+ let packet1 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read connect packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ let (handshake, to_send) = match handshake.connect(&packet1) {
+ Ok(x) => (x.handshake, x.extra),
+ Err(e) => {
+ log::error!("Failed to handle connect handshake: {:?}", e.extra);
+ return None;
+ }
+ };
+ match send_packet(to_send, buf, socket, None).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send connect ack packet: {}", e);
+ return None;
+ }
+ }
+ // encrypt
+ log::debug!("(connect) Handling second packet");
+ let mut packet2 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) public key packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet2 {
+ handle_ping(ping, buf, socket).await;
+ packet2 = match receive_packet(buf, socket, max_retries, None).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) public key packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) {
+ Ok(x) => (x.handshake, x.extra.0, x.extra.1),
+ Err(e) => {
+ log::error!("Failed to handle encryption handshake: {:?}", e.extra);
+ return None;
+ }
+ };
+ match send_packet(to_send, buf, socket, None).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send encryption ack packet: {}", e);
+ return None;
+ }
+ }
+ // pre-auth
+ let handshake = handshake.with_auth(AuthImpl);
+ // authenticate
+ log::debug!("(connect) Handling third packet");
+ let mut packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) auth packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet3 {
+ handle_ping(ping, buf, socket).await;
+ packet3 = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) auth packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ let to_send = match handshake.authenticate(&packet3, crypto.clone()) {
+ Ok(x) => x,
+ Err(h) => match h.extra {
+ polariton_auth::AuthError::Validation(e) => {
+ e.log_err();
+ return None;
+ },
+ e => {
+ log::error!("Failed to handle auth handshake: {:?}", e);
+ return None;
+ },
+ },
+ };
+ match send_packet(to_send, buf, socket, Some(crypto.clone())).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send auth ack packet: {}", e);
+ return None;
+ }
+ }
+
+ // join lobby
+ log::debug!("(join lobby) Handling fourth packet");
+ let mut packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) join packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ while let Packet::Ping(ping) = packet_j {
+ handle_ping(ping, buf, socket).await;
+ packet_j = match receive_packet(buf, socket, max_retries, Some(crypto.clone())).await {
+ Ok(x) => x,
+ Err(e) => {
+ log::error!("Failed to read (maybe) join packet: {}", e);
+ return None;
+ }
+ };
+ buf.clear();
+ }
+ if let Packet::Packet(msg) = &packet_j {
+ if let Message::Standard(st) = &msg.message {
+ if let Data::OpReq(req) = &st.data {
+ if req.code == 226 { // join lobby (but for real this time)
+ let mut params = std::collections::HashMap::::new();
+ //params.insert(252 /* actors in game */, Typed::Str(game_server_url.into()));
+ params.insert(254 /* game server address */, Typed::Int(42));
+ params.insert(249 /* actor properties */, Typed::HashMap(Vec::new().into()));
+ params.insert(248 /* game properties */, Typed::HashMap(Vec::new().into()));
+ let resp = Packet::from_message(
+ Message::Standard(
+ StandardMessage { flags: 0,
+ data: Data::OpResp(OperationResponse {
+ code: req.code,
+ return_code: 0,
+ message: Typed::Null,
+ params: params.into(),
+ }),
+ }.encrypt(true)), 0, true, Some(crypto.clone())).unwrap();
+ match send_packet(resp, buf, socket, Some(crypto.clone())).await {
+ Ok(_) => {},
+ Err(e) => {
+ log::error!("Failed to send lobby ack packet: {}", e);
+ return None;
+ }
+ }
+ }
+ }
+ }
+ }
+ buf.clear();
+
+ Some(crypto)
+}
diff --git a/rc_social_room/src/operations/clan_invite.rs b/rc_social_room/src/operations/clan_invite.rs
new file mode 100644
index 0000000..f779b9c
--- /dev/null
+++ b/rc_social_room/src/operations/clan_invite.rs
@@ -0,0 +1,26 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Arr};
+
+use crate::data::clan_invite::*;
+
+const PARAM_KEY: u8 = 42;
+
+pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Arr(Arr {
+ ty: 104, // hashmap
+ items: vec![
+ ClanInviteInfo {
+ username: "RE_user1".to_owned(),
+ display_name: "RE_user1".to_owned(),
+ clan_name: "RE_clan1".to_owned(),
+ clan_size: 42,
+ use_custom_avatar: false,
+ avatar_id: 0,
+ }.as_transmissible()
+ ],
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_social_room/src/operations/friend_list.rs b/rc_social_room/src/operations/friend_list.rs
new file mode 100644
index 0000000..2669c8a
--- /dev/null
+++ b/rc_social_room/src/operations/friend_list.rs
@@ -0,0 +1,29 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Arr};
+
+use crate::data::friend::*;
+
+const FRIENDS_PARAM_KEY: u8 = 5;
+const AVATAR_PARAM_KEY: u8 = 76;
+
+pub(super) fn friends_provider() -> SimpleFunc<4, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(FRIENDS_PARAM_KEY, Typed::Arr(Arr {
+ ty: 99, // custom
+ items: vec![Typed::Custom(vec![ // FIXME don't manually serialize
+ 0u8, // byte custom type
+ 0u8, 5u8, // short custom object size
+ 3u8, 0u8, 0u8, 0u8, 0u8, // content
+ ].into())] }));
+ params.insert(AVATAR_PARAM_KEY, Typed::Arr(Arr {
+ ty: 104, // hashmap
+ items: vec![AvatarInfo {
+ name: "".to_string(),
+ use_custom_avatar: false,
+ avatar_id: 1,
+ }.as_transmissible()],
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs
new file mode 100644
index 0000000..47bd1d3
--- /dev/null
+++ b/rc_social_room/src/operations/mod.rs
@@ -0,0 +1,17 @@
+mod more_auth;
+mod friend_list;
+mod settings;
+mod clan_invite;
+
+use polariton_server::operations::OperationsHandler;
+
+pub fn handler() -> OperationsHandler {
+ OperationsHandler::new()
+ .without_state(more_auth::MoreLobbyAuth)
+ .without_state(polariton_server::operations::Ack::<33, _>::default()) // get user clan info (this is equivalent to not being in a clan)
+ .without_state(friend_list::friends_provider()) // TODO friend object parsing Token: 0x0200169C RID: 5788
+ .without_state(settings::settings_provider()) // TODO save settings persistently
+ .without_state(polariton_server::operations::Ack::<43, _>::default()) // get my clan info (this is equivalent to not being in a clan)
+ .without_state(clan_invite::clan_invites_provider())
+ .without_state(polariton_server::operations::Ack::<19, _>::default()) // get pending platoon invite (this is equivalent to having no pending invite)
+}
diff --git a/rc_social_room/src/operations/more_auth.rs b/rc_social_room/src/operations/more_auth.rs
new file mode 100644
index 0000000..b154590
--- /dev/null
+++ b/rc_social_room/src/operations/more_auth.rs
@@ -0,0 +1,42 @@
+use polariton::operation::Typed;
+use polariton_server::operations::{Operation, OperationCode};
+
+pub struct MoreLobbyAuth;
+
+impl MoreLobbyAuth {
+ const AUTH_PAYLOAD_KEY: u8 = 245;
+}
+
+impl Operation for MoreLobbyAuth {
+ type State = ();
+ type User = crate::UserTy;
+
+ fn handle(&self, params: polariton::operation::ParameterTable, _: &mut Self::State, user: &Self::User) -> polariton::operation::OperationResponse {
+ let params_dict = params.to_dict();
+ if let Some(Typed::Str(auth_payload)) = params_dict.get(&Self::AUTH_PAYLOAD_KEY) {
+ let mut write_lock = user.write().unwrap();
+ if write_lock.update_with_auth(&auth_payload.string) {
+ let mut resp_params = std::collections::HashMap::new();
+ resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0));
+ return polariton::operation::OperationResponse {
+ code: 230,
+ return_code: 0,
+ message: polariton::operation::Typed::Null,
+ params: resp_params.into(),
+ }
+ }
+ }
+ polariton::operation::OperationResponse {
+ code: 230,
+ return_code: 120,
+ message: polariton::operation::Typed::Null,
+ params: std::collections::HashMap::new().into(),
+ }
+ }
+}
+
+impl OperationCode for MoreLobbyAuth {
+ fn op_code() -> u8 {
+ 230
+ }
+}
diff --git a/rc_social_room/src/operations/platoon_invite.rs b/rc_social_room/src/operations/platoon_invite.rs
new file mode 100644
index 0000000..8a6664d
--- /dev/null
+++ b/rc_social_room/src/operations/platoon_invite.rs
@@ -0,0 +1,20 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Arr};
+
+use crate::data::friend::*;
+
+const INVITER_NAME_PARAM_KEY: u8 = 19;
+const INVITER_DISPLAY_NAME_PARAM_KEY: u8 = 75;
+const INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY: u8 = 13;
+const INVITER_AVATAR_ID_NAME_PARAM_KEY: u8 = 14;
+
+pub(super) fn platoon_pending_provider() -> SimpleFunc<19, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(INVITER_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter".into())));
+ params.insert(INVITER_DISPLAY_NAME_PARAM_KEY, Typed::Str("RE_platoon_inviter_display".into())));
+ params.insert(INVITER_CUSTOM_AVATAR_NAME_PARAM_KEY, Typed::Bool(false.into())));
+ params.insert(INVITER_AVATAR_ID_NAME_PARAM_KEY, Typed::Int(1)));
+ Ok(params.into())
+ })
+}
diff --git a/rc_social_room/src/operations/settings.rs b/rc_social_room/src/operations/settings.rs
new file mode 100644
index 0000000..469d81d
--- /dev/null
+++ b/rc_social_room/src/operations/settings.rs
@@ -0,0 +1,16 @@
+use polariton_server::operations::SimpleFunc;
+use polariton::operation::{ParameterTable, Typed, Dict};
+
+const PARAM_KEY: u8 = 30;
+
+pub(super) fn settings_provider() -> SimpleFunc<24, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> {
+ SimpleFunc::new(|params, _| {
+ let mut params = params.to_dict();
+ params.insert(PARAM_KEY, Typed::Dict(Dict {
+ key_ty: 115,
+ val_ty: 42,
+ items: Vec::default(),
+ }));
+ Ok(params.into())
+ })
+}
diff --git a/rc_social_room/src/state.rs b/rc_social_room/src/state.rs
new file mode 100644
index 0000000..7adeeb0
--- /dev/null
+++ b/rc_social_room/src/state.rs
@@ -0,0 +1,43 @@
+use std::sync::{Arc, RwLock};
+
+pub struct State {
+ pub crypto: Box>,
+}
+
+impl State {
+ pub fn new(c: Box>) -> Self {
+ Self {
+ crypto: c,
+ }
+ }
+
+ pub fn binrw_args(&self) -> polariton::packet::WriteArgs {
+ Some(self.crypto.clone())
+ }
+
+ pub fn user(&self) -> crate::UserTy {
+ RwLock::new(UserState::default())
+ }
+}
+
+#[derive(Default, Debug)]
+pub struct UserState {
+ pub uuid: String,
+ pub token: String,
+ pub refresh_token: String,
+}
+
+impl UserState {
+ pub fn update_with_auth(&mut self, auth_str: &str) -> bool {
+ let splits: Vec<&str> = auth_str.split(';').collect();
+ if splits.len() != 3 {
+ log::warn!("Invalid auth payload: {}", auth_str);
+ false
+ } else {
+ self.uuid = splits[0].to_owned();
+ self.token = splits[1].to_owned();
+ self.refresh_token = splits[2].to_owned();
+ true
+ }
+ }
+}
diff --git a/rc_static_data/Rocket.toml b/rc_static_data/Rocket.toml
index 5127b13..3841eb4 100644
--- a/rc_static_data/Rocket.toml
+++ b/rc_static_data/Rocket.toml
@@ -5,7 +5,7 @@ port = 8010
## set only when compiled in debug mode, i.e, `cargo build`
[debug]
-port = 80
+port = 8010
## set only when compiled in release mode, i.e, `cargo build --release`
[release]
diff --git a/rc_static_data/run_debug.sh b/rc_static_data/run_debug.sh
index d307e6c..6351ef2 100755
--- a/rc_static_data/run_debug.sh
+++ b/rc_static_data/run_debug.sh
@@ -1,3 +1,3 @@
#!/bin/bash
-cargo build
-RUST_LOG=debug sudo -HE ../target/debug/rc_static_data
+#cargo build
+RUST_LOG=debug cargo run