diff --git a/.gitignore b/.gitignore index 87860e1..d792194 100644 --- a/.gitignore +++ b/.gitignore @@ -306,3 +306,5 @@ MultiplayerAssets/ProjectSettings/* !MultiplayerAssets/ProjectSettings/ProjectVersion.txt # Packages !MultiplayerAssets/Packages +/Lobby Servers/Rust Server/target +*.pem diff --git a/Directory.Build.targets.EXAMPLE b/Directory.Build.targets.EXAMPLE new file mode 100644 index 0000000..29314c4 --- /dev/null +++ b/Directory.Build.targets.EXAMPLE @@ -0,0 +1,25 @@ + + + + C:\Program Files (x86)\Steam\steamapps\common\Derail Valley + C:\Program Files\Unity\Hub\Editor\2019.4.40f1\Editor + + $(DvInstallDir)\DerailValley_Data\Managed\; + $(DvInstallDir)\DerailValley_Data\Managed\UnityModManager\; + $(UnityInstallDir)\Data\Managed\ + + $(AssemblySearchPaths);$(ReferencePath); + C:\Program Files (x86)\Microsoft SDKs\ClickOnce\SignTool\ + 7cf2b8a98a09ffd407ada2e94f200af24a0e68bc + + \ No newline at end of file diff --git a/Lobby Servers/PHP Server/.htaccess b/Lobby Servers/PHP Server/.htaccess new file mode 100644 index 0000000..c8f0917 --- /dev/null +++ b/Lobby Servers/PHP Server/.htaccess @@ -0,0 +1,11 @@ +# Enable the RewriteEngine +RewriteEngine On + +# Uncomment below to force HTTPS +# RewriteCond %{HTTPS} off +# RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + +# Redirect all non-existing paths to index.php +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^ index.php [QSA,L] \ No newline at end of file diff --git a/Lobby Servers/PHP Server/DatabaseInterface.php b/Lobby Servers/PHP Server/DatabaseInterface.php new file mode 100644 index 0000000..ae751d4 --- /dev/null +++ b/Lobby Servers/PHP Server/DatabaseInterface.php @@ -0,0 +1,10 @@ + diff --git a/Lobby Servers/PHP Server/FlatfileDatabase.php b/Lobby Servers/PHP Server/FlatfileDatabase.php new file mode 100644 index 0000000..c649d39 --- /dev/null +++ b/Lobby Servers/PHP Server/FlatfileDatabase.php @@ -0,0 +1,99 @@ +filePath = $dbConfig['flatfile_path']; + } + + private function readData() { + if (!file_exists($this->filePath)) { + return []; + } + return json_decode(file_get_contents($this->filePath), true) ?? []; + } + + private function writeData($data) { + file_put_contents($this->filePath, json_encode($data, JSON_PRETTY_PRINT)); + } + + public function addGameServer($data) { + $data['last_update'] = time(); // Set current time as last_update + + $servers = $this->readData(); + $servers[] = $data; + $this->writeData($servers); + + return json_encode([ + "game_server_id" => $data['game_server_id'], + "private_key" => $data['private_key'], + ]); + } + + public function updateGameServer($data) { + $servers = $this->readData(); + $updated = false; + + foreach ($servers as &$server) { + if ($server['game_server_id'] === $data['game_server_id']) { + $server['current_players'] = $data['current_players']; + $server['time_passed'] = $data['time_passed']; + $server['last_update'] = time(); // Update with current time + + $updated = true; + break; + } + } + + if ($updated) { + $this->writeData($servers); + return json_encode(["message" => "Server updated"]); + } else { + return json_encode(["error" => "Failed to update server"]); + } + } + + public function removeGameServer($data) { + $servers = $this->readData(); + $servers = array_filter($servers, function($server) use ($data) { + return $server['game_server_id'] !== $data['game_server_id']; + }); + $this->writeData(array_values($servers)); + return json_encode(["message" => "Server removed"]); + } + + public function listGameServers() { + $servers = $this->readData(); + $current_time = time(); + $active_servers = []; + $changed = false; + + foreach ($servers as $key => $server) { + if ($current_time - $server['last_update'] <= TIMEOUT) { + $active_servers[] = $server; + } else { + $changed = true; // Indicates there's a change if any server is removed + } + } + + if ($changed) { + $this->writeData($active_servers); // Write back only if there are changes + } + + return json_encode($active_servers); + } + + public function getGameServer($game_server_id) { + $servers = $this->readData(); + foreach ($servers as $server) { + if ($server['game_server_id'] === $game_server_id) { + return json_encode($server); + } + } + return json_encode(null); + } +} + +?> diff --git a/Lobby Servers/PHP Server/MySQLDatabase.php b/Lobby Servers/PHP Server/MySQLDatabase.php new file mode 100644 index 0000000..c6caf42 --- /dev/null +++ b/Lobby Servers/PHP Server/MySQLDatabase.php @@ -0,0 +1,79 @@ +pdo = new PDO("mysql:host={$dbConfig['host']};dbname={$dbConfig['dbname']}", $dbConfig['username'], $dbConfig['password']); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } + + public function addGameServer($data) { + $stmt = $this->pdo->prepare("INSERT INTO game_servers (game_server_id, private_key, ipv4, ipv6, port, server_name, password_protected, game_mode, difficulty, time_passed, current_players, max_players, required_mods, game_version, multiplayer_version, server_info, last_update) + VALUES (:game_server_id, :private_key, :ip, :port, :server_name, :password_protected, :game_mode, :difficulty, :time_passed, :current_players, :max_players, :required_mods, :game_version, :multiplayer_version, :server_info, :last_update)"); + $stmt->execute([ + ':game_server_id' => $data['game_server_id'], + ':private_key' => $data['private_key'], + ':ipv4' => isset($data['ipv4']) ? $data['ipv4'] : '', + ':ipv6' => isset($data['ipv6']) ? $data['ipv6'] : '', + ':port' => $data['port'], + ':server_name' => $data['server_name'], + ':password_protected' => $data['password_protected'], + ':game_mode' => $data['game_mode'], + ':difficulty' => $data['difficulty'], + ':time_passed' => $data['time_passed'], + ':current_players' => $data['current_players'], + ':max_players' => $data['max_players'], + ':required_mods' => $data['required_mods'], + ':game_version' => $data['game_version'], + ':multiplayer_version' => $data['multiplayer_version'], + ':server_info' => $data['server_info'], + ':last_update' => time() //use current time + ]); + return json_encode([ + "game_server_id" => $data['game_server_id'], + "private_key" => $data['private_key'] + ]); + } + + public function updateGameServer($data) { + $stmt = $this->pdo->prepare("UPDATE game_servers + SET current_players = :current_players, time_passed = :time_passed, last_update = :last_update + WHERE game_server_id = :game_server_id"); + $stmt->execute([ + ':current_players' => $data['current_players'], + ':time_passed' => $data['time_passed'], + ':last_update' => time(), // Update with current time + ':game_server_id' => $data['game_server_id'] + ]); + + return $stmt->rowCount() > 0 ? json_encode(["message" => "Server updated"]) : json_encode(["error" => "Failed to update server"]); + } + + public function removeGameServer($data) { + $stmt = $this->pdo->prepare("DELETE FROM game_servers WHERE game_server_id = :game_server_id"); + $stmt->execute([':game_server_id' => $data['game_server_id']]); + return $stmt->rowCount() > 0 ? json_encode(["message" => "Server removed"]) : json_encode(["error" => "Failed to remove server"]); + } + + public function listGameServers() { + // Remove servers that exceed TIMEOUT directly in the SQL query + $stmt = $this->pdo->prepare("DELETE FROM game_servers WHERE last_update < :timeout"); + $stmt->execute([':timeout' => time() - TIMEOUT]); + + // Fetch remaining servers + $stmt = $this->pdo->query("SELECT * FROM game_servers"); + $servers = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return json_encode($servers); + } + + public function getGameServer($game_server_id) { + $stmt = $this->pdo->prepare("SELECT * FROM game_servers WHERE game_server_id = :game_server_id"); + $stmt->execute([':game_server_id' => $game_server_id]); + return json_encode($stmt->fetch(PDO::FETCH_ASSOC)); + } +} + +?> diff --git a/Lobby Servers/PHP Server/Read Me.md b/Lobby Servers/PHP Server/Read Me.md new file mode 100644 index 0000000..5bc4c50 --- /dev/null +++ b/Lobby Servers/PHP Server/Read Me.md @@ -0,0 +1,149 @@ +# Lobby Server - PHP + +This is a PHP implementation of the Derail Valley Lobby Server REST API service. It is designed to run on any standard web hosting and does not rely on long-running/persistent behaviour. +HTTPS support depends on the configuration of the hosting environment. + +As this implementation is not persistent in memory, a database is used to store server information. Two options are available for the database engine - a JSON based flatfile or a MySQL database. + +## Installing + +The following instructions assume you will be using an Apache web server and may need to be modified for other configurations. + +1. Copy the following files to your public html folder (consult your web server/web host's documentation) +``` +index.php +DatabaseInterface.php +FlatfileDatabase.php +MySQLDatabase.php +.htaccess +``` +2. Copy `config.php` to a secure location outside of your public html directory +3. Edit `index.php` and update the path to the config file on line 2: +```php + 'mysql', + 'host' => 'localhost', + 'dbname' => 'dv_lobby', + 'username' => 'dv_lobby_server', + 'password' => 'n16O5+LMpeqI`{E', + 'flatfile_path' => '' // Path to store the flatfile database +]; +?> +``` + +Example `config.php` using Flatfile: +```php + 'flatfile', + 'host' => '', + 'dbname' => '', + 'username' => '', + 'password' => '', + 'flatfile_path' => '/dv_lobby/flatfile.db' // Path to store the flatfile database +]; +?> +``` + +## Security Considerations +This is a non-comprehensive overview of security considerations. You should always use up-to-date best practices and seek professional advice where required. + +### Environment variables +Consider using environment variables to store sensitive database credentials (e.g. `dbConfig`.`host`, `dbConfig`.`dbname`, `dbConfig`.`username`, `dbConfig`.`password`) instead of hardcoding them in config.php. +Your `config.php` can be updated to reference the environment variables. + +Example: +```php +$dbConfig = [ + 'type' => 'mysql', + 'host' => getenv('DB_HOST'), + 'dbname' => getenv('DB_NAME'), + 'username' => getenv('DB_USER'), + 'password' => getenv('DB_PASSWORD'), + 'flatfile_path' => '/path/to/flatfile.db' +]; +``` + + +### File Permissions +Ensure that `config.php` and any other sensitive files outside the web root are only readable by the web server user (chmod 600). +For directories containing flatfile databases, restrict permissions (chmod 700 or 750) to prevent unauthorised access. + +### HTTPS (SSL) +Configure your server to use https. Many web hosts provide free SSL certificates via Let's Encrypt. +Consider forcing https via server config/`.httaccess`. + +Example: +```apacheconf +RewriteEngine On +RewriteCond %{HTTPS} off +RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] +``` diff --git a/Lobby Servers/PHP Server/config.php b/Lobby Servers/PHP Server/config.php new file mode 100644 index 0000000..52073ea --- /dev/null +++ b/Lobby Servers/PHP Server/config.php @@ -0,0 +1,16 @@ + 'mysql', // Change to 'flatfile' to use flatfile database + 'host' => 'localhost', + 'dbname' => 'your_database', + 'username' => 'your_username', + 'password' => 'your_password', + 'flatfile_path' => '/path/to/flatfile.db' // Path to store the flatfile database +]; + +?> \ No newline at end of file diff --git a/Lobby Servers/PHP Server/index.php b/Lobby Servers/PHP Server/index.php new file mode 100644 index 0000000..68751f1 --- /dev/null +++ b/Lobby Servers/PHP Server/index.php @@ -0,0 +1,158 @@ + "Invalid server information"]); + } + + $data['game_server_id'] = uuid_create(); + $data['private_key'] = generate_private_key(); + + return $db->addGameServer($data); +} + +function update_game_server($db, $data) { + if (!validate_server_update($db, $data)) { + http_response_code(500); + return json_encode(["error" => "Invalid game server ID or private key"]); + } + + $data['last_update'] = time(); + return $db->updateGameServer($data); +} + +function remove_game_server($db, $data) { + if (!validate_server_update($db, $data)) { + return json_encode(["error" => "Invalid game server ID or private key"]); + } + + return $db->removeGameServer($data); +} + +function list_game_servers($db) { + $servers = json_decode($db->listGameServers(), true); + + // Remove private keys from the servers before returning + // and select the correct protocol version for the requestor + foreach ($servers as &$server) { + unset($server['private_key']); + unset($server['last_update']); + + if(!isset($server['ipv4'])){ + $server['ipv4'] = ''; + } + + if(!isset($server['ipv6'])){ + $server['ipv6'] = ''; + } + + if(filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){ + //Host made a request on IPv4, remove IPv6 address as we assume they don't support it. + unset($server['ipv6']); + + } + } + return json_encode($servers); +} + +function validate_server_info($data) { + + if(!isset($data['ipv4']) || !filter_var($data['ipv4'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)){ + $data['ipv4'] = ''; + }elseif(!isset($data['ipv6']) || !filter_var($data['ipv6'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){ + $data['ipv6'] = ''; + } + + if ( + //make sure we have at least one IP + $data['ipv4'] == '' && $data['ipv6'] == '' || + + //Make sure we have all required fields + !isset($data['server_name']) || + !isset($data['server_info']) || + !isset($data['current_players']) || + !isset($data['max_players']) || + + //Validate fields + strlen($data['server_name']) > 25 || + strlen($data['server_info']) > 500 || + $data['current_players'] > $data['max_players'] || + $data['max_players'] < 1 + ){ + + return false; + } + + return true; +} + +function validate_server_update($db, $data) { + $server = json_decode($db->getGameServer($data['game_server_id']), true); + return $server && $server['private_key'] === $data['private_key']; +} + +function uuid_create() { + return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); +} + +function generate_private_key() { + // Generate a 128-bit (16 bytes) random binary string + $random_bytes = random_bytes(16); + + // Convert the binary string to a hexadecimal representation + $private_key = bin2hex($random_bytes); + + return $private_key; +} + +?> \ No newline at end of file diff --git a/Lobby Servers/PHP Server/install.php b/Lobby Servers/PHP Server/install.php new file mode 100644 index 0000000..f323dcb --- /dev/null +++ b/Lobby Servers/PHP Server/install.php @@ -0,0 +1,55 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Create the database if it doesn't exist + $sql = "CREATE DATABASE IF NOT EXISTS " . $dbConfig['dbname']; + $pdo->exec($sql); + echo "Database created successfully.
"; + + // Connect to the newly created database + $dsn = 'mysql:host=' . $dbConfig['host'] . ';dbname=' . $dbConfig['dbname']; + $pdo = new PDO($dsn, $dbConfig['username'], $dbConfig['password']); + $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Create the game_servers table + $sql = " + CREATE TABLE IF NOT EXISTS game_servers ( + game_server_id VARCHAR(50) PRIMARY KEY, + private_key VARCHAR(255) NOT NULL, + ipv4 VARCHAR(45) NOT NULL, + ipv6 VARCHAR(45) NOT NULL, + port INT NOT NULL, + server_name VARCHAR(100) NOT NULL, + password_protected BOOLEAN NOT NULL, + game_mode VARCHAR(50) NOT NULL, + difficulty VARCHAR(50) NOT NULL, + time_passed INT NOT NULL, + current_players INT NOT NULL, + max_players INT NOT NULL, + required_mods TEXT NOT NULL, + game_version VARCHAR(50) NOT NULL, + multiplayer_version VARCHAR(50) NOT NULL, + server_info TEXT NOT NULL, + last_update INT NOT NULL + ); + "; + + // Execute the SQL to create the table + $pdo->exec($sql); + echo "Table 'game_servers' created successfully.
"; + +} catch (PDOException $e) { + die("DB ERROR: " . $e->getMessage()); +} +?> diff --git a/Lobby Servers/RestAPI.md b/Lobby Servers/RestAPI.md new file mode 100644 index 0000000..cd9d574 --- /dev/null +++ b/Lobby Servers/RestAPI.md @@ -0,0 +1,272 @@ +# Derail Valley Lobby Server REST API Documentation + +Revision: A +Date: 2024-06-22 + +## Overview + +This document describes the REST API endpoints for the Derail Valley Lobby Server service. The service allows game servers to register, update, and deregister themselves, and provides a list of active servers to clients. +This spec does not provide the server address, as new servers can be created by anyone wishing to host their own lobby server. + +## Enums + +### Game Modes + +The game_mode field in the request body for adding a game server must be one of the following integer values, each representing a specific game mode: + +- 0: Career +- 1: Sandbox +- 2: Scenario + +### Difficulty Levels + +The difficulty field in the request body for adding a game server must be one of the following integer values, each representing a specific difficulty level: + +- 0: Standard +- 1: Comfort +- 2: Realistic +- 3: Custom + +## Endpoints + +### Add Game Server + +- **URL:** `/add_game_server` +- **Method:** `POST` +- **Content-Type:** `application/json` +- **Request Body:** + ```json + { + "ipv4": "string", + "ipv6": "string", + "port": "integer", + "server_name": "string", + "password_protected": "boolean", + "game_mode": "integer", + "difficulty": "integer", + "time_passed": "string", + "current_players": "integer", + "max_players": "integer", + "required_mods": "string", + "game_version": "string", + "multiplayer_version": "string", + "server_info": "string" + } + ``` + - **Fields:** + - ipv4 (optional string): The publically accessible IPv4 address of the game server - if this is not supplied, then the IPv6 address must be. + - ipv6 (optional string): The publically accessible IPv4 address of the game server - if this is not supplied, then the IPv4 address must be.. + - port (integer): The port number of the game server. + - server_name (string): The name of the game server (maximum 25 characters). + - password_protected (boolean): Indicates if the server is password-protected. + - game_mode (integer): The game mode (see [Game Modes](#game-modes)). + - difficulty (integer): The difficulty level (see [Difficulty Levels](#difficulty-levels)). + - time_passed (string): The in-game time passed since the game/session was started. + - current_players (integer): The current number of players on the server (0 - max_players). + - max_players (integer): The maximum number of players allowed on the server (>= 1). + - required_mods (string): The required mods for the server, supplied as a JSON string. + - game_version (string): The game version the server is running. + - multiplayer_version (string): The Multiplayer Mod version the server is running. + - server_info (string): Additional information about the server (maximum 500 characters). +- **Response:** + - **Success:** + - **Code:** 200 OK + - **Content-Type:** `application/json` + - **Content:** + ```json + { + "game_server_id": "string", + "private_key": "string" + } + ``` + - game_server_id (string): A GUID assigned to the game server. This GUID uniquely identifies the game server and is used when updating the lobby server. + - private_key (string): A shared secret between the lobby server and the game server. Must be supplied when updating the lobby server. + - **Error:** + - **Code:** 500 Internal Server Error + - **Content:** `"Failed to add server"` + +### Update Server + +- **URL:** `/update_game_server` +- **Method:** `POST` +- **Content-Type:** `application/json` +- **Request Body:** + ```json + { + "game_server_id": "string", + "private_key": "string", + "current_players": "integer", + "time_passed": "string", + } + ``` + - **Fields:** + - game_server_id (string): The GUID assigned to the game server (returned from `add_game_server`). + - private_key (string): The shared secret between the lobby server and the game server (returned from `add_game_server`). + - current_players (integer): The current number of players on the server (0 - max_players). + - time_passed (string): The in-game time passed since the game/session was started. +- **Response:** + - **Success:** + - **Code:** 200 OK + - **Content:** `"Server updated"` + - **Error:** + - **Code:** 500 Internal Server Error + - **Content:** `"Failed to update server"` + +### Remove Server + +- **URL:** `/remove_game_server` +- **Method:** `POST` +- **Content-Type:** `application/json` +- **Request Body:** + ```json + { + "game_server_id": "string", + "private_key": "string" + } + ``` + - **Fields:** + - game_server_id (string): The GUID assigned to the game server (returned from `add_game_server`). + - private_key (string): The shared secret between the lobby server and the game server (returned from `add_game_server`). +- **Response:** + - **Success:** + - **Code:** 200 OK + - **Content:** `"Server removed"` + - **Error:** + - **Code:** 500 Internal Server Error + - **Content:** `"Failed to remove server"` + +### List Game Servers + +- **URL:** `/list_game_servers` +- **Method:** `GET` +- **Response:** + - **Success:** + - **Code:** 200 OK + - **Content-Type:** `application/json` + - **Content:** + ```json + [ + { + "ipv4": "string", + "ipv6": "string", + "port": "integer", + "server_name": "string", + "password_protected": "boolean", + "game_mode": "integer", + "difficulty": "integer", + "time_passed": "string", + "current_players": "integer", + "max_players": "integer", + "required_mods": "string", + "game_version": "string", + "multiplayer_version": "string", + "server_info": "string", + "game_server_id": "string" + }, + ... + ] + ``` + - **Fields:** + - ipv4 (optional string): The IPv4 address of the game server, if known. + - ipv6 (optional string): The IPv6 address of the game server, if known and if the end point request is made using IPv6, i.e. IPv4 clients will not be provided with the ``ipv6`` field. + - port (integer): The port number of the game server. + - server_name (string): The name of the game server (maximum 25 characters). + - password_protected (boolean): Indicates if the server is password-protected. + - game_mode (integer): The game mode (see [Game Modes](#game-modes)). + - difficulty (integer): The difficulty level (see [Difficulty Levels](#difficulty-levels)). + - time_passed (string): The in-game time passed since the game/session was started. + - current_players (integer): The current number of players on the server (0 - max_players). + - max_players (integer): The maximum number of players allowed on the server (>= 1). + - required_mods (string): The required mods for the server, supplied as a JSON string. + - game_version (string): The game version the server is running. + - multiplayer_version (string): The Multiplayer Mod version the server is running. + - server_info (string): Additional information about the server (maximum 500 characters). + - game_server_id (string): The GUID assigned to the game server. + - **Error:** + - **Code:** 500 Internal Server Error + - **Content:** `"Failed to retrieve servers"` + +## Example Requests + +### Add Game Server +Example request: +```bash +curl -X POST -H "Content-Type: application/json" -d '{ + "ipv4": "127.0.0.1", + "ipv6": "::1", + "port": 7777, + "server_name": "My Derail Valley Server", + "password_protected": false, + "current_players": 1, + "max_players": 10, + "game_mode": 0, + "difficulty": 0, + "time_passed": "0d 10h 45m 12s", + "required_mods": "", + "game_version": "98", + "multiplayer_version": "0.1.0", + "server_info": "License unlocked server
Join our discord and have fun!" +}' http:///add_game_server +``` +Example response: +```json +{ + "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342", + "private_key": "6fca6e1499dab0358f79dc0b251b4e23" +} +``` + +### Update Game Server +Example request: +```bash +curl -X POST -H "Content-Type: application/json" -d '{ + "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342", + "private_key": "6fca6e1499dab0358f79dc0b251b4e23", + "current_players": 2, + "time_passed": "0d 10h 47m 12s" +}' http:///update_game_server +``` +Example response: +```json +{ + "message": "Server updated" +} +``` +### Remove Game Server +Example request: +```bash +curl -X POST -H "Content-Type: application/json" -d '{ + "game_server_id": "0e1759fd-ba6e-4476-ace2-f173af9db342", + "private_key": "6fca6e1499dab0358f79dc0b251b4e23" +}' http:///remove_game_server +``` +Example response: +```json +{ + "message": "Server removed" +} +``` + +### List Game Servers + +```bash +curl http:///list_game_servers +``` + +## Error Handling + +In case of an error, the API will return a JSON response with a message indicating the failure. + +```json +{ + "error": "string" +} +``` + +### Common Error Responses + +- **500 Internal Server Error** + - **Content:** `"Failed to add server"` + - **Content:** `"Failed to update server"` + - **Content:** `"Failed to remove server"` + - **Content:** `"Failed to retrieve servers"` diff --git a/Lobby Servers/Rust Server/Cargo.lock b/Lobby Servers/Rust Server/Cargo.lock new file mode 100644 index 0000000..f80e1d8 --- /dev/null +++ b/Lobby Servers/Rust Server/Cargo.lock @@ -0,0 +1,1540 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae682f693a9cd7b058f2b0b5d9a6d7728a8555779bedbbc35dd88528611d020" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-tls", + "actix-utils", + "ahash", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b02303ce8d4e8be5b855af6cf3c3a08f3eff26880faad82bab679c22d3650cb5" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-tls" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "impl-more", + "openssl", + "pin-project-lite", + "tokio", + "tokio-openssl", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1988c02af8d2b718c05bc4aeb6a66395b7cdf32858c2c71131e5637a8c05a9ff" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-tls", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] + +[[package]] +name = "cc" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +dependencies = [ + "jobserver", + "libc", + "once_cell", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "impl-more" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "lobby_server" +version = "0.1.0" +dependencies = [ + "actix-web", + "env_logger", + "log", + "openssl", + "rand", + "serde", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "object" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "syn" +version = "2.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff8655ed1d86f3af4ee3fd3263786bc14245ad17c4c7e85ba7187fb3ae028c90" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-openssl" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ffab79df67727f6acf57f1ff743091873c24c579b1e2ce4d8f53e47ded4d63d" +dependencies = [ + "futures-util", + "openssl", + "openssl-sys", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.11+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Lobby Servers/Rust Server/Cargo.toml b/Lobby Servers/Rust Server/Cargo.toml new file mode 100644 index 0000000..2e80b78 --- /dev/null +++ b/Lobby Servers/Rust Server/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "lobby_server" +version = "0.1.0" +edition = "2018" + +[dependencies] +actix-web = "4.0" +tokio = { version = "1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +log = "0.4" +env_logger = "0.9" +uuid = { version = "1.0", features = ["v4"] } +openssl = "0.10" +rand = "0.8" + +[features] +default = ["actix-web/openssl"] \ No newline at end of file diff --git a/Lobby Servers/Rust Server/Read Me.md b/Lobby Servers/Rust Server/Read Me.md new file mode 100644 index 0000000..db84e87 --- /dev/null +++ b/Lobby Servers/Rust Server/Read Me.md @@ -0,0 +1,56 @@ +# Lobby Server - Rust + +This is a [Rust](https://www.rust-lang.org/) implementation of the Derail Valley Lobby Server REST API service. The server can be run in either HTTP or HTTPS (SSL) modes (cert and key PEM files will need to be provided for SSL mode). + +## Building the Code + +To build the Lobby Server code, you'll need Rust, Cargo and OpenSSL installed on your system. + + +### Installing OpenSSL (Windows) +OpenSSL can be installed as follows [[source](https://stackoverflow.com/a/70949736)]: +1. Install OpenSSL from [http://slproweb.com/products/Win32OpenSSL.html](http://slproweb.com/products/Win32OpenSSL.html) into `C:\Program Files\OpenSSL-Win64` +2. In an elevated terminal +``` +$env:path = $env:path+ ";C:\Program Files\OpenSSL-Win64\bin" +cd "C:\Program Files\OpenSSL-Win64" +mkdir certs +cd certs +wget https://curl.se/ca/cacert.pem -o cacert.pem +``` +4. In the VSCode Rust Server terminal set the following environment variables +``` +$env:OPENSSL_CONF='C:\Program Files\OpenSSL-Win64\bin\openssl.cfg' +$env:OPENSSL_NO_VENDOR=1 +$env:RUSTFLAGS='-Ctarget-feature=+crt-static' +$env:SSL_CERT = 'C:\Program Files\OpenSSL-Win64\certs\cacert.pem' +$env:OPENSSL_DIR = 'C:\Program Files\OpenSSL-Win64' +$env:OPENSSL_LIB_DIR = "C:\Program Files\OpenSSL-Win64\lib\VC\x64\MD" +``` + + +### Building +The code can be built using `cargo build --release` or built and run (for testing purposes) using `cargo run --release` + +## Configuration Parameters +The server can be configured using a `config.json` file; if one is not supplied, the server will create one with the defaults. + +Below are the available parameters along with their defaults: +- `port` (u16): The port number on which the server will listen. Default: `8080` +- `timeout` (u64): The time-out period in seconds for server removal. Default: `120` +- `ssl_enabled` (bool): Whether SSL is enabled. Default: `false` +- `ssl_cert_path` (string): Path to the SSL certificate file. Default: `"cert.pem"` +- `ssl_key_path` (string): Path to the SSL private key file. Default: `"key.pem"` + +To customise these parameters, create a `config.json` file in the project directory with the desired values. +Example `config.json`: +```json +{ + "port": 8080, + "timeout": 120, + "ssl_enabled": false, + "ssl_cert_path": "cert.pem", + "ssl_key_path": "key.pem" +} +``` + diff --git a/Lobby Servers/Rust Server/config.json b/Lobby Servers/Rust Server/config.json new file mode 100644 index 0000000..e863e8b --- /dev/null +++ b/Lobby Servers/Rust Server/config.json @@ -0,0 +1,7 @@ +{ + "port": 8080, + "timeout": 120, + "ssl_enabled": false, + "ssl_cert_path": "cert.pem", + "ssl_key_path": "key.pem" +} \ No newline at end of file diff --git a/Lobby Servers/Rust Server/src/config.rs b/Lobby Servers/Rust Server/src/config.rs new file mode 100644 index 0000000..bc25a1f --- /dev/null +++ b/Lobby Servers/Rust Server/src/config.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::{Read, Write}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Config { + pub port: u16, + pub timeout: u64, + pub ssl_enabled: bool, + pub ssl_cert_path: String, + pub ssl_key_path: String, +} + +impl Default for Config { + fn default() -> Self { + Config { + port: 8080, + timeout: 120, + ssl_enabled: false, + ssl_cert_path: String::from("cert.pem"), + ssl_key_path: String::from("key.pem"), + } + } +} + +pub fn read_or_create_config() -> Config { + let config_path = "config.json"; + let mut config = Config::default(); + + if let Ok(mut file) = File::open(config_path) { + let mut contents = String::new(); + if file.read_to_string(&mut contents).is_ok() { + if let Ok(parsed_config) = serde_json::from_str(&contents) { + config = parsed_config; + } + } + } else { + if let Ok(mut file) = File::create(config_path) { + let _ = file.write_all(serde_json::to_string_pretty(&config).unwrap().as_bytes()); + } + } + + config +} diff --git a/Lobby Servers/Rust Server/src/handlers.rs b/Lobby Servers/Rust Server/src/handlers.rs new file mode 100644 index 0000000..36961fd --- /dev/null +++ b/Lobby Servers/Rust Server/src/handlers.rs @@ -0,0 +1,204 @@ +use actix_web::{web, HttpResponse, HttpRequest, Responder}; +use serde::{Deserialize, Serialize}; +use crate::state::AppState; +use crate::server::{ServerInfo, PublicServerInfo, AddServerResponse, validate_server_info}; +use crate::utils::generate_private_key; +use uuid::Uuid; + +#[derive(Deserialize)] +pub struct AddServerRequest { + pub port: u16, + pub server_name: String, + pub password_protected: bool, + pub game_mode: u8, + pub difficulty: u8, + pub time_passed: String, + pub current_players: u32, + pub max_players: u32, + pub required_mods: String, + pub game_version: String, + pub multiplayer_version: String, + pub server_info: String, +} + +pub async fn add_server(data: web::Data, server_info: web::Json, req: HttpRequest) -> impl Responder { + let client_ip = req.connection_info().realip_remote_addr().unwrap_or("unknown").to_string(); + + let (ipv4, ipv6): (String, String) = match client_ip { + IpAddr::V4(ipv4) => (ipv4.to_string(), String::new()), // IPv4 case + IpAddr::V6(ipv6) => (String::new(), ipv6.to_string()), // IPv6 case + }; + + let private_key = generate_private_key(); // Generate a private key + let info = ServerInfo { + ipv4: ipv4.clone(), + ipv6: ipv6.clone(), + port: server_info.port, + server_name: server_info.server_name.clone(), + password_protected: server_info.password_protected, + game_mode: server_info.game_mode, + difficulty: server_info.difficulty, + time_passed: server_info.time_passed.clone(), + current_players: server_info.current_players, + max_players: server_info.max_players, + required_mods: server_info.required_mods.clone(), + game_version: server_info.game_version.clone(), + multiplayer_version: server_info.multiplayer_version.clone(), + server_info: server_info.server_info.clone(), + last_update: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(), + private_key: private_key.clone(), + }; + + if let Err(e) = validate_server_info(&info) { + log::error!("Validation failed: {}", e); + return HttpResponse::BadRequest().json(e); + } + + let game_server_id = Uuid::new_v4().to_string(); + let key = game_server_id.clone(); + let ipv4_request: bool = (ipv4 == String::new()); + + match data.servers.lock() { + Ok(mut servers) => { + servers.insert(key.clone(), info); + log::info!("Server added: {}", key); + HttpResponse::Ok().json(AddServerResponse { game_server_id: key, private_key, ipv4_request }) + } + Err(_) => { + log::error!("Failed to add server: {}", key); + HttpResponse::InternalServerError().json("Failed to add server") + } + } +} + +#[derive(Deserialize)] +pub struct UpdateServerRequest { + pub game_server_id: String, + pub private_key: String, + pub current_players: u32, + pub time_passed: String, + pub ipv4: Option, +} + +pub async fn update_server(data: web::Data, server_info: web::Json) -> impl Responder { + let mut updated = false; + match data.servers.lock() { + Ok(mut servers) => { + if let Some(info) = servers.get_mut(&server_info.game_server_id) { + if info.private_key == server_info.private_key { + if server_info.current_players <= info.max_players { + info.current_players = server_info.current_players; + info.time_passed = server_info.time_passed.clone(); + info.last_update = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(); + + // Check if ipv4 field is provided, not empty, and valid + if let Some(ipv4_str) = &server_info.ipv4 { + if !ipv4_str.is_empty() { + if let Ok(ip) = ipv4_str.parse::() { + if let IpAddr::V4(_) = ip { + info.ipv4 = ipv4_str.clone(); + } + } + } + } + + updated = true; + } + } else { + return HttpResponse::Unauthorized().json("Invalid private key"); + } + } + } + Err(_) => { + log::error!("Failed to update server: {}", server_info.game_server_id); + return HttpResponse::InternalServerError().json("Failed to update server"); + } + } + + if updated { + log::info!("Server updated: {}", server_info.game_server_id); + HttpResponse::Ok().json("Server updated") + } else { + log::error!("Server not found or invalid current players: {}", server_info.game_server_id); + HttpResponse::BadRequest().json("Server not found or invalid current players") + } +} + +#[derive(Deserialize)] +pub struct RemoveServerRequest { + pub game_server_id: String, + pub private_key: String, +} + +pub async fn remove_server(data: web::Data, server_info: web::Json) -> impl Responder { + let mut removed = false; + match data.servers.lock() { + Ok(mut servers) => { + if let Some(info) = servers.get(&server_info.game_server_id) { + if info.private_key == server_info.private_key { + servers.remove(&server_info.game_server_id); + removed = true; + } else { + return HttpResponse::Unauthorized().json("Invalid private key"); + } + } + } + Err(_) => { + log::error!("Failed to remove server: {}", server_info.game_server_id); + return HttpResponse::InternalServerError().json("Failed to remove server"); + } + }; + + if removed { + log::info!("Server removed: {}", server_info.game_server_id); + HttpResponse::Ok().json("Server removed") + } else { + log::error!("Server not found: {}", server_info.game_server_id); + HttpResponse::BadRequest().json("Server not found or invalid private key") + } +} + +pub async fn list_servers(data: web::Data, req: HttpRequest) -> impl Responder { + let client_ip = req.connection_info().realip_remote_addr().unwrap_or("unknown").to_string(); + + let ip_version = match client_ip { + IpAddr::V4(_) => "IPv4", + IpAddr::V6(_) => "IPv6", + }; + + match data.servers.lock() { + Ok(servers) => { + let public_servers: Vec = servers.iter().map(|(id, info)| { + let ip = match ip_version { + "IPv4" => info.ipv4.clone(), + "IPv6" => if info.ipv6 != String::new() { + info.ipv6.clone() + } else { + info.ipv4.clone() + }, + _ => info.ipv4.clone(), // Default to IPv4 if something goes wrong + }; + + PublicServerInfo { + id: id.clone(), + ip: ip, + port: info.port, + server_name: info.server_name.clone(), + password_protected: info.password_protected, + game_mode: info.game_mode, + difficulty: info.difficulty, + time_passed: info.time_passed.clone(), + current_players: info.current_players, + max_players: info.max_players, + required_mods: info.required_mods.clone(), + game_version: info.game_version.clone(), + multiplayer_version: info.multiplayer_version.clone(), + server_info: info.server_info.clone(), + } + }).collect(); + + HttpResponse::Ok().json(public_servers) + } + Err(_) => HttpResponse::InternalServerError().json("Failed to list servers"), + } +} diff --git a/Lobby Servers/Rust Server/src/main.rs b/Lobby Servers/Rust Server/src/main.rs new file mode 100644 index 0000000..286a442 --- /dev/null +++ b/Lobby Servers/Rust Server/src/main.rs @@ -0,0 +1,74 @@ +mod config; +mod server; +mod state; +mod handlers; +mod ssl; +mod utils; + +use crate::config::read_or_create_config; +use crate::state::AppState; +use crate::ssl::setup_ssl; +use actix_web::{web, App, HttpServer}; +use std::sync::{Arc, Mutex}; +use tokio::time::{interval, Duration}; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + let config = read_or_create_config(); + let state = AppState { + servers: Arc::new(Mutex::new(std::collections::HashMap::new())), + }; + + let cleanup_state = state.clone(); + let config_clone = config.clone(); + + tokio::spawn(async move { + let mut interval = interval(Duration::from_secs(60)); + loop { + interval.tick().await; + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + if let Ok(mut servers) = cleanup_state.servers.lock() { + let keys_to_remove: Vec = servers + .iter() + .filter_map(|(key, info)| { + if now - info.last_update > config_clone.timeout { + Some(key.clone()) + } else { + None + } + }) + .collect(); + for key in keys_to_remove { + servers.remove(&key); + } + } + } + }); + + let server = { + let server_builder = HttpServer::new(move || { + App::new() + .app_data(web::Data::new(state.clone())) + .route("/add_game_server", web::post().to(handlers::add_server)) + .route("/update_game_server", web::post().to(handlers::update_server)) + .route("/remove_game_server", web::post().to(handlers::remove_server)) + .route("/list_game_servers", web::get().to(handlers::list_servers)) + }); + + if config.ssl_enabled { + let ssl_builder = setup_ssl(&config)?; + server_builder + .bind_openssl(format!("0.0.0.0:{}", config.port), (move || ssl_builder)())? + } else { + server_builder.bind(format!("0.0.0.0:{}", config.port))? + } + }; + + // Start the server + server.run().await +} \ No newline at end of file diff --git a/Lobby Servers/Rust Server/src/server.rs b/Lobby Servers/Rust Server/src/server.rs new file mode 100644 index 0000000..a4c9abc --- /dev/null +++ b/Lobby Servers/Rust Server/src/server.rs @@ -0,0 +1,64 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct ServerInfo { + pub ipv4: String, + pub ipv6: String, + pub port: u16, + pub server_name: String, + pub password_protected: bool, + pub game_mode: u8, + pub difficulty: u8, + pub time_passed: String, + pub current_players: u32, + pub max_players: u32, + pub required_mods: String, + pub game_version: String, + pub multiplayer_version: String, + pub server_info: String, + #[serde(skip_serializing)] + pub last_update: u64, + #[serde(skip_serializing)] + pub private_key: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct PublicServerInfo { + pub id: String, + pub ip: String, + pub port: u16, + pub server_name: String, + pub password_protected: bool, + pub game_mode: u8, + pub difficulty: u8, + pub time_passed: String, + pub current_players: u32, + pub max_players: u32, + pub required_mods: String, + pub game_version: String, + pub multiplayer_version: String, + pub server_info: String, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct AddServerResponse { + pub game_server_id: String, + pub private_key: String, + pub ipv4_request: bool, +} + +pub fn validate_server_info(info: &ServerInfo) -> Result<(), &'static str> { + if info.server_name.len() > 25 { + return Err("Server name exceeds 25 characters"); + } + if info.server_info.len() > 500 { + return Err("Server info exceeds 500 characters"); + } + if info.current_players > info.max_players { + return Err("Current players exceed max players"); + } + if info.max_players < 1 { + return Err("Max players must be at least 1"); + } + Ok(()) +} diff --git a/Lobby Servers/Rust Server/src/ssl.rs b/Lobby Servers/Rust Server/src/ssl.rs new file mode 100644 index 0000000..f8c9f70 --- /dev/null +++ b/Lobby Servers/Rust Server/src/ssl.rs @@ -0,0 +1,10 @@ +use crate::config::Config; +use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; +use openssl::ssl::SslAcceptorBuilder; + +pub fn setup_ssl(config: &Config) -> std::io::Result { + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls())?; + builder.set_private_key_file(&config.ssl_key_path, SslFiletype::PEM)?; + builder.set_certificate_chain_file(&config.ssl_cert_path)?; + Ok(builder) +} diff --git a/Lobby Servers/Rust Server/src/state.rs b/Lobby Servers/Rust Server/src/state.rs new file mode 100644 index 0000000..a1335a9 --- /dev/null +++ b/Lobby Servers/Rust Server/src/state.rs @@ -0,0 +1,7 @@ +use std::sync::{Arc, Mutex}; +use crate::server::ServerInfo; + +#[derive(Clone)] +pub struct AppState { + pub servers: Arc>>, +} diff --git a/Lobby Servers/Rust Server/src/utils.rs b/Lobby Servers/Rust Server/src/utils.rs new file mode 100644 index 0000000..b89c13c --- /dev/null +++ b/Lobby Servers/Rust Server/src/utils.rs @@ -0,0 +1,8 @@ +use rand::Rng; + +pub fn generate_private_key() -> String { + let mut rng = rand::thread_rng(); + let random_bytes: Vec = (0..16).map(|_| rng.gen::()).collect(); + let private_key: String = random_bytes.iter().map(|b| format!("{:02x}", b)).collect(); + private_key +} diff --git a/Multiplayer/Components/IdMonoBehaviour.cs b/Multiplayer/Components/IdMonoBehaviour.cs index f8fa3c6..44e05f8 100644 --- a/Multiplayer/Components/IdMonoBehaviour.cs +++ b/Multiplayer/Components/IdMonoBehaviour.cs @@ -9,7 +9,7 @@ namespace Multiplayer.Components; public abstract class IdMonoBehaviour : MonoBehaviour where T : struct where I : MonoBehaviour { private static readonly IdPool idPool = new(); - private static readonly Dictionary> indexToObject = new(); + private static readonly Dictionary> indexToObject = []; private T _netId; @@ -32,7 +32,16 @@ protected static bool Get(T netId, out IdMonoBehaviour obj) return true; obj = null; if ((netId as dynamic).CompareTo(default(T)) != 0) - Multiplayer.LogDebug(() => $"Got invalid NetId {netId} while processing packet {NetPacketProcessor.CurrentlyProcessingPacket}"); + Multiplayer.LogDebug(() => $"Got invalid NetId {netId} while processing packet {NetworkLifecycle.Instance.IsProcessingPacket}"); + return false; + } + + protected static bool TryGet(T netId, out IdMonoBehaviour obj) + { + if (indexToObject.TryGetValue(netId, out obj)) + return true; + + obj = null; return false; } diff --git a/Multiplayer/Components/MainMenu/HostGamePane.cs b/Multiplayer/Components/MainMenu/HostGamePane.cs new file mode 100644 index 0000000..3f35b8d --- /dev/null +++ b/Multiplayer/Components/MainMenu/HostGamePane.cs @@ -0,0 +1,431 @@ +using System; +using System.Reflection; +using DV; +using DV.UI; +using DV.UI.PresetEditors; +using DV.UIFramework; +using DV.Localization; +using DV.Common; +using Multiplayer.Utils; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using UnityEngine.Events; +using Multiplayer.Networking.Data; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Util; +using UnityModManagerNet; +using System.Linq; +using Multiplayer.Networking.Managers.Server; +namespace Multiplayer.Components.MainMenu; + +public class HostGamePane : MonoBehaviour +{ + private const int MAX_SERVER_NAME_LEN = 25; + private const int MAX_PORT_LEN = 5; + private const int MAX_DETAILS_LEN = 500; + + private const int MIN_PORT = 1024; + private const int MAX_PORT = 49151; + private const int MIN_PLAYERS = 2; + private const int MAX_PLAYERS = 10; + + private const int DEFAULT_PORT = 7777; + + TMP_InputField serverName; + TMP_InputField password; + TMP_InputField port; + TMP_InputField details; + TextMeshProUGUI serverDetails; + + SliderDV maxPlayers; + Toggle gamePublic; + ButtonDV startButton; + + public ISaveGame saveGame; + public UIStartGameData startGameData; + public AUserProfileProvider userProvider; + public AScenarioProvider scenarioProvider; + LauncherController lcInstance; + + public Action continueCareerRequested; + #region setup + + public void Awake() + { + Multiplayer.Log("HostGamePane Awake()"); + + CleanUI(); + BuildUI(); + ValidateInputs(null); + } + + public void Start() + { + Multiplayer.Log("HostGamePane Started"); + + } + + public void OnEnable() + { + //Multiplayer.Log("HostGamePane OnEnable()"); + this.SetupListeners(true); + } + + // Disable listeners + public void OnDisable() + { + this.SetupListeners(false); + } + + private void CleanUI() + { + //top elements + GameObject.Destroy(this.FindChildByName("Text Content")); + + //body elements + GameObject.Destroy(this.FindChildByName("GRID VIEW")); + GameObject.Destroy(this.FindChildByName("HardcoreSavingBanner")); + GameObject.Destroy(this.FindChildByName("TutorialSavingBanner")); + + //footer elements + GameObject.Destroy(this.FindChildByName("ButtonIcon OpenFolder")); + GameObject.Destroy(this.FindChildByName("ButtonIcon Rename")); + GameObject.Destroy(this.FindChildByName("ButtonIcon Delete")); + GameObject.Destroy(this.FindChildByName("ButtonTextIcon Load")); + GameObject.Destroy(this.FindChildByName("ButtonTextIcon Overwrite")); + + } + private void BuildUI() + { + //Create Prefabs + GameObject goMMC = GameObject.FindObjectOfType().gameObject; + + GameObject dividerPrefab = goMMC.FindChildByName("Divider"); + if (dividerPrefab == null) + { + Multiplayer.LogError("Divider not found!"); + return; + } + + GameObject cbPrefab = goMMC.FindChildByName("CheckboxFreeCam"); + if (cbPrefab == null) + { + Multiplayer.LogError("CheckboxFreeCam not found!"); + return; + } + + GameObject sliderPrefab = goMMC.FindChildByName("Field Of View").gameObject; + if (sliderPrefab == null) + { + Multiplayer.LogError("SliderLimitSession not found!"); + return; + } + + GameObject inputPrefab = MainMenuThingsAndStuff.Instance.references.popupTextInput.gameObject.FindChildByName("TextFieldTextIcon"); + if (inputPrefab == null) + { + Multiplayer.LogError("TextFieldTextIcon not found!"); + return; + } + + + lcInstance = goMMC.FindChildByName("PaneRight Launcher").GetComponent(); + if (lcInstance == null) + { + Multiplayer.LogError("No Run Button"); + return; + } + Sprite playSprite = lcInstance.runButton.FindChildByName("[icon]").GetComponent().sprite; + + + //update title + GameObject titleObj = this.FindChildByName("Title"); + GameObject.Destroy(titleObj.GetComponentInChildren()); + titleObj.GetComponentInChildren().key = Locale.SERVER_HOST__TITLE_KEY; + titleObj.GetComponentInChildren().UpdateLocalization(); + + //update right hand info pane (this will be used later for more settings or information + GameObject serverWindowGO = this.FindChildByName("Save Description"); + GameObject serverDetailsGO = serverWindowGO.FindChildByName("text list [noloc]"); + HyperlinkHandler hyperLinks = serverDetailsGO.GetOrAddComponent(); + + hyperLinks.linkColor = new Color(0.302f, 0.651f, 1f); // #4DA6FF + hyperLinks.linkHoverColor = new Color(0.498f, 0.749f, 1f); // #7FBFFF + + serverWindowGO.name = "Host Details"; + serverDetails = serverDetailsGO.GetComponent(); + serverDetails.textWrappingMode = TextWrappingModes.Normal; + serverDetails.text = Locale.Get(Locale.SERVER_HOST__INSTRUCTIONS_FIRST_KEY, ["", ""]) + "


" + + Locale.Get(Locale.SERVER_HOST__MOD_WARNING_KEY, ["", ""]) + "

" + + Locale.SERVER_HOST__RECOMMEND + "

" + + Locale.SERVER_HOST__SIGNOFF; + /*"First time hosts, please see the Hosting section of our Wiki.


" + + + "Using other mods may cause unexpected behaviour including de-syncs. See Mod Compatibility for more info.

" + + "It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.

" + + + "We hope to have your favourite mods compatible with multiplayer in the future.";*/ + + + //Find scrolling viewport + ScrollRect scroller = this.FindChildByName("Scroll View").GetComponent(); + RectTransform scrollerRT = scroller.transform.GetComponent(); + scrollerRT.sizeDelta = new Vector2(scrollerRT.sizeDelta.x, 504); + + // Create the content object + GameObject controls = new("Controls"); + controls.SetLayersRecursive(Layers.UI); + controls.transform.SetParent(scroller.viewport.transform, false); + + // Assign the content object to the ScrollRect + RectTransform contentRect = controls.AddComponent(); + contentRect.anchorMin = new Vector2(0, 1); + contentRect.anchorMax = new Vector2(1, 1); + contentRect.pivot = new Vector2(0f, 1); + contentRect.anchoredPosition = new Vector2(0, 21); + contentRect.sizeDelta = scroller.viewport.sizeDelta; + scroller.content = contentRect; + + // Add VerticalLayoutGroup and ContentSizeFitter + VerticalLayoutGroup layoutGroup = controls.AddComponent(); + layoutGroup.childControlWidth = false; + layoutGroup.childControlHeight = false; + layoutGroup.childScaleWidth = false; + layoutGroup.childScaleHeight = false; + layoutGroup.childForceExpandWidth = true; + layoutGroup.childForceExpandHeight = true; + + layoutGroup.spacing = 0; // Adjust the spacing as needed + layoutGroup.padding = new RectOffset(0,0,0,0); + + ContentSizeFitter sizeFitter = controls.AddComponent(); + sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + GameObject go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform,false); + go.name = "Server Name"; + //go.AddComponent(); + serverName = go.GetComponent(); + serverName.text = Multiplayer.Settings.ServerName?.Trim().Substring(0,Mathf.Min(Multiplayer.Settings.ServerName.Trim().Length,MAX_SERVER_NAME_LEN)); + serverName.placeholder.GetComponent().text = Locale.SERVER_HOST_NAME; + serverName.characterLimit = MAX_SERVER_NAME_LEN; + go.AddComponent(); + go.ResetTooltip(); + + + go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Password"; + password = go.GetComponent(); + password.text = Multiplayer.Settings.Password; + //password.contentType = TMP_InputField.ContentType.Password; //re-introduce later when code for toggling has been implemented + password.placeholder.GetComponent().text = Locale.SERVER_HOST_PASSWORD; + go.AddComponent();//.enabledKey = Locale.SERVER_HOST_PASSWORD__TOOLTIP_KEY; + go.ResetTooltip(); + + + go = GameObject.Instantiate(cbPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Public"; + TMP_Text label = go.FindChildByName("text").GetComponent(); + label.text = "Public Game"; + gamePublic = go.GetComponent(); + gamePublic.isOn = Multiplayer.Settings.PublicGame; + gamePublic.interactable = true; + go.GetComponentInChildren().key = Locale.SERVER_HOST_PUBLIC_KEY; + GameObject.Destroy(go.GetComponentInChildren()); + go.ResetTooltip(); + + + go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta,106).transform, false); + go.name = "Details"; + go.transform.GetComponent().sizeDelta = new Vector2(go.transform.GetComponent().sizeDelta.x, 106); + details = go.GetComponent(); + details.characterLimit = MAX_DETAILS_LEN; + details.lineType = TMP_InputField.LineType.MultiLineNewline; + details.FindChildByName("text [noloc]").GetComponent().alignment = TextAlignmentOptions.TopLeft; + + details.placeholder.GetComponent().text = Locale.SERVER_HOST_DETAILS; + + + go = GameObject.Instantiate(dividerPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Divider"; + + + go = GameObject.Instantiate(sliderPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Max Players"; + go.FindChildByName("[text label]").GetComponent().key = Locale.SERVER_HOST_MAX_PLAYERS_KEY; + go.ResetTooltip(); + go.FindChildByName("[text label]").GetComponent().UpdateLocalization(); + maxPlayers = go.GetComponent(); + maxPlayers.stepIncrement = 1; + maxPlayers.minValue = MIN_PLAYERS; + maxPlayers.maxValue = MAX_PLAYERS; + maxPlayers.value = Mathf.Clamp(Multiplayer.Settings.MaxPlayers,MIN_PLAYERS,MAX_PLAYERS); + maxPlayers.interactable = true; + + + go = GameObject.Instantiate(inputPrefab, NewContentGroup(controls, scroller.viewport.sizeDelta).transform, false); + go.name = "Port"; + port = go.GetComponent(); + port.characterValidation = TMP_InputField.CharacterValidation.Integer; + port.characterLimit = MAX_PORT_LEN; + port.placeholder.GetComponent().text = "7777"; + port.text = (Multiplayer.Settings.Port >= MIN_PORT && Multiplayer.Settings.Port <= MAX_PORT) ? Multiplayer.Settings.Port.ToString() : DEFAULT_PORT.ToString(); + + + go = this.gameObject.UpdateButton("ButtonTextIcon Save", "ButtonTextIcon Start", Locale.SERVER_HOST_START_KEY, null, playSprite); + go.FindChildByName("[text]").GetComponent().UpdateLocalization(); + + startButton = go.GetComponent(); + startButton.onClick.RemoveAllListeners(); + startButton.onClick.AddListener(StartClick); + + + } + + private GameObject NewContentGroup(GameObject parent, Vector2 sizeDelta, int cellMaxHeight = 53) + { + // Create a content group + GameObject contentGroup = new("ContentGroup"); + contentGroup.SetLayersRecursive(Layers.UI); + RectTransform groupRect = contentGroup.AddComponent(); + contentGroup.transform.SetParent(parent.transform, false); + groupRect.sizeDelta = sizeDelta; + + ContentSizeFitter sizeFitter = contentGroup.AddComponent(); + sizeFitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + // Add VerticalLayoutGroup and ContentSizeFitter + GridLayoutGroup glayoutGroup = contentGroup.AddComponent(); + glayoutGroup.startCorner = GridLayoutGroup.Corner.LowerLeft; + glayoutGroup.startAxis = GridLayoutGroup.Axis.Vertical; + glayoutGroup.cellSize = new Vector2(617.5f, cellMaxHeight); + glayoutGroup.spacing = new Vector2(0, 0); + glayoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount; + glayoutGroup.constraintCount = 1; + glayoutGroup.padding = new RectOffset(10, 0, 0, 10); + + return contentGroup; + } + + + +private void SetupListeners(bool on) + { + if (on) + { + serverName.onValueChanged.RemoveAllListeners(); + serverName.onValueChanged.AddListener(new UnityAction(ValidateInputs)); + + port.onValueChanged.RemoveAllListeners(); + port.onValueChanged.AddListener(new UnityAction(ValidateInputs)); + } + else + { + this.serverName.onValueChanged.RemoveAllListeners(); + } + + } + + #endregion + + #region UI callbacks + private void ValidateInputs(string text) + { + bool valid = true; + int portNum; + + if (serverName.text.Trim() == "" || serverName.text.Length > MAX_SERVER_NAME_LEN) + valid = false; + + if (port.text != "") + { + portNum = int.Parse(port.text); + if(portNum < MIN_PORT || portNum > MAX_PORT) + return; + + } + + if( port.text == "" && (Multiplayer.Settings.Port < MIN_PORT || Multiplayer.Settings.Port > MAX_PORT)) + valid = false; + + startButton.ToggleInteractable(valid); + + //Multiplayer.Log($"HostPane validated: {valid}"); + } + + private void StartClick() + { + + using (LobbyServerData serverData = new()) + { + serverData.port = (port.text == "") ? Multiplayer.Settings.Port : int.Parse(port.text); ; + serverData.Name = serverName.text.Trim(); + serverData.HasPassword = password.text != ""; + serverData.isPublic = gamePublic.isOn; + + serverData.GameMode = 0; //replaced with details from save / new game + serverData.Difficulty = 0; //replaced with details from save / new game + serverData.TimePassed = "N/A"; //replaced with details from save, or persisted if new game (will be updated in lobby server update cycle) + + serverData.CurrentPlayers = 0; + serverData.MaxPlayers = (int)maxPlayers.value; + + ModInfo[] serverMods = ModInfo.FromModEntries(UnityModManager.modEntries) + .Where(mod => !NetworkServer.modWhiteList.Contains(mod.Id) && mod.Id != Multiplayer.ModEntry.Info.Id).ToArray(); + + string requiredMods = ""; + if (serverMods.Length > 0) + { + requiredMods = string.Join(", ", serverMods.Select(mod => $"{{{mod.Id}, {mod.Version}}}")); + } + + serverData.RequiredMods = requiredMods; //FIX THIS - get the mods required + serverData.GameVersion = BuildInfo.BUILD_VERSION_MAJOR.ToString(); + serverData.MultiplayerVersion = Multiplayer.Ver; + + serverData.ServerDetails = details.text.Trim(); + + if (saveGame != null) + { + ISaveGameplayInfo saveGameplayInfo = this.userProvider.GetSaveGameplayInfo(this.saveGame); + if (!saveGameplayInfo.IsCorrupt) + { + serverData.TimePassed = (saveGameplayInfo.InGameDate != DateTime.MinValue) ? saveGameplayInfo.InGameTimePassed.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s") : "N/A"; + serverData.Difficulty = LobbyServerData.GetDifficultyFromString(this.userProvider.GetSessionDifficulty(saveGame.ParentSession).Name); + serverData.GameMode = LobbyServerData.GetGameModeFromString(saveGame.GameMode); + } + } + else if (startGameData != null) + { + serverData.Difficulty = LobbyServerData.GetDifficultyFromString(this.startGameData.difficulty.Name); + serverData.GameMode = LobbyServerData.GetGameModeFromString(startGameData.session.GameMode); + } + + + Multiplayer.Settings.ServerName = serverData.Name; + Multiplayer.Settings.Password = password.text; + Multiplayer.Settings.PublicGame = serverData.isPublic; + Multiplayer.Settings.Port = serverData.port; + Multiplayer.Settings.MaxPlayers = serverData.MaxPlayers; + Multiplayer.Settings.Details = serverData.ServerDetails; + + + //Pass the server data to the NetworkLifecycle manager + NetworkLifecycle.Instance.serverData = serverData; + } + //Mark it as a real multiplayer game + NetworkLifecycle.Instance.IsSinglePlayer = false; + + + var ContinueGameRequested = lcInstance.GetType().GetMethod("OnRunClicked", BindingFlags.NonPublic | BindingFlags.Instance); + + //Multiplayer.Log($"OnRunClicked exists: {ContinueGameRequested != null}"); + ContinueGameRequested?.Invoke(lcInstance, null); + } + + + + #endregion + + +} diff --git a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs index 02a6d6b..2dea38f 100644 --- a/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs +++ b/Multiplayer/Components/MainMenu/MainMenuThingsAndStuff.cs @@ -1,93 +1,150 @@ using System; +using DV.UI; using DV.UIFramework; using DV.Utils; using JetBrains.Annotations; using UnityEngine; -namespace Multiplayer.Components.MainMenu; - -public class MainMenuThingsAndStuff : SingletonBehaviour +namespace Multiplayer.Components.MainMenu { - public PopupManager popupManager; - public Popup renamePopupPrefab; - public Popup okPopupPrefab; - public UIMenuController uiMenuController; - - protected override void Awake() + public class MainMenuThingsAndStuff : SingletonBehaviour { - bool shouldDestroy = false; + public PopupManager popupManager; + //public Popup renamePopupPrefab; + //public Popup okPopupPrefab; + //public Popup yesNoPopupPrefab; + public UIMenuController uiMenuController; + public PopupNotificationReferences references; - if (popupManager == null) + protected override void Awake() { - Multiplayer.LogError("Failed to find PopupManager! Destroying self."); - shouldDestroy = true; + bool shouldDestroy = false; + + popupManager = GameObject.FindObjectOfType(); + references = GameObject.FindObjectOfType(); + + // Check if PopupManager is assigned + if (popupManager == null) + { + Multiplayer.LogError("Failed to find PopupManager! Destroying self."); + shouldDestroy = true; + } + + //// Check if renamePopupPrefab is assigned + //if (renamePopupPrefab == null) + //{ + // Multiplayer.LogError($"{nameof(renamePopupPrefab)} is null! Destroying self."); + // shouldDestroy = true; + //} + + //// Check if okPopupPrefab is assigned + //if (okPopupPrefab == null) + //{ + // Multiplayer.LogError($"{nameof(okPopupPrefab)} is null! Destroying self."); + // shouldDestroy = true; + //} + + // Check if uiMenuController is assigned + if (uiMenuController == null) + { + Multiplayer.LogError($"{nameof(uiMenuController)} is null! Destroying self."); + shouldDestroy = true; + } + + // If all required components are assigned, call base.Awake(), otherwise destroy self + if (!shouldDestroy) + { + base.Awake(); + return; + } + + Destroy(this); } - if (renamePopupPrefab == null) + // Switch to the default menu + public void SwitchToDefaultMenu() { - Multiplayer.LogError($"{nameof(renamePopupPrefab)} is null! Destroying self."); - shouldDestroy = true; + uiMenuController.SwitchMenu(uiMenuController.defaultMenuIndex); } - if (okPopupPrefab == null) + // Switch to a specific menu by index + public void SwitchToMenu(byte index) { - Multiplayer.LogError($"{nameof(okPopupPrefab)} is null! Destroying self."); - shouldDestroy = true; + if (uiMenuController.ActiveIndex == index) + return; + + uiMenuController.SwitchMenu(index); } - if (uiMenuController == null) + // Show the rename popup if possible + [CanBeNull] + public Popup ShowRenamePopup() { - Multiplayer.LogError($"{nameof(uiMenuController)} is null! Destroying self."); - shouldDestroy = true; + Multiplayer.Log("public Popup ShowRenamePopup() ..."); + return ShowPopup(references.popupTextInput); } - if (!shouldDestroy) + // Show the OK popup if possible + [CanBeNull] + public Popup ShowOkPopup() { - base.Awake(); - return; + return ShowPopup(references.popupOk); } - Destroy(this); - } + // Show the Yes No popup if possible + [CanBeNull] + public Popup ShowYesNoPopup() + { + return ShowPopup(references.popupYesNo); + } - public void SwitchToDefaultMenu() - { - uiMenuController.SwitchMenu(uiMenuController.defaultMenuIndex); - } + // Show the Wait Spinner popup if possible + [CanBeNull] + public Popup ShowSpinnerPopup() + { + return ShowPopup(references.popupWaitSpinner); + } + + // Show the Slider popup if possible + [CanBeNull] + public Popup ShowSliderPopup() + { + return ShowPopup(references.popupSlider); + } - public void SwitchToMenu(byte index) - { - uiMenuController.SwitchMenu(index); - } + // Generic method to show a popup if the PopupManager can show it + [CanBeNull] + private Popup ShowPopup(Popup popup) + { + if (popupManager.CanShowPopup()) + return popupManager.ShowPopup(popup); - [CanBeNull] - public Popup ShowRenamePopup() - { - return ShowPopup(renamePopupPrefab); - } + Multiplayer.LogError($"{nameof(PopupManager)} cannot show popup!"); + return null; + } - [CanBeNull] - public Popup ShowOkPopup() - { - return ShowPopup(okPopupPrefab); - } + public void ShowOkPopup(string text, Action onClick) + { + var popup = ShowOkPopup(); + if (popup == null) return; - [CanBeNull] - private Popup ShowPopup(Popup popup) - { - if (popupManager.CanShowPopup()) - return popupManager.ShowPopup(popup); - Multiplayer.LogError($"{nameof(PopupManager)} cannot show popup!"); - return null; - } + popup.labelTMPro.text = text; + popup.Closed += _ => onClick(); + } - /// A function to apply to the MainMenuPopupManager while the object is disabled - public static void Create(Action func) - { - GameObject go = new($"[{nameof(MainMenuThingsAndStuff)}]"); - go.SetActive(false); - MainMenuThingsAndStuff manager = go.AddComponent(); - func.Invoke(manager); - go.SetActive(true); + /// A function to apply to the MainMenuPopupManager while the object is disabled + public static void Create(Action func) + { + // Create a new GameObject for MainMenuThingsAndStuff and disable it + GameObject go = new($"[{nameof(MainMenuThingsAndStuff)}]"); + go.SetActive(false); + + // Add MainMenuThingsAndStuff component and apply the provided function + MainMenuThingsAndStuff manager = go.AddComponent(); + func.Invoke(manager); + + // Re-enable the GameObject + go.SetActive(true); + } } } diff --git a/Multiplayer/Components/MainMenu/MultiplayerPane.cs b/Multiplayer/Components/MainMenu/MultiplayerPane.cs deleted file mode 100644 index be42068..0000000 --- a/Multiplayer/Components/MainMenu/MultiplayerPane.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using DV.UIFramework; -using DV.Utils; -using Multiplayer.Components.Networking; -using UnityEngine; - -namespace Multiplayer.Components.MainMenu; - -public class MultiplayerPane : MonoBehaviour -{ - // @formatter:off - // Patterns from https://ihateregex.io/ - private static readonly Regex IPv4 = new(@"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}"); - private static readonly Regex IPv6 = new(@"(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); - private static readonly Regex PORT = new(@"^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$"); - // @formatter:on - - private bool why; - - private string address; - private ushort port; - - private void OnEnable() - { - if (!why) - { - why = true; - return; - } - - ShowIpPopup(); - } - - private void ShowIpPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; - - popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; - - popup.Closed += result => - { - if (result.closedBy == PopupClosedByAction.Abortion) - { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); - return; - } - - if (!IPv4.IsMatch(result.data) && !IPv6.IsMatch(result.data)) - { - ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); - return; - } - - address = result.data; - - ShowPortPopup(); - }; - } - - private void ShowPortPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; - - popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; - - popup.Closed += result => - { - if (result.closedBy == PopupClosedByAction.Abortion) - { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); - return; - } - - if (!PORT.IsMatch(result.data)) - { - ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowPortPopup); - return; - } - - port = ushort.Parse(result.data); - - ShowPasswordPopup(); - }; - } - - private void ShowPasswordPopup() - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); - if (popup == null) - return; - - popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; - - popup.Closed += result => - { - if (result.closedBy == PopupClosedByAction.Abortion) - { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); - return; - } - - SingletonBehaviour.Instance.StartClient(address, port, result.data); - }; - } - - private static void ShowOkPopup(string text, Action onClick) - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - if (popup == null) - return; - - popup.labelTMPro.text = text; - popup.Closed += _ => { onClick(); }; - } -} diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs new file mode 100644 index 0000000..997e367 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/IServerBrowserGameDetails.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.CompilerServices; +using Newtonsoft.Json.Linq; +using UnityEngine; +using Newtonsoft.Json; + +namespace Multiplayer.Components.MainMenu +{ + // + public interface IServerBrowserGameDetails : IDisposable + { + string id { get; set; } + string ipv6 { get; set; } + string ipv4 { get; set; } + string LocalIPv4 { get; set; } + string LocalIPv6 { get; set; } + int port { get; set; } + string Name { get; set; } + bool HasPassword { get; set; } + int GameMode { get; set; } + int Difficulty { get; set; } + string TimePassed { get; set; } + int CurrentPlayers { get; set; } + int MaxPlayers { get; set; } + string RequiredMods { get; set; } + string GameVersion { get; set; } + string MultiplayerVersion { get; set; } + string ServerDetails { get; set; } + int Ping {get; set; } + bool isPublic { get; set; } + int LastSeen { get; set; } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs b/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs new file mode 100644 index 0000000..170caab --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/PopupTextInputFieldControllerNoValidation.cs @@ -0,0 +1,93 @@ +using System; +using System.Reflection; +using DV.UIFramework; +using TMPro; +using UnityEngine; +using UnityEngine.Events; + +namespace Multiplayer.Components.MainMenu +{ + public class PopupTextInputFieldControllerNoValidation : MonoBehaviour, IPopupSubmitHandler + { + public Popup popup; + public TMP_InputField field; + public ButtonDV confirmButton; + + private void Awake() + { + // Find the components + popup = this.GetComponentInParent(); + field = popup.GetComponentInChildren(); + + foreach (ButtonDV btn in popup.GetComponentsInChildren()) + { + if (btn.name == "ButtonYes") + { + confirmButton = btn; + } + } + + // Set this instance as the new handler for the dialog + typeof(Popup).GetField("handler", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(popup, this); + } + + private void Start() + { + // Add listener for input field value changes + field.onValueChanged.AddListener(new UnityAction(OnInputValueChanged)); + OnInputValueChanged(field.text); + field.Select(); + field.ActivateInputField(); + } + + private void OnInputValueChanged(string value) + { + // Toggle confirm button interactability based on input validity + confirmButton.ToggleInteractable(IsInputValid(value)); + } + + public void HandleAction(PopupClosedByAction action) + { + switch (action) + { + case PopupClosedByAction.Positive: + if (IsInputValid(field.text)) + { + RequestPositive(); + return; + } + break; + case PopupClosedByAction.Negative: + RequestNegative(); + return; + case PopupClosedByAction.Abortion: + RequestAbortion(); + return; + default: + Multiplayer.LogError(string.Format("Unhandled action {0}", action)); + break; + } + } + + private bool IsInputValid(string value) + { + // Always return true to disable validation + return true; + } + + private void RequestPositive() + { + this.popup.RequestClose(PopupClosedByAction.Positive, this.field.text); + } + + private void RequestNegative() + { + this.popup.RequestClose(PopupClosedByAction.Negative, null); + } + + private void RequestAbortion() + { + this.popup.RequestClose(PopupClosedByAction.Abortion, null); + } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs new file mode 100644 index 0000000..bf56f5f --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserDummyElement.cs @@ -0,0 +1,55 @@ +using DV.UI; +using DV.UIFramework; +using DV.Localization; +using Multiplayer.Utils; +using System.ComponentModel; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu.ServerBrowser +{ + public class ServerBrowserDummyElement : AViewElement + { + private TextMeshProUGUI networkName; + + protected override void Awake() + { + // Find and assign TextMeshProUGUI components for displaying server details + GameObject networkNameGO = this.FindChildByName("name [noloc]"); + networkName = networkNameGO.GetComponent(); + this.FindChildByName("date [noloc]").SetActive(false); + this.FindChildByName("time [noloc]").SetActive(false); + this.FindChildByName("autosave icon").SetActive(false); + + //Remove doubled up components + GameObject.Destroy(this.transform.GetComponent()); + GameObject.Destroy(this.transform.GetComponent()); + GameObject.Destroy(this.transform.GetComponent()); + GameObject.Destroy(this.transform.GetComponent()); + + RectTransform networkNameRT = networkNameGO.transform.GetComponent(); + networkNameRT.sizeDelta = new Vector2(600, networkNameRT.sizeDelta.y); + + this.SetInteractable(false); + + Localize loc = networkNameGO.GetOrAddComponent(); + loc.key = Locale.SERVER_BROWSER__NO_SERVERS_KEY ; + loc.UpdateLocalization(); + + this.GetOrAddComponent().enabled = true; + this.gameObject.ResetTooltip(); + + } + + public override void SetData(IServerBrowserGameDetails data, AGridView _) + { + //do nothing + } + + private void UpdateView(object sender = null, PropertyChangedEventArgs e = null) + { + //do nothing + } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs new file mode 100644 index 0000000..f925ec6 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserElement.cs @@ -0,0 +1,132 @@ +using DV.UIFramework; +using Multiplayer.Utils; +using System; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu.ServerBrowser +{ + public class ServerBrowserElement : AViewElement + { + private TextMeshProUGUI networkName; + private TextMeshProUGUI playerCount; + private TextMeshProUGUI ping; + private GameObject goIconPassword; + private Image iconPassword; + private GameObject goIconLAN; + private Image iconLAN; + private IServerBrowserGameDetails data; + + private const int PING_WIDTH = 124; // Adjusted width for the ping text + private const int PING_POS_X = 650; // X position for the ping text + + private const string PING_COLOR_UNKNOWN = "#808080"; + private const string PING_COLOR_EXCELLENT = "#00ff00"; + private const string PING_COLOR_GOOD = "#ffa500"; + private const string PING_COLOR_HIGH = "#ff4500"; + private const string PING_COLOR_POOR = "#ff0000"; + + private const int PING_THRESHOLD_NONE = -1; + private const int PING_THRESHOLD_EXCELLENT = 60; + private const int PING_THRESHOLD_GOOD = 100; + private const int PING_THRESHOLD_HIGH = 150; + + protected override void Awake() + { + // Find and assign TextMeshProUGUI components for displaying server details + networkName = this.FindChildByName("name [noloc]").GetComponent(); + playerCount = this.FindChildByName("date [noloc]").GetComponent(); + ping = this.FindChildByName("time [noloc]").GetComponent(); + goIconPassword = this.FindChildByName("autosave icon"); + iconPassword = goIconPassword.GetComponent(); + + // Fix alignment of the player count text relative to the network name text + Vector3 namePos = networkName.transform.position; + Vector2 nameSize = networkName.rectTransform.sizeDelta; + playerCount.transform.position = new Vector3(namePos.x + nameSize.x, namePos.y, namePos.z); + + // Adjust the size and position of the ping text + Vector2 rowSize = transform.GetComponentInParent().sizeDelta; + Vector3 pingPos = ping.transform.position; + Vector2 pingSize = ping.rectTransform.sizeDelta; + + ping.rectTransform.sizeDelta = new Vector2(PING_WIDTH, pingSize.y); + ping.transform.position = new Vector3(PING_POS_X, pingPos.y, pingPos.z); + ping.alignment = TextAlignmentOptions.Right; + + // Set password icon + iconPassword.sprite = Multiplayer.AssetIndex.lockIcon; + + // Set LAN icon + if(this.HasChildWithName("LAN Icon")) + { + goIconLAN = this.FindChildByName("LAN Icon"); + } + else + { + goIconLAN = Instantiate(goIconPassword, goIconPassword.transform.parent); + goIconLAN.name = "LAN Icon"; + Vector3 LANpos = goIconLAN.transform.localPosition; + Vector3 LANSize = goIconLAN.GetComponent().sizeDelta; + LANpos.x += (PING_POS_X - LANpos.x - LANSize.x) / 2; + goIconLAN.transform.localPosition = LANpos; + iconLAN = goIconLAN.GetComponent(); + iconLAN.sprite = Multiplayer.AssetIndex.lanIcon; + } + + } + + public override void SetData(IServerBrowserGameDetails data, AGridView _) + { + // Clear existing data + if (this.data != null) + { + this.data = null; + } + // Set new data + if (data != null) + { + this.data = data; + } + // Update the view with the new data + UpdateView(); + } + + public void UpdateView() + { + + // Update the text fields with the data from the server + networkName.text = data.Name; + playerCount.text = $"{data.CurrentPlayers} / {data.MaxPlayers}"; + + //if (data.MultiplayerVersion == Multiplayer.Ver) + ping.text = $"{(data.Ping < 0 ? "?" : data.Ping)} ms"; + //else + // ping.text = $"N/A"; + + // Hide the icon if the server does not have a password + goIconPassword.SetActive(data.HasPassword); + + bool isLan = !string.IsNullOrEmpty(data.LocalIPv4) || !string.IsNullOrEmpty(data.LocalIPv6); + goIconLAN.SetActive(isLan); + } + + private string GetColourForPing(int ping) + { + switch (ping) + { + case PING_THRESHOLD_NONE: + return PING_COLOR_UNKNOWN; + case < PING_THRESHOLD_EXCELLENT: + return PING_COLOR_EXCELLENT; + case < PING_THRESHOLD_GOOD: + return PING_COLOR_GOOD; + case < PING_THRESHOLD_HIGH: + return PING_COLOR_HIGH; + default: + return PING_COLOR_POOR; + } + } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs new file mode 100644 index 0000000..ea52694 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowser/ServerBrowserGridView.cs @@ -0,0 +1,44 @@ +using System; +using DV.UI; +using DV.UIFramework; +using Multiplayer.Components.MainMenu.ServerBrowser; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Components.MainMenu +{ + [RequireComponent(typeof(ContentSizeFitter))] + [RequireComponent(typeof(VerticalLayoutGroup))] + // + public class ServerBrowserGridView : AGridView + { + + protected override void Awake() + { + //Multiplayer.Log("serverBrowserGridview Awake()"); + + //copy the copy + this.viewElementPrefab.SetActive(false); + this.dummyElementPrefab = Instantiate(this.viewElementPrefab); + + //swap controllers + GameObject.Destroy(this.viewElementPrefab.GetComponent()); + GameObject.Destroy(this.dummyElementPrefab.GetComponent()); + + this.viewElementPrefab.AddComponent(); + this.dummyElementPrefab.AddComponent(); + + this.viewElementPrefab.name = "prefabServerBrowserElement"; + this.dummyElementPrefab.name = "prefabServerBrowserDummyElement"; + + this.viewElementPrefab.SetActive(true); + this.dummyElementPrefab.SetActive(true); + + } + + public ServerBrowserElement GetElementAt(int index) + { + return transform.GetChild(index + indexOffset).GetComponent(); + } + } +} diff --git a/Multiplayer/Components/MainMenu/ServerBrowserPane.cs b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs new file mode 100644 index 0000000..f3ab768 --- /dev/null +++ b/Multiplayer/Components/MainMenu/ServerBrowserPane.cs @@ -0,0 +1,1097 @@ +using System; +using System.Collections; +using DV.Localization; +using DV.UI; +using DV.UIFramework; +using DV.Util; +using DV.Utils; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using System.Linq; +using Multiplayer.Networking.Data; +using DV; +using System.Net; +using LiteNetLib; +using System.Collections.Generic; +using Steamworks; +using Steamworks.Data; + +namespace Multiplayer.Components.MainMenu +{ + public class ServerBrowserPane : MonoBehaviour + { + private enum ConnectionState + { + NotConnected, + AttemptingSteamRelay, + AttemptingIPv6, + AttemptingIPv6Punch, + AttemptingIPv4, + AttemptingIPv4Punch, + Failed, + Aborted + } + + private const int MAX_PORT_LEN = 5; + private const int MIN_PORT = 1024; + private const int MAX_PORT = 49151; + + //Gridview variables + private readonly ObservableCollectionExt gridViewModel = []; + private ServerBrowserGridView gridView; + private ScrollRect parentScroller; + private string serverIDOnRefresh; + private IServerBrowserGameDetails selectedServer; + + + //ping tracking + private float pingTimer = 0f; + private const float PING_INTERVAL = 2f; // base interval to refresh all pings + + //Button variables + private ButtonDV buttonJoin; + private ButtonDV buttonRefresh; + private ButtonDV buttonDirectIP; + + //Misc GUI Elements + private TextMeshProUGUI serverName; + private TextMeshProUGUI detailsPane; + + //Remote server tracking + private readonly List remoteServers = []; + private bool serverRefreshing = false; + private float timePassed = 0f; //time since last refresh + private const int AUTO_REFRESH_TIME = 30; //how often to refresh in auto + private const int REFRESH_MIN_TIME = 10; //Stop refresh spam + private bool remoteRefreshComplete; + + //connection parameters + private string address; + private int portNumber; + private Lobby? selectedLobby; + private static Lobby? joinedLobby; + public static Lobby? lobbyToJoin; + string password = null; + bool direct = false; + + private ConnectionState connectionState = ConnectionState.NotConnected; + private Popup connectingPopup; + private int attempt; + + private Lobby[] lobbies; + + + #region setup + + public void Awake() + { + Multiplayer.Log("MultiplayerPane Awake()"); + joinedLobby?.Leave(); + joinedLobby = null; + + CleanUI(); + BuildUI(); + + SetupServerBrowser(); + RefreshGridView(); + } + + public void OnEnable() + { + //Multiplayer.Log("MultiplayerPane OnEnable()"); + if (!this.parentScroller) + { + //Multiplayer.Log("Find ScrollRect"); + this.parentScroller = this.gridView.GetComponentInParent(); + //Multiplayer.Log("Found ScrollRect"); + } + this.SetupListeners(true); + this.serverIDOnRefresh = ""; + + buttonDirectIP.ToggleInteractable(true); + buttonRefresh.ToggleInteractable(true); + + RefreshAction(); + } + + // Disable listeners + public void OnDisable() + { + this.SetupListeners(false); + } + + public void Update() + { + SteamClient.RunCallbacks(); + + //Handle server refresh interval + timePassed += Time.deltaTime; + + if (!serverRefreshing) + { + if (timePassed >= AUTO_REFRESH_TIME) + { + RefreshAction(); + } + else if(timePassed >= REFRESH_MIN_TIME) + { + buttonRefresh.ToggleInteractable(true); + } + } + else if(remoteRefreshComplete) + { + RefreshGridView(); + IndexChanged(gridView); //Revalidate any selected servers + remoteRefreshComplete = false; + serverRefreshing = false; + timePassed = 0; + } + + //Handle pinging servers + pingTimer += Time.deltaTime; + + if (pingTimer >= PING_INTERVAL) + { + UpdatePings(); + pingTimer = 0f; + } + + if (lobbyToJoin != null && lobbyToJoin?.Data?.Count() > 0) + { + //For invites + Multiplayer.Log($"Player Invite initiated"); + if (lobbyToJoin != null) + { + direct = false; + selectedLobby = lobbyToJoin; + lobbyToJoin = null; + + string hasPass = selectedLobby?.GetData(SteamworksUtils.LOBBY_HAS_PASSWORD); + Multiplayer.Log($"Player Invite ({selectedLobby?.Id}) Has Password: {hasPass}"); + + if (string.IsNullOrEmpty(hasPass)) + { + Multiplayer.Log($"Player Invite ({selectedLobby?.Id}) Attempting connection..."); + AttemptConnection(); + } + else + { + Multiplayer.Log($"Player Invite ({selectedLobby?.Id}) Ask Password..."); + ShowPasswordPopup(); + } + } + } + } + + private void CleanUI() + { + GameObject.Destroy(this.FindChildByName("Text Content")); + + GameObject.Destroy(this.FindChildByName("HardcoreSavingBanner")); + GameObject.Destroy(this.FindChildByName("TutorialSavingBanner")); + + GameObject.Destroy(this.FindChildByName("Thumbnail")); + + GameObject.Destroy(this.FindChildByName("ButtonIcon OpenFolder")); + GameObject.Destroy(this.FindChildByName("ButtonIcon Rename")); + GameObject.Destroy(this.FindChildByName("ButtonTextIcon Load")); + + } + private void BuildUI() + { + + // Update title + GameObject titleObj = this.FindChildByName("Title"); + GameObject.Destroy(titleObj.GetComponentInChildren()); + titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; + titleObj.GetComponentInChildren().UpdateLocalization(); + + //Rebuild the save description pane + GameObject serverWindowGO = this.FindChildByName("Save Description"); + GameObject serverNameGO = serverWindowGO.FindChildByName("text list [noloc]"); + GameObject scrollViewGO = this.FindChildByName("Scroll View"); + + //Create new objects + GameObject serverScroll = Instantiate(scrollViewGO, serverNameGO.transform.position, Quaternion.identity, serverWindowGO.transform); + + + /* + * Setup server name + */ + serverNameGO.name = "Server Title"; + + //Positioning + RectTransform serverNameRT = serverNameGO.GetComponent(); + serverNameRT.pivot = new Vector2(1f, 1f); + serverNameRT.anchorMin = new Vector2(0f, 1f); + serverNameRT.anchorMax = new Vector2(1f, 1f); + serverNameRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, 54); + + //Text + serverName = serverNameGO.GetComponentInChildren(); + serverName.alignment = TextAlignmentOptions.Center; + serverName.textWrappingMode = TextWrappingModes.Normal; + serverName.fontSize = 22; + serverName.text = Locale.SERVER_BROWSER__INFO_TITLE;// "Server Browser Info"; + + /* + * Setup server details + */ + + // Create new ScrollRect object + GameObject viewport = serverScroll.FindChildByName("Viewport"); + serverScroll.transform.SetParent(serverWindowGO.transform, false); + + // Positioning ScrollRect + RectTransform serverScrollRT = serverScroll.GetComponent(); + serverScrollRT.pivot = new Vector2(1f, 1f); + serverScrollRT.anchorMin = new Vector2(0f, 1f); + serverScrollRT.anchorMax = new Vector2(1f, 1f); + serverScrollRT.localEulerAngles = Vector3.zero; + serverScrollRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 54, 400); + serverScrollRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, serverNameGO.GetComponent().rect.width); + + RectTransform viewportRT = viewport.GetComponent(); + + // Assign Viewport to ScrollRect + ScrollRect scrollRect = serverScroll.GetComponent(); + scrollRect.viewport = viewportRT; + + // Create Content + GameObject.Destroy(serverScroll.FindChildByName("GRID VIEW").gameObject); + GameObject content = new("Content", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup)); + content.transform.SetParent(viewport.transform, false); + ContentSizeFitter contentSF = content.GetComponent(); + contentSF.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + VerticalLayoutGroup contentVLG = content.GetComponent(); + contentVLG.childControlWidth = true; + contentVLG.childControlHeight = true; + RectTransform contentRT = content.GetComponent(); + contentRT.pivot = new Vector2(0f, 1f); + contentRT.anchorMin = new Vector2(0f, 1f); + contentRT.anchorMax = new Vector2(1f, 1f); + contentRT.offsetMin = Vector2.zero; + contentRT.offsetMax = Vector2.zero; + scrollRect.content = contentRT; + + // Create TextMeshProUGUI object + GameObject textContainerGO = new ("Details Container", typeof(HorizontalLayoutGroup)); + textContainerGO.transform.SetParent(content.transform, false); + contentRT.localPosition = new Vector3(contentRT.localPosition.x + 10, contentRT.localPosition.y, contentRT.localPosition.z); + + + GameObject textGO = new("Details Text", typeof(TextMeshProUGUI)); + textGO.transform.SetParent(textContainerGO.transform, false); + HorizontalLayoutGroup textHLG = textGO.GetComponent(); + detailsPane = textGO.GetComponent(); + detailsPane.textWrappingMode = TextWrappingModes.Normal; + detailsPane.fontSize = 18; + detailsPane.text = Locale.Get(Locale.SERVER_BROWSER__INFO_CONTENT_KEY, [AUTO_REFRESH_TIME, REFRESH_MIN_TIME]);// "Welcome to Derail Valley Multiplayer Mod!

The server list refreshes automatically every 30 seconds, but you can refresh manually once every 10 seconds."; + + // Adjust text RectTransform to fit content + RectTransform textRT = textGO.GetComponent(); + textRT.pivot = new Vector2(0.5f, 1f); + textRT.anchorMin = new Vector2(0, 1); + textRT.anchorMax = new Vector2(1, 1); + textRT.offsetMin = new Vector2(0, -detailsPane.preferredHeight); + textRT.offsetMax = new Vector2(0, 0); + + // Set content size to fit text + contentRT.sizeDelta = new Vector2(contentRT.sizeDelta.x -50, detailsPane.preferredHeight); + + // Update buttons on the multiplayer pane + GameObject goDirectIP = this.gameObject.UpdateButton("ButtonTextIcon Overwrite", "ButtonTextIcon Manual", Locale.SERVER_BROWSER__MANUAL_CONNECT_KEY, null, Multiplayer.AssetIndex.multiplayerIcon); + GameObject goJoin = this.gameObject.UpdateButton("ButtonTextIcon Save", "ButtonTextIcon Join", Locale.SERVER_BROWSER__JOIN_KEY, null, Multiplayer.AssetIndex.connectIcon); + GameObject goRefresh = this.gameObject.UpdateButton("ButtonIcon Delete", "ButtonIcon Refresh", Locale.SERVER_BROWSER__REFRESH_KEY, null, Multiplayer.AssetIndex.refreshIcon); + + + if (goDirectIP == null || goJoin == null || goRefresh == null) + { + Multiplayer.LogError("One or more buttons not found."); + return; + } + + // Set up event listeners + buttonDirectIP = goDirectIP.GetComponent(); + buttonDirectIP.onClick.AddListener(DirectAction); + + buttonJoin = goJoin.GetComponent(); + buttonJoin.onClick.AddListener(JoinAction); + + buttonRefresh = goRefresh.GetComponent(); + buttonRefresh.onClick.AddListener(RefreshAction); + + //Lock out the join button until a server has been selected + buttonJoin.ToggleInteractable(false); + } + private void SetupServerBrowser() + { + GameObject GridviewGO = this.FindChildByName("Scroll View").FindChildByName("GRID VIEW"); + + //Disable before we make any changes + GridviewGO.SetActive(false); + + + //load our custom controller + SaveLoadGridView slgv = GridviewGO.GetComponent(); + gridView = GridviewGO.AddComponent(); + + //grab the original prefab + slgv.viewElementPrefab.SetActive(false); + gridView.viewElementPrefab = Instantiate(slgv.viewElementPrefab); + slgv.viewElementPrefab.SetActive(true); + + //Remove original controller + GameObject.Destroy(slgv); + + //Don't forget to re-enable! + GridviewGO.SetActive(true); + + gridView.showDummyElement = true; + } + private void SetupListeners(bool on) + { + if (on) + { + this.gridView.SelectedIndexChanged += this.IndexChanged; + } + else + { + this.gridView.SelectedIndexChanged -= this.IndexChanged; + } + } + #endregion + + #region UI callbacks + private void RefreshAction() + { + if (serverRefreshing) + return; + + if (selectedServer != null) + serverIDOnRefresh = selectedServer.id; + + remoteServers.Clear(); + + serverRefreshing = true; + buttonJoin.ToggleInteractable(false); + buttonRefresh.ToggleInteractable(false); + + if (DVSteamworks.Success) + ListActiveLobbies(); + + } + private void JoinAction() + { + if (selectedServer != null) + { + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(false); + + //not making a direct connection + direct = false; + portNumber = -1; + var lobby = GetLobbyFromServer(selectedServer); + if (lobby != null) + { + selectedLobby = (Lobby)lobby; + password = null; //clear the password + + if (selectedServer.HasPassword) + { + ShowPasswordPopup(); + return; + } + + AttemptConnection(); + + } + } + } + + private void DirectAction() + { + //Debug.Log($"DirectAction()"); + buttonDirectIP.ToggleInteractable(false); + buttonJoin.ToggleInteractable(false) ; + + //making a direct connection + direct = true; + password = null; + + //ShowSteamID(); + ShowIpPopup(); + } + + private void IndexChanged(AGridView gridView) + { + if (serverRefreshing) + return; + + if (gridView.SelectedModelIndex >= 0) + { + selectedServer = gridViewModel[gridView.SelectedModelIndex]; + + UpdateDetailsPane(); + + //Check if we can connect to this server + Multiplayer.Log($"Server: \"{selectedServer.GameVersion}\" \"{selectedServer.MultiplayerVersion}\""); + Multiplayer.Log($"Client: \"{BuildInfo.BUILD_VERSION_MAJOR}\" \"{Multiplayer.Ver}\""); + Multiplayer.Log($"Result: \"{selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString()}\" \"{selectedServer.MultiplayerVersion == Multiplayer.Ver}\""); + + bool canConnect = selectedServer.GameVersion == BuildInfo.BUILD_VERSION_MAJOR.ToString() && + selectedServer.MultiplayerVersion == Multiplayer.Ver; + + buttonJoin.ToggleInteractable(canConnect); + } + else + { + buttonJoin.ToggleInteractable(false); + } + } + + private void UpdateElement(IServerBrowserGameDetails element) + { + int index = gridViewModel.IndexOf(element); + + if (index >= 0) + { + var viewElement = gridView.GetElementAt(index); + viewElement?.UpdateView(); + } + } + #endregion + + private void UpdateDetailsPane() + { + string details; + + if (selectedServer != null) + { + serverName.text = selectedServer.Name; + + //note: built-in localisations have a trailing colon e.g. 'Game mode:' + + details = "" + LocalizationAPI.L("launcher/game_mode", []) + " " + LobbyServerData.GetGameModeFromInt(selectedServer.GameMode) + "
"; + details += "" + LocalizationAPI.L("launcher/difficulty", []) + " " + LobbyServerData.GetDifficultyFromInt(selectedServer.Difficulty) + "
"; + details += "" + LocalizationAPI.L("launcher/in_game_time_passed", []) + " " + selectedServer.TimePassed + "
"; + details += "" + Locale.SERVER_BROWSER__PLAYERS + ": " + selectedServer.CurrentPlayers + '/' + selectedServer.MaxPlayers + "
"; + details += "" + Locale.SERVER_BROWSER__PASSWORD_REQUIRED + ": " + (selectedServer.HasPassword ? Locale.SERVER_BROWSER__YES : Locale.SERVER_BROWSER__NO) + "
"; + details += "" + Locale.SERVER_BROWSER__MODS_REQUIRED + ": " + (string.IsNullOrEmpty(selectedServer.RequiredMods) ? Locale.SERVER_BROWSER__NO : Locale.SERVER_BROWSER__YES) + "
"; + details += "
"; + details += "" + Locale.SERVER_BROWSER__GAME_VERSION + ": " + (selectedServer.GameVersion != BuildInfo.BUILD_VERSION_MAJOR.ToString() ? "" : "") + selectedServer.GameVersion + "
"; + details += "" + Locale.SERVER_BROWSER__MOD_VERSION + ": " + (selectedServer.MultiplayerVersion != Multiplayer.Ver ? "" : "") + selectedServer.MultiplayerVersion + "
"; + details += "
"; + details += selectedServer.ServerDetails; + + //Multiplayer.Log("Finished Prepping Data"); + detailsPane.text = details; + } + } + + private void ShowIpPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) + { + Multiplayer.LogError("Popup not found."); + return; + } + + popup.labelTMPro.text = Locale.SERVER_BROWSER__IP; + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemoteIP; + + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) + { + buttonDirectIP.ToggleInteractable(true); + IndexChanged(gridView); //re-enable the join button if a valid gridview item is selected + return; + } + + if (!IPAddress.TryParse(result.data, out IPAddress parsedAddress)) + { + string inputUrl = result.data; + + if (!inputUrl.StartsWith("http://") && !inputUrl.StartsWith("https://")) + { + inputUrl = "http://" + inputUrl; + } + + bool isValidURL = Uri.TryCreate(inputUrl, UriKind.Absolute, out Uri uriResult) + && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); + + if (isValidURL) + { + string domainName = ExtractDomainName(result.data); + try + { + IPHostEntry hostEntry = Dns.GetHostEntry(domainName); + IPAddress[] addresses = hostEntry.AddressList; + + if (addresses.Length > 0) + { + string address2 = addresses[0].ToString(); + + address = address2; + Multiplayer.Log(address); + + ShowPortPopup(); + return; + } + } + catch (Exception ex) + { + Multiplayer.LogError($"An error occurred: {ex.Message}"); + } + } + + MainMenuThingsAndStuff.Instance.ShowOkPopup(Locale.SERVER_BROWSER__IP_INVALID, ShowIpPopup); + } + else + { + if (parsedAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + connectionState = ConnectionState.AttemptingIPv4; + else + connectionState = ConnectionState.AttemptingIPv6; + + address = result.data; + ShowPortPopup(); + } + }; + } + + //private void ShowSteamID() + //{ + // var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + // if (popup == null) + // { + // Multiplayer.LogError("Popup not found."); + // return; + // } + + // popup.labelTMPro.text = "SteamID"; + // //popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemoteIP; + + // popup.Closed += result => + // { + // if (result.closedBy == PopupClosedByAction.Abortion) + // { + // buttonDirectIP.ToggleInteractable(true); + // IndexChanged(gridView); //re-enable the join button if a valid gridview item is selected + // return; + // } + + // steamId = popup.GetComponentInChildren().text; + // Multiplayer.LogDebug(() => $"Attempting to connecto SteamID: {steamId}"); + + // ShowPasswordPopup(); + // }; + //} + + private void ShowPortPopup() + { + + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) + { + Multiplayer.LogError("Popup not found."); + return; + } + + popup.labelTMPro.text = Locale.SERVER_BROWSER__PORT; + popup.GetComponentInChildren().text = $"{Multiplayer.Settings.LastRemotePort}"; + popup.GetComponentInChildren().contentType = TMP_InputField.ContentType.IntegerNumber; + popup.GetComponentInChildren().characterLimit = MAX_PORT_LEN; + + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) + { + buttonDirectIP.ToggleInteractable(true); + return; + } + + if (!int.TryParse(result.data, out portNumber) || portNumber < MIN_PORT || portNumber > MAX_PORT) + { + MainMenuThingsAndStuff.Instance.ShowOkPopup(Locale.SERVER_BROWSER__PORT_INVALID, ShowIpPopup); + } + else + { + ShowPasswordPopup(); + } + + }; + + } + + private void ShowPasswordPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowRenamePopup(); + if (popup == null) + { + Multiplayer.LogError("Popup not found."); + return; + } + + popup.labelTMPro.text = Locale.SERVER_BROWSER__PASSWORD; + + //direct IP connection + if (direct) + { + //Prefill with stored password + popup.GetComponentInChildren().text = Multiplayer.Settings.LastRemotePassword; + + //Set us up to allow a blank password + DestroyImmediate(popup.GetComponentInChildren()); + popup.GetOrAddComponent(); + } + + popup.Closed += result => + { + if (result.closedBy == PopupClosedByAction.Abortion) + { + buttonDirectIP.ToggleInteractable(true); + return; + } + + if (direct) + { + //store params for later + Multiplayer.Settings.LastRemoteIP = address; + Multiplayer.Settings.LastRemotePort = portNumber; + Multiplayer.Settings.LastRemotePassword = result.data; + } + + password = result.data; + + AttemptConnection(); + }; + } + + public void ShowConnectingPopup() + { + var popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); + + if (popup == null) + { + Multiplayer.LogError("ShowConnectingPopup() Popup not found."); + return; + } + + connectingPopup = popup; + + Localize loc = popup.positiveButton.GetComponentInChildren(); + loc.key ="cancel"; + loc.UpdateLocalization(); + + + popup.labelTMPro.text = $"Connecting, please wait..."; //to be localised + + popup.Closed += (PopupResult result) => + { + connectionState = ConnectionState.Aborted; + }; + + } + + #region workflow + private void UpdatePings() + { + UpdatePingsSteam(); + } + + private async void AttemptConnection() + { + + Multiplayer.Log($"AttemptConnection Direct: {direct}, Address: {address}, Lobby: {selectedLobby?.Id.ToString()}"); + + attempt = 0; + connectionState = ConnectionState.NotConnected; + ShowConnectingPopup(); + + if (!direct) + { + if(selectedLobby != null) + { + joinedLobby = selectedLobby; //store the lobby for when we disconnect + + connectionState = ConnectionState.AttemptingSteamRelay; + + var joinResult = await joinedLobby?.Join(); + if (joinResult == RoomEnter.Success) + { + string hostId = ((Lobby)joinedLobby).Owner.Id.Value.ToString(); + NetworkLifecycle.Instance.StartClient(hostId, -1, password, false, OnDisconnect); + } + else + { + Multiplayer.LogDebug(() => "AttemptConnection() Leaving Lobby"); + joinedLobby?.Leave(); + joinedLobby = null; + Multiplayer.Log($"Failed to join lobby: {joinResult}"); + AttemptFail(); + } + + return; + } + } + + Multiplayer.Log($"AttemptConnection address: {address}"); + + if (IPAddress.TryParse(address, out IPAddress IPaddress)) + { + Multiplayer.Log($"AttemptConnection tryParse: {IPaddress.AddressFamily}"); + + if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + AttemptIPv4(); + } + else if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6) + { + AttemptIPv6(); + } + + return; + } + + Multiplayer.LogError($"IP address invalid: {address}"); + + AttemptFail(); + } + + private void AttemptIPv6() + { + Multiplayer.Log($"AttemptIPv6() {address}"); + + if (connectionState == ConnectionState.Aborted) + return; + + attempt++; + if (connectingPopup != null) + connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + + Multiplayer.Log($"AttemptIPv6() starting attempt"); + connectionState = ConnectionState.AttemptingIPv6; + SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + + } + + //private void AttemptIPv6Punch() + //{ + // Multiplayer.Log($"AttemptIPv6Punch() {address}"); + + // if (connectionState == ConnectionState.Aborted) + // return; + + // attempt++; + // if (connectingPopup != null) + // connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + + // //punching not implemented we'll just try again for now + // connectionState = ConnectionState.AttemptingIPv6Punch; + // SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + + //} + private void AttemptIPv4() + { + Multiplayer.Log($"AttemptIPv4() {address}, {connectionState}"); + + if (connectionState == ConnectionState.Aborted) + return; + + attempt++; + if (connectingPopup != null) + connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + + if (!direct) + { + if (selectedServer.ipv4 == null || selectedServer.ipv4 == string.Empty) + { + AttemptFail(); + return; + } + + address = selectedServer.ipv4; + } + + Multiplayer.Log($"AttemptIPv4() {address}"); + + if (IPAddress.TryParse(address, out IPAddress IPaddress)) + { + Multiplayer.Log($"AttemptIPv4() TryParse passed"); + if (IPaddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + Multiplayer.Log($"AttemptIPv4() starting attempt"); + connectionState = ConnectionState.AttemptingIPv4; + SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + return; + } + } + + Multiplayer.Log($"AttemptIPv4() TryParse failed"); + AttemptFail(); + string message = "Host Unreachable"; + MainMenuThingsAndStuff.Instance.ShowOkPopup(message, () => { }); + } + + //private void AttemptIPv4Punch() + //{ + // Multiplayer.Log($"AttemptIPv4Punch() {address}"); + + // if (connectionState == ConnectionState.Aborted) + // return; + + // attempt++; + // if (connectingPopup != null) + // connectingPopup.labelTMPro.text = $"Connecting, please wait...\r\nAttempt: {attempt}"; + + // //punching not implemented we'll just try again for now + // connectionState = ConnectionState.AttemptingIPv4Punch; + // SingletonBehaviour.Instance.StartClient(address, portNumber, password, false, OnDisconnect); + //} + + private void AttemptFail() + { + connectionState = ConnectionState.Failed; + + if (connectingPopup != null) + { + connectingPopup.RequestClose(PopupClosedByAction.Abortion, null); + connectingPopup = null; // Clear the reference + } + + if (gameObject != null && gameObject.activeInHierarchy) + { + if (gridView != null) + IndexChanged(gridView); + + if (buttonDirectIP != null && buttonDirectIP.gameObject != null) + buttonDirectIP.ToggleInteractable(true); + } + } + + private void OnDisconnect(DisconnectReason reason, string message) + { + Multiplayer.Log($"Disconnected due to: {reason}, \"{message}\""); + + string displayMessage = message; + + Multiplayer.LogDebug(() => "OnDisconnect() Leaving Lobby"); + joinedLobby?.Leave(); + joinedLobby = null; + + if (string.IsNullOrEmpty(message)) + { + //fallback for no message (server initiated disconnects should have a message) + //if (reason == DisconnectReason.ConnectionFailed) + //{ + // switch (connectionState) + // { + // case ConnectionState.AttemptingIPv6: + // if (Multiplayer.Settings.EnableNatPunch) + // AttemptIPv6Punch(); + // else + // AttemptIPv4(); + // return; + // case ConnectionState.AttemptingIPv6Punch: + // AttemptIPv4(); + // return; + // case ConnectionState.AttemptingIPv4: + // if (Multiplayer.Settings.EnableNatPunch) + // { + // AttemptIPv4Punch(); + // return; + // } + // break; + // } + //} + + displayMessage = GetDisplayMessageForDisconnect(reason); + AttemptFail(); + } + else + { + connectionState = ConnectionState.NotConnected; + } + + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + Multiplayer.LogDebug(() => "OnDisconnect() Queuing"); + MainMenuThingsAndStuff.Instance?.ShowOkPopup(displayMessage, () => { }); + }); + } + + private string GetDisplayMessageForDisconnect(DisconnectReason reason) + { + return reason switch + { + DisconnectReason.UnknownHost => "Unknown Host", + DisconnectReason.DisconnectPeerCalled => "Player Kicked", + DisconnectReason.ConnectionFailed => "Host Unreachable", + DisconnectReason.ConnectionRejected => "Rejected!", + DisconnectReason.RemoteConnectionClose => "Server Shutting Down", + DisconnectReason.Timeout => "Server Timed Out", + _ => "Connection Failed" + }; + } + #endregion + + + #region steam lobby + private async void ListActiveLobbies() + { + lobbies = await SteamMatchmaking.LobbyList.WithMaxResults(100) + //.WithKeyValue(SteamworksUtils.MP_MOD_KEY, string.Empty) + .RequestAsync(); + + Multiplayer.LogDebug(() => $"ListActiveLobbies() lobbies found: {lobbies?.Count()}"); + + remoteServers.Clear(); + + if (lobbies != null) + { + var myLoc = SteamNetworkingUtils.LocalPingLocation; + + foreach (var lobby in lobbies) + { + LobbyServerData server = SteamworksUtils.GetLobbyData(lobby); + + server.id = lobby.Id.ToString(); + + server.CurrentPlayers = lobby.MemberCount; + server.MaxPlayers = lobby.MaxMembers; + + remoteServers.Add(server); + + Multiplayer.LogDebug(() => $"ListActiveLobbies() lobby {server.Name}, {lobby.MemberCount}/{lobby.MaxMembers}"); + + } + } + remoteRefreshComplete = true; + } + + private void UpdatePingsSteam() + { + foreach (var server in gridViewModel) + { + if (server is LobbyServerData lobbyServer) + { + if (ulong.TryParse(server.id,out ulong id)) + { + Lobby? lobby = lobbies.FirstOrDefault(l => l.Id.Value == id); + if (lobby != null) + { + string strLoc = ((Lobby)lobby).GetData(SteamworksUtils.LOBBY_NET_LOCATION_KEY); + NetPingLocation? location = NetPingLocation.TryParseFromString(strLoc); + + if (location != null) + server.Ping = SteamNetworkingUtils.EstimatePingTo((NetPingLocation)location) / 2; //normalise to one way ping + } + } + + UpdateElement(lobbyServer); + } + } + } + + private Lobby? GetLobbyFromServer(IServerBrowserGameDetails server) + { + if (ulong.TryParse(server.id, out ulong id)) + return lobbies.FirstOrDefault(l => l.Id.Value == id); + + return null; + } + #endregion + private void RefreshGridView() + { + + var allServers = new List(); + allServers.AddRange(remoteServers); + + // Get all active IDs + List activeIDs = allServers.Select(s => s.id).Distinct().ToList(); + + // Find servers to remove + List removeList = gridViewModel.Where(gv => !activeIDs.Contains(gv.id)).ToList(); + + // Remove expired servers + foreach (var remove in removeList) + { + gridViewModel.Remove(remove); + } + + // Update existing servers and add new ones + foreach (var server in allServers) + { + var existingServer = gridViewModel.FirstOrDefault(gv => gv.id == server.id); + if (existingServer != null) + { + // Update existing server + existingServer.TimePassed = server.TimePassed; + existingServer.CurrentPlayers = server.CurrentPlayers; + existingServer.LocalIPv4 = server.LocalIPv4; + existingServer.LastSeen = server.LastSeen; + } + else + { + // Add new server + gridViewModel.Add(server); + } + } + + if (gridViewModel.Count() == 0) + { + gridView.showDummyElement = true; + buttonJoin.ToggleInteractable(false); + } + else + { + gridView.showDummyElement = false; + } + + //Update the gridview rendering + gridView.SetModel(gridViewModel); + + //if we have a server selected, we need to re-select it after refresh + if (serverIDOnRefresh != null) + { + int selID = Array.FindIndex(gridViewModel.ToArray(), server => server.id == serverIDOnRefresh); + if (selID >= 0) + { + gridView.SetSelected(selID); + + if (this.parentScroller) + { + this.parentScroller.verticalNormalizedPosition = 1f - (float)selID / (float)gridView.Model.Count; + } + } + serverIDOnRefresh = null; + } + } + + private string ExtractDomainName(string input) + { + if (input.StartsWith("http://")) + { + input = input.Substring(7); + } + else if (input.StartsWith("https://")) + { + input = input.Substring(8); + } + + int portIndex = input.IndexOf(':'); + if (portIndex != -1) + { + input = input.Substring(0, portIndex); + } + + return input; + } + } +} diff --git a/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs new file mode 100644 index 0000000..2f3232d --- /dev/null +++ b/Multiplayer/Components/Networking/Jobs/NetworkedJob.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using DV.Logic.Job; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; +using Newtonsoft.Json.Linq; +using UnityEngine; + + +namespace Multiplayer.Components.Networking.Jobs; + +public class NetworkedJob : IdMonoBehaviour +{ + #region Lookup Cache + + private static readonly Dictionary jobToNetworkedJob = new(); + private static readonly Dictionary jobIdToNetworkedJob = new(); + private static readonly Dictionary jobIdToJob = new(); + + public static bool Get(ushort netId, out NetworkedJob obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedJob)rawObj; + return b; + } + + public static bool GetJob(ushort netId, out Job obj) + { + bool b = Get(netId, out NetworkedJob networkedJob); + obj = b ? networkedJob.Job : null; + return b; + } + + public static bool TryGetFromJob(Job job, out NetworkedJob networkedJob) + { + return jobToNetworkedJob.TryGetValue(job, out networkedJob); + } + + public static bool TryGetFromJobId(string jobId, out NetworkedJob networkedJob) + { + return jobIdToNetworkedJob.TryGetValue(jobId, out networkedJob); + } + + #endregion + protected override bool IsIdServerAuthoritative => true; + public enum DirtyCause + { + JobOverview, + JobBooklet, + JobReport, + JobState + } + + public Job Job { get; private set; } + public NetworkedStationController Station { get; private set; } + + private NetworkedItem _jobOverview; + public NetworkedItem JobOverview + { + get => _jobOverview; + set + { + if (value != null && value.GetTrackedItem() == null) + return; + + _jobOverview = value; + + if (value != null) + { + Cause = DirtyCause.JobOverview; + OnJobDirty?.Invoke(this); + } + } + } + + private NetworkedItem _jobBooklet; + public NetworkedItem JobBooklet + { + get => _jobBooklet; + set + { + if (value != null && value.GetTrackedItem() == null) + return; + + _jobBooklet = value; + if (value != null) + { + Cause = DirtyCause.JobBooklet; + OnJobDirty?.Invoke(this); + } + } + } + private NetworkedItem _jobReport; + public NetworkedItem JobReport + { + get => _jobReport; + set + { + if (value != null && value.GetTrackedItem() == null) + return; + + _jobReport = value; + if (value != null) + { + Cause = DirtyCause.JobReport; + OnJobDirty?.Invoke(this); + } + } + } + + private List JobReports = new List(); + + public Guid OwnedBy { get; set; } = Guid.Empty; + public JobValidator JobValidator { get; set; } + + public bool ValidatorRequestSent { get; set; } = false; + public bool ValidatorResponseReceived { get; set; } = false; + public bool ValidationAccepted { get; set; } = false; + public ValidationType ValidationType { get; set; } + + public DirtyCause Cause { get; private set; } + + public Action OnJobDirty; + + public List JobCars = []; + + protected override void Awake() + { + base.Awake(); + } + + private void Start() + { + if (Job != null) + { + AddToCache(); + } + else + { + Multiplayer.LogError($"NetworkedJob Start(): Job is null for {gameObject.name}"); + } + } + + public void Initialize(Job job, NetworkedStationController station) + { + Job = job; + Station = station; + + // Setup handlers + job.JobTaken += OnJobTaken; + job.JobAbandoned += OnJobAbandoned; + job.JobCompleted += OnJobCompleted; + job.JobExpired += OnJobExpired; + + // If this is called after Start(), we need to add to cache here + if (gameObject.activeInHierarchy) + { + AddToCache(); + } + } + + private void AddToCache() + { + jobToNetworkedJob[Job] = this; + jobIdToNetworkedJob[Job.ID] = this; + jobIdToJob[Job.ID] = Job; + //Multiplayer.Log($"NetworkedJob added to cache: {Job.ID}"); + } + + private void OnJobTaken(Job job, bool viaLoadGame) + { + if (viaLoadGame) + return; + + Cause = DirtyCause.JobState; + OnJobDirty?.Invoke(this); + } + + private void OnJobAbandoned(Job job) + { + Cause = DirtyCause.JobState; + OnJobDirty?.Invoke(this); + } + + private void OnJobCompleted(Job job) + { + Cause = DirtyCause.JobState; + OnJobDirty?.Invoke(this); + } + + private void OnJobExpired(Job job) + { + Cause = DirtyCause.JobState; + OnJobDirty?.Invoke(this); + } + + public void AddReport(NetworkedItem item) + { + if (item == null || !item.UsefulItem) + { + Multiplayer.LogError($"Attempted to add a null or uninitialised report: JobId: {Job?.ID}, JobNetID: {NetId}"); + return; + } + + Type reportType = item.TrackedItemType; + if( reportType == typeof(JobReport) || + reportType == typeof(JobExpiredReport) || + reportType == typeof(JobMissingLicenseReport) /*|| + reportType == typeof(Debtre) ||*/ + ) + { + JobReports.Add(item); + Cause = DirtyCause.JobReport; + OnJobDirty?.Invoke(this); + } + } + + public void RemoveReport(NetworkedItem item) + { + + } + + public void ClearReports() + { + foreach (var report in JobReports) + { + Destroy(report.gameObject); + } + + JobReports.Clear(); + } + + private void OnDisable() + { + if (UnloadWatcher.isQuitting || UnloadWatcher.isUnloading) + return; + + // Remove from lookup caches + jobToNetworkedJob.Remove(Job); + jobIdToNetworkedJob.Remove(Job.ID); + jobIdToJob.Remove(Job.ID); + + // Unsubscribe from events + Job.JobTaken -= OnJobTaken; + Job.JobAbandoned -= OnJobAbandoned; + Job.JobCompleted -= OnJobCompleted; + Job.JobExpired -= OnJobExpired; + + Destroy(this); + } +} diff --git a/Multiplayer/Components/Networking/NetworkLifecycle.cs b/Multiplayer/Components/Networking/NetworkLifecycle.cs index 7c14288..d3917a9 100644 --- a/Multiplayer/Components/Networking/NetworkLifecycle.cs +++ b/Multiplayer/Components/Networking/NetworkLifecycle.cs @@ -1,13 +1,21 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Net; using System.Text; using DV.Scenarios.Common; using DV.Utils; using LiteNetLib; using LiteNetLib.Utils; -using Multiplayer.Networking.Listeners; +using Multiplayer.Components.Networking.UI; +using Multiplayer.Networking.Data; +using Multiplayer.Networking.Managers; +using Multiplayer.Networking.Managers.Client; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; +using Newtonsoft.Json; +using Steamworks; using UnityEngine; using UnityEngine.SceneManagement; @@ -19,6 +27,11 @@ public class NetworkLifecycle : SingletonBehaviour public const byte TICK_RATE = 24; private const float TICK_INTERVAL = 1.0f / TICK_RATE; + public LobbyServerData serverData; + public bool IsPublicGame { get; set; } = false; + public bool IsSinglePlayer { get; set; } = true; + + public NetworkServer Server { get; private set; } public NetworkClient Client { get; private set; } @@ -28,7 +41,7 @@ public class NetworkLifecycle : SingletonBehaviour public bool IsServerRunning => Server?.IsRunning ?? false; public bool IsClientRunning => Client?.IsRunning ?? false; - public bool IsProcessingPacket => Client.IsProcessingPacket; + public bool IsProcessingPacket => Client?.IsProcessingPacket ?? false; private PlayerListGUI playerList; private NetworkStatsGui Stats; @@ -36,12 +49,12 @@ public class NetworkLifecycle : SingletonBehaviour private readonly ExecutionTimer tickWatchdog = new(0.25f); /// - /// Whether the provided NetPeer is the host. + /// Whether the provided ITransportPeer is the host. /// Note that this does NOT check authority, and should only be used for client-only logic. /// - public bool IsHost(NetPeer peer) + public bool IsHost(ITransportPeer peer) { - return Server?.IsRunning == true && Client?.IsRunning == true && Client?.selfPeer?.Id == peer?.Id; + return Server?.IsRunning == true && Client?.IsRunning == true && Client?.SelfPeer?.Id == peer?.Id; } /// @@ -50,7 +63,7 @@ public bool IsHost(NetPeer peer) /// public bool IsHost() { - return IsHost(Client?.selfPeer); + return IsHost(Client?.SelfPeer); } private readonly Queue mainMenuLoadedQueue = new(); @@ -60,30 +73,20 @@ protected override void Awake() base.Awake(); playerList = gameObject.AddComponent(); Stats = gameObject.AddComponent(); - RegisterPackets(); + //RegisterPackets(); WorldStreamingInit.LoadingFinished += () => { playerList.RegisterListeners(); }; Settings.OnSettingsUpdated += OnSettingsUpdated; SceneManager.sceneLoaded += (scene, _) => { if (scene.buildIndex != (int)DVScenes.MainMenu) return; + + playerList.UnRegisterListeners(); TriggerMainMenuEventLater(); }; StartCoroutine(PollEvents()); } - private static void RegisterPackets() - { - IReadOnlyDictionary packetMappings = NetPacketProcessor.RegisterPacketTypes(); - Multiplayer.LogDebug(() => - { - StringBuilder stringBuilder = new($"Registered {packetMappings.Count} packets. Mappings:\n"); - foreach (KeyValuePair kvp in packetMappings) - stringBuilder.AppendLine($"{kvp.Value}: {kvp.Key}"); - return stringBuilder; - }); - } - private void OnSettingsUpdated(Settings settings) { if (!IsClientRunning && !IsServerRunning) @@ -111,25 +114,42 @@ public void QueueMainMenuEvent(Action action) mainMenuLoadedQueue.Enqueue(action); } - public bool StartServer(int port, IDifficulty difficulty) + public bool StartServer(IDifficulty difficulty) { + int port = Multiplayer.Settings.Port; + if (Server != null) throw new InvalidOperationException("NetworkManager already exists!"); + + if (!IsSinglePlayer) + { + if (serverData != null) + { + port = serverData.port; + } + } + Multiplayer.Log($"Starting server on port {port}"); - NetworkServer server = new(difficulty, Multiplayer.Settings); + NetworkServer server = new(difficulty, Multiplayer.Settings, IsSinglePlayer, serverData); + + //reset for next game + IsSinglePlayer = true; + serverData = null; + if (!server.Start(port)) return false; + Server = server; - StartClient("localhost", port, Multiplayer.Settings.Password); + StartClient(IPAddress.Loopback.ToString(), port, Multiplayer.Settings.Password, IsSinglePlayer, null/* (DisconnectReason dr,string msg) =>{ }*/); return true; } - public void StartClient(string address, int port, string password) + public void StartClient(string address, int port, string password, bool isSinglePlayer, Action onDisconnect ) { if (Client != null) throw new InvalidOperationException("NetworkManager already exists!"); NetworkClient client = new(Multiplayer.Settings); - client.Start(address, port, password); + client.Start(address, port, password, isSinglePlayer, onDisconnect); Client = client; OnSettingsUpdated(Multiplayer.Settings); // Show stats if enabled } @@ -156,8 +176,11 @@ private IEnumerator PollEvents() tickWatchdog.Stop(time => Multiplayer.LogWarning($"OnTick took {time} ms!")); } - TickManager(Client); - TickManager(Server); + if(Client != null) + TickManager(Client); + + if(Server != null) + TickManager(Server); float elapsedTime = tickTimer.Stop(); float remainingTime = Mathf.Max(0f, TICK_INTERVAL - elapsedTime); @@ -186,7 +209,7 @@ private void TickManager(NetworkManager manager) public void Stop() { - if (Stats != null) Stats.Hide(); + Stats?.Hide(); Server?.Stop(); Client?.Stop(); Server = null; @@ -206,4 +229,5 @@ public static void CreateLifecycle() gameObject.AddComponent(); DontDestroyOnLoad(gameObject); } + } diff --git a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs index fec0ea6..561267a 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedPlayer.cs @@ -10,7 +10,7 @@ public class NetworkedPlayer : MonoBehaviour private const float LERP_SPEED = 5.0f; public byte Id; - public Guid Guid; + //public Guid Guid; private AnimationHandler animationHandler; private NameTag nameTag; @@ -106,6 +106,14 @@ public void UpdatePosition(Vector3 position, Vector2 moveDir, float rotation, bo public void UpdateCar(ushort netId) { isOnCar = NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar); + + if(isOnCar && trainCar == null) + { + //we have a desync! + Multiplayer.LogWarning($"Desync detected! Trying to update player '{username}' position to TrainCar netId {netId}, but car is null!"); + return; + } + selfTransform.SetParent(isOnCar ? trainCar.transform : null, true); targetPos = isOnCar ? transform.localPosition : selfTransform.position; targetRotation = isOnCar ? transform.localRotation : selfTransform.rotation; diff --git a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs index ccc044b..fe99948 100644 --- a/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs +++ b/Multiplayer/Components/Networking/Player/NetworkedWorldMap.cs @@ -1,25 +1,24 @@ +using DV; using System.Collections.Generic; using TMPro; using UnityEngine; namespace Multiplayer.Components.Networking.Player; -public class NetworkedWorldMap : MonoBehaviour +public class NetworkedMapMarkersController : MonoBehaviour { - private WorldMap worldMap; private MapMarkersController markersController; private GameObject textPrefab; - private readonly Dictionary playerIndicators = new(); + private readonly Dictionary playerIndicators = []; private void Awake() { - worldMap = GetComponent(); markersController = GetComponent(); - textPrefab = worldMap.GetComponentInChildren().gameObject; - foreach (NetworkedPlayer networkedPlayer in NetworkLifecycle.Instance.Client.PlayerManager.Players) + textPrefab = markersController.GetComponentInChildren().gameObject; + foreach (NetworkedPlayer networkedPlayer in NetworkLifecycle.Instance.Client.ClientPlayerManager.Players) OnPlayerConnected(networkedPlayer.Id, networkedPlayer); - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerConnected += OnPlayerConnected; - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerDisconnected += OnPlayerDisconnected; + NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerConnected += OnPlayerConnected; + NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerDisconnected += OnPlayerDisconnected; NetworkLifecycle.Instance.OnTick += OnTick; } @@ -30,22 +29,22 @@ private void OnDestroy() NetworkLifecycle.Instance.OnTick -= OnTick; if (UnloadWatcher.isUnloading) return; - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerConnected -= OnPlayerConnected; - NetworkLifecycle.Instance.Client.PlayerManager.OnPlayerDisconnected -= OnPlayerDisconnected; + NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerConnected -= OnPlayerConnected; + NetworkLifecycle.Instance.Client.ClientPlayerManager.OnPlayerDisconnected -= OnPlayerDisconnected; } private void OnPlayerConnected(byte id, NetworkedPlayer player) { - Transform root = new GameObject($"{player.Username}'s Indicator") { + Transform root = new GameObject($"MapMarkerPlayer({player.Username})") { transform = { - parent = worldMap.playerIndicator.parent, + parent = this.transform, localPosition = Vector3.zero, localEulerAngles = Vector3.zero } }.transform; WorldMapIndicatorRefs refs = root.gameObject.AddComponent(); - GameObject indicator = Instantiate(worldMap.playerIndicator.gameObject, root); + GameObject indicator = Instantiate(markersController.playerMarkerPrefab.gameObject, root); indicator.transform.localPosition = Vector3.zero; refs.indicator = indicator.transform; @@ -54,6 +53,8 @@ private void OnPlayerConnected(byte id, NetworkedPlayer player) textGo.transform.localEulerAngles = new Vector3(90f, 0, 0); refs.text = textGo.GetComponent(); TMP_Text text = textGo.GetComponent(); + + text.name = "Player Name"; text.text = player.Username; text.alignment = TextAlignmentOptions.Center; text.fontSize /= 1.25f; @@ -74,29 +75,47 @@ private void OnPlayerDisconnected(byte id, NetworkedPlayer player) private void OnTick(uint obj) { - if (!worldMap.initialized) + if (markersController == null || UnloadWatcher.isUnloading) return; UpdatePlayers(); } public void UpdatePlayers() { + if (playerIndicators == null) + { + Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() playerIndicators: {playerIndicators != null}, count: {playerIndicators?.Count}"); + return; + } + foreach (KeyValuePair kvp in playerIndicators) { - if (!NetworkLifecycle.Instance.Client.PlayerManager.TryGetPlayer(kvp.Key, out NetworkedPlayer networkedPlayer)) + if(kvp.Value == null) + Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, value is null: {kvp.Value == null}"); + + if (!NetworkLifecycle.Instance.Client.ClientPlayerManager.TryGetPlayer(kvp.Key, out NetworkedPlayer networkedPlayer)) { Multiplayer.LogWarning($"Player indicator for {kvp.Key} exists but {nameof(NetworkedPlayer)} does not!"); OnPlayerDisconnected(kvp.Key, null); continue; } + if(kvp.Value == null) + { + Multiplayer.LogWarning($"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, value is null skipping"); + continue; + } + WorldMapIndicatorRefs refs = kvp.Value; - bool active = worldMap.gameParams.PlayerMarkerDisplayed; + bool active = Globals.G.gameParams.PlayerMarkerDisplayed; if (refs.gameObject.activeSelf != active) refs.gameObject.SetActive(active); if (!active) + { + Multiplayer.LogDebug(() => $"NetworkedWorldMap.UpdatePlayers() key: {kvp.Key}, is NOT active"); return; + } Transform playerTransform = networkedPlayer.transform; @@ -104,7 +123,7 @@ public void UpdatePlayers() if (normalized != Vector3.zero) refs.indicator.localRotation = Quaternion.LookRotation(normalized); - Vector3 position = markersController.GetMapPosition(playerTransform.position - WorldMover.currentMove, worldMap.triggerExtentsXZ); + Vector3 position = markersController.GetMapPosition(playerTransform.position - WorldMover.currentMove, true); refs.indicator.localPosition = position; refs.text.localPosition = position with { y = position.y + 0.025f }; } diff --git a/Multiplayer/Components/Networking/TickedQueue.cs b/Multiplayer/Components/Networking/TickedQueue.cs index 31447e1..30aad3a 100644 --- a/Multiplayer/Components/Networking/TickedQueue.cs +++ b/Multiplayer/Components/Networking/TickedQueue.cs @@ -32,7 +32,7 @@ public void ReceiveSnapshot(T snapshot, uint tick) private void OnTick(uint tick) { - if (snapshots.Count == 0) + if (snapshots.Count == 0 || UnloadWatcher.isUnloading) return; while (snapshots.Count > 0) { @@ -41,5 +41,10 @@ private void OnTick(uint tick) } } + public void Clear() + { + snapshots.Clear(); + } + protected abstract void Process(T snapshot, uint snapshotTick); } diff --git a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs index 249b47f..d5d6752 100644 --- a/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs +++ b/Multiplayer/Components/Networking/Train/NetworkTrainsetWatcher.cs @@ -1,9 +1,10 @@ using System.Linq; using DV.Utils; +using UnityEngine; using JetBrains.Annotations; -using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Utils; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Components.Networking.Train; @@ -11,6 +12,9 @@ public class NetworkTrainsetWatcher : SingletonBehaviour { private ClientboundTrainsetPhysicsPacket cachedSendPacket; + const float DESIRED_FULL_SYNC_INTERVAL = 2f; // in seconds + const int MAX_UNSYNC_TICKS = (int)(NetworkLifecycle.TICK_RATE * DESIRED_FULL_SYNC_INTERVAL); + protected override void Awake() { base.Awake(); @@ -33,64 +37,102 @@ protected override void OnDestroy() private void Server_OnTick(uint tick) { + if (UnloadWatcher.isUnloading) + return; + cachedSendPacket.Tick = tick; foreach (Trainset set in Trainset.allSets) - Server_TickSet(set); + Server_TickSet(set, tick); } - - private void Server_TickSet(Trainset set) + private void Server_TickSet(Trainset set, uint tick) { - bool dirty = false; - foreach (TrainCar trainCar in set.cars) + bool anyCarMoving = false; + bool maxTicksReached = false; + bool anyTracksDirty = false; + + if (set == null) { - if (trainCar.isStationary) - continue; - dirty = true; - break; + Multiplayer.LogError($"Server_TickSet(): Received null set!"); + return; } - if (!dirty) + cachedSendPacket.FirstNetId = set.firstCar.GetNetId(); + cachedSendPacket.LastNetId = set.lastCar.GetNetId(); + //car may not be initialised, missing a valid NetID + if (cachedSendPacket.FirstNetId == 0 || cachedSendPacket.LastNetId == 0) return; - cachedSendPacket.NetId = set.firstCar.GetNetId(); - - if (set.cars.Contains(null)) + foreach (TrainCar trainCar in set.cars) { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a null car!"); - return; + if (trainCar == null || !trainCar.gameObject.activeSelf) + { + Multiplayer.LogError($"Trainset {set.id} ({set.firstCar?.GetNetId()} has a null or inactive ({trainCar?.gameObject.activeSelf}) car!"); + return; + } + + //If we can locate the networked car, we'll add to the ticks counter and check if any tracks are dirty + if (NetworkedTrainCar.TryGetFromTrainCar(trainCar, out NetworkedTrainCar netTC)) + { + maxTicksReached |= netTC.TicksSinceSync >= MAX_UNSYNC_TICKS; + anyTracksDirty |= netTC.BogieTracksDirty; + } + + //Even if the car is stationary, if the max ticks has been exceeded we will still sync + if (!trainCar.isStationary) + anyCarMoving = true; + + //we can finish checking early if we have BOTH a dirty and a max ticks + if (anyCarMoving && maxTicksReached) + break; } - if (set.cars.Any(car => !car.gameObject.activeSelf)) - { - Multiplayer.LogError($"Trainset {set.id} ({set.firstCar.GetNetId()} has a non-active car!"); + //if any car is dirty or exceeded its max ticks we will re-sync the entire train + if (!anyCarMoving && !maxTicksReached) return; - } TrainsetMovementPart[] trainsetParts = new TrainsetMovementPart[set.cars.Count]; - bool anyTracksDirty = false; + for (int i = 0; i < set.cars.Count; i++) { TrainCar trainCar = set.cars[i]; - if (!trainCar.TryNetworked(out NetworkedTrainCar _)) + if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) { Multiplayer.LogDebug(() => $"TrainCar {trainCar.ID} is not networked! Is active? {trainCar.gameObject.activeInHierarchy}"); continue; } - NetworkedTrainCar networkedTrainCar = trainCar.Networked(); - anyTracksDirty |= networkedTrainCar.BogieTracksDirty; - if (trainCar.derailed) - trainsetParts[i] = new TrainsetMovementPart(RigidbodySnapshot.From(trainCar.rb)); + { + trainsetParts[i] = new TrainsetMovementPart(networkedTrainCar.NetId, RigidbodySnapshot.From(trainCar.rb)); + } else + { + Vector3? position = null; + Quaternion? rotation = null; + + //Have we exceeded the max ticks? + if (maxTicksReached) + { + //Multiplayer.Log($"Max Ticks Reached for TrainSet with cars {set.firstCar.ID}, {set.lastCar.ID}"); + + position = trainCar.transform.position - WorldMover.currentMove; + rotation = trainCar.transform.rotation; + networkedTrainCar.TicksSinceSync = 0; //reset this car's tick count + } + trainsetParts[i] = new TrainsetMovementPart( + networkedTrainCar.NetId, trainCar.GetForwardSpeed(), trainCar.stress.slowBuildUpStress, - BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie1TrackDirection), - BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty, networkedTrainCar.Bogie2TrackDirection) + BogieData.FromBogie(trainCar.Bogies[0], networkedTrainCar.BogieTracksDirty), + BogieData.FromBogie(trainCar.Bogies[1], networkedTrainCar.BogieTracksDirty), + position, //only used in full sync + rotation //only used in full sync ); + } } + //Multiplayer.Log($"Server_TickSet({set.firstCar.ID}): SendTrainsetPhysicsUpdate, tick: {cachedSendPacket.Tick}"); cachedSendPacket.TrainsetParts = trainsetParts; NetworkLifecycle.Instance.Server.SendTrainsetPhysicsUpdate(cachedSendPacket, anyTracksDirty); } @@ -101,24 +143,42 @@ private void Server_TickSet(Trainset set) public void Client_HandleTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket packet) { - Trainset set = Trainset.allSets.Find(set => set.firstCar.GetNetId() == packet.NetId || set.lastCar.GetNetId() == packet.NetId); + Trainset set = Trainset.allSets.Find(set => set.firstCar.GetNetId() == packet.FirstNetId || set.lastCar.GetNetId() == packet.FirstNetId || + set.firstCar.GetNetId() == packet.LastNetId || set.lastCar.GetNetId() == packet.LastNetId); + if (set == null) { - Multiplayer.LogDebug(() => $"Received {nameof(ClientboundTrainsetPhysicsPacket)} for unknown trainset with netId {packet.NetId}"); + Multiplayer.LogWarning($"Received {nameof(ClientboundTrainsetPhysicsPacket)} for unknown trainset with FirstNetId: {packet.FirstNetId} and LastNetId: {packet.LastNetId}"); return; } if (set.cars.Count != packet.TrainsetParts.Length) { - Multiplayer.LogDebug(() => - $"Received {nameof(ClientboundTrainsetPhysicsPacket)} for trainset with netId {packet.NetId} with {packet.TrainsetParts.Length} parts, but trainset has {set.cars.Count} parts"); + //log the discrepancies + Multiplayer.LogWarning( + $"Received {nameof(ClientboundTrainsetPhysicsPacket)} for trainset with FirstNetId: {packet.FirstNetId} and LastNetId: {packet.LastNetId} with {packet.TrainsetParts.Length} parts, but trainset has {set.cars.Count} parts"); + + for (int i = 0; i < packet.TrainsetParts.Length; i++) + { + if (NetworkedTrainCar.Get(packet.TrainsetParts[i].NetId ,out NetworkedTrainCar networkedTrainCar)) + networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); + } return; } + //Check direction of trainset vs packet + if(set.firstCar.GetNetId() == packet.LastNetId) + packet.TrainsetParts = packet.TrainsetParts.Reverse().ToArray(); + + //Multiplayer.Log($"Client_HandleTrainsetPhysicsUpdate({set.firstCar.ID}):, tick: {packet.Tick}"); + for (int i = 0; i < packet.TrainsetParts.Length; i++) - set.cars[i].Networked().Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); + { + if(set.cars[i].TryNetworked(out NetworkedTrainCar networkedTrainCar)) + networkedTrainCar.Client_ReceiveTrainPhysicsUpdate(in packet.TrainsetParts[i], packet.Tick); + } } - + #endregion [UsedImplicitly] diff --git a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs index 6da72fd..c316948 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedBogie.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedBogie.cs @@ -1,23 +1,40 @@ using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; +using System.Collections; using UnityEngine; namespace Multiplayer.Components.Networking.Train; public class NetworkedBogie : TickedQueue { + private const int MAX_FRAMES = 60; private Bogie bogie; protected override void OnEnable() { - bogie = GetComponent(); - if (bogie == null) + StartCoroutine(WaitForBogie()); + } + + protected IEnumerator WaitForBogie() + { + int counter = 0; + + while (bogie == null && counter < MAX_FRAMES) { - Multiplayer.LogError($"{gameObject.name}: {nameof(NetworkedBogie)} requires a {nameof(Bogie)} component on the same GameObject!"); - return; + bogie = GetComponent(); + if (bogie == null) + { + counter++; + yield return new WaitForEndOfFrame(); + } } base.OnEnable(); + + if (bogie == null) + { + Multiplayer.LogError($"{gameObject.name} ({bogie?.Car?.ID}): {nameof(NetworkedBogie)} requires a {nameof(Bogie)} component on the same GameObject! Waited {counter} iterations"); + } } protected override void Process(BogieData snapshot, uint snapshotTick) @@ -25,7 +42,7 @@ protected override void Process(BogieData snapshot, uint snapshotTick) if (bogie.HasDerailed) return; - if (snapshot.HasDerailed || !bogie.track) + if (snapshot.HasDerailed) { bogie.Derail(); return; @@ -33,12 +50,21 @@ protected override void Process(BogieData snapshot, uint snapshotTick) if (snapshot.IncludesTrackData) { - if (NetworkedRailTrack.Get(snapshot.TrackNetId, out NetworkedRailTrack track)) - bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); + if (!NetworkedRailTrack.Get(snapshot.TrackNetId, out NetworkedRailTrack track)) + { + Multiplayer.LogWarning($"NetworkedBogie.Process() Failed to find track {snapshot.TrackNetId} for bogie: {bogie.Car.ID}"); + return; + } + + bogie.SetTrack(track.RailTrack, snapshot.PositionAlongTrack, snapshot.TrackDirection); + } else { - bogie.traveller.MoveToSpan(snapshot.PositionAlongTrack); + if(bogie.track) + bogie.traveller.MoveToSpan(snapshot.PositionAlongTrack); + else + Multiplayer.LogWarning($"NetworkedBogie.Process() No track for current bogie for bogie: {bogie?.Car?.ID}, unable to move position!"); } int physicsSteps = Mathf.FloorToInt((NetworkLifecycle.Instance.Tick - (float)snapshotTick) / NetworkLifecycle.TICK_RATE / Time.fixedDeltaTime) + 1; diff --git a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs index 4268ceb..4625a3d 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedCarSpawner.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System.Collections; +using DV.LocoRestoration; +using DV.Simulation.Brake; using DV.ThingTypes; using Multiplayer.Components.Networking.World; -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; using Multiplayer.Utils; using UnityEngine; @@ -9,16 +11,28 @@ namespace Multiplayer.Components.Networking.Train; public static class NetworkedCarSpawner { - public static void SpawnCars(TrainsetSpawnPart[] parts) + public static void SpawnCars(TrainsetSpawnPart[] parts, bool autoCouple) { - TrainCar[] cars = new TrainCar[parts.Length]; + NetworkedTrainCar[] cars = new NetworkedTrainCar[parts.Length]; + + //spawn the cars for (int i = 0; i < parts.Length; i++) cars[i] = SpawnCar(parts[i], true); + + //Set brake params + for (int i = 0; i < cars.Length; i++) + SetBrakeParams(parts[i].BrakeData, cars[i].TrainCar); + + //couple them if marked as coupled for (int i = 0; i < cars.Length; i++) - AutoCouple(parts[i], cars[i]); + Couple(parts[i], cars[i].TrainCar, autoCouple); + + //update speed queue data + for (int i = 0; i < cars.Length; i++) + cars[i].Client_trainSpeedQueue.ReceiveSnapshot(parts[i].Speed, NetworkLifecycle.Instance.Tick); } - public static TrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCoupling = false) + public static NetworkedTrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCoupling = false) { if (!NetworkedRailTrack.Get(spawnPart.Bogie1.TrackNetId, out NetworkedRailTrack bogie1Track) && spawnPart.Bogie1.TrackNetId != 0) { @@ -38,24 +52,44 @@ public static TrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCouplin return null; } - (TrainCar trainCar, bool isPooled) = GetFromPool(livery); + //TrainCar trainCar = CarSpawner.Instance.BaseSpawn(livery.prefab, spawnPart.PlayerSpawnedCar, false); //todo: do we need to set the unique flag ever on a client? + TrainCar trainCar = (CarSpawner.Instance.useCarPooling ? CarSpawner.Instance.GetFromPool(livery.prefab) : UnityEngine.Object.Instantiate(livery.prefab)).GetComponentInChildren(); + //Multiplayer.LogDebug(() => $"SpawnCar({spawnPart.CarId}) activePrefab: {livery.prefab.activeSelf} activeInstance: {trainCar.gameObject.activeSelf}"); + trainCar.playerSpawnedCar = spawnPart.PlayerSpawnedCar; + trainCar.uniqueCar = false; + trainCar.InitializeExistingLogicCar(spawnPart.CarId, spawnPart.CarGuid); - NetworkedTrainCar networkedTrainCar = trainCar.gameObject.GetOrAddComponent(); - networkedTrainCar.NetId = spawnPart.NetId; - trainCar.gameObject.GetOrAddComponent(); + //Restoration vehicle hack + //todo: make it work properly + if (spawnPart.IsRestorationLoco) + switch(spawnPart.RestorationState) + { + case LocoRestorationController.RestorationState.S0_Initialized: + case LocoRestorationController.RestorationState.S1_UnlockedRestorationLicense: + case LocoRestorationController.RestorationState.S2_LocoUnblocked: + BlockLoco(trainCar); - trainCar.gameObject.SetActive(true); + break; + } - if (isPooled) - trainCar.AwakeForPooledCar(); + if (trainCar.PaintExterior != null && spawnPart.PaintExterior != null) + trainCar.PaintExterior.currentTheme = spawnPart.PaintExterior; - trainCar.InitializeExistingLogicCar(spawnPart.CarId, spawnPart.CarGuid); + if (trainCar.PaintInterior != null && spawnPart.PaintInterior != null) + trainCar.PaintInterior.currentTheme = spawnPart.PaintInterior; + //Add networked components + NetworkedTrainCar networkedTrainCar = trainCar.gameObject.GetOrAddComponent(); + networkedTrainCar.NetId = spawnPart.NetId; + + //Setup positions and bogies Transform trainTransform = trainCar.transform; trainTransform.position = spawnPart.Position + WorldMover.currentMove; - trainTransform.eulerAngles = spawnPart.Rotation; - trainCar.playerSpawnedCar = spawnPart.PlayerSpawnedCar; - trainCar.preventAutoCouple = true; + trainTransform.rotation = spawnPart.Rotation; + + //Multiplayer.LogDebug(() => $"SpawnCar({spawnPart.CarId}) Bogie1 derailed: {spawnPart.Bogie1.HasDerailed}, Rail Track: {bogie1Track?.RailTrack?.name}, Position along track: {spawnPart.Bogie1.PositionAlongTrack}, Track direction: {spawnPart.Bogie1.TrackDirection}, " + + // $"Bogie2 derailed: {spawnPart.Bogie2.HasDerailed}, Rail Track: {bogie2Track?.RailTrack?.name}, Position along track: {spawnPart.Bogie2.PositionAlongTrack}, Track direction: {spawnPart.Bogie2.TrackDirection}" + //); if (!spawnPart.Bogie1.HasDerailed) trainCar.Bogies[0].SetTrack(bogie1Track.RailTrack, spawnPart.Bogie1.PositionAlongTrack, spawnPart.Bogie1.TrackDirection); @@ -67,62 +101,133 @@ public static TrainCar SpawnCar(TrainsetSpawnPart spawnPart, bool preventCouplin else trainCar.Bogies[1].SetDerailedOnLoadFlag(true); + trainCar.TryAddFastTravelDestination(); + CarSpawner.Instance.FireCarSpawned(trainCar); - networkedTrainCar.Client_trainSpeedQueue.ReceiveSnapshot(spawnPart.Speed, NetworkLifecycle.Instance.Tick); + return networkedTrainCar; + } + + private static void Couple(in TrainsetSpawnPart spawnPart, TrainCar trainCar, bool autoCouple) + { + TrainsetSpawnPart sp = spawnPart; + Multiplayer.LogDebug(() =>$"Couple([{sp.CarId}, {sp.NetId}], trainCar, {autoCouple})"); + + if (autoCouple) + { + trainCar.frontCoupler.preventAutoCouple = spawnPart.FrontCoupling.PreventAutoCouple; + trainCar.rearCoupler.preventAutoCouple = spawnPart.RearCoupling.PreventAutoCouple; + + trainCar.frontCoupler.AttemptAutoCouple(); + trainCar.rearCoupler.AttemptAutoCouple(); + + return; + } - if (!preventCoupling) - AutoCouple(spawnPart, trainCar); + //Handle coupling at front of car + HandleCoupling(spawnPart.FrontCoupling, trainCar.frontCoupler); - return trainCar; + //Handle coupling at rear of car + HandleCoupling(spawnPart.RearCoupling, trainCar.rearCoupler); } - private static void AutoCouple(TrainsetSpawnPart spawnPart, TrainCar trainCar) + private static void HandleCoupling(CouplingData couplingData, Coupler currentCoupler) { - if (spawnPart.IsFrontCoupled) trainCar.frontCoupler.TryCouple(false, true); - if (spawnPart.IsRearCoupled) trainCar.rearCoupler.TryCouple(false, true); + + CouplingData cd = couplingData; + TrainCar tc = currentCoupler.train; + var net = tc.GetNetId(); + + Multiplayer.LogDebug(() => $"HandleCoupling([{tc?.ID}, {net}]) couplingData: is front: {currentCoupler.isFrontCoupler}, {couplingData.HoseConnected}, {couplingData.CockOpen}"); + + if (couplingData.IsCoupled) + { + if (!NetworkedTrainCar.GetTrainCar(couplingData.ConnectionNetId, out TrainCar otherCar)) + { + Multiplayer.LogWarning($"HandleCoupling([{currentCoupler?.train?.ID}, {currentCoupler?.train?.GetNetId()}]) did not find car at {(currentCoupler.isFrontCoupler ? "Front" : "Rear")} car with netId: {couplingData.ConnectionNetId}"); + } + else + { + var otherCoupler = couplingData.ConnectionToFront ? otherCar.frontCoupler : otherCar.rearCoupler; + SetCouplingState(currentCoupler, otherCoupler, couplingData.State); + } + } + + CarsSaveManager.RestoreHoseAndCock(currentCoupler, couplingData.HoseConnected, couplingData.CockOpen); } - private static (TrainCar, bool) GetFromPool(TrainCarLivery livery) + public static void SetCouplingState(Coupler coupler, Coupler otherCoupler, ChainCouplerInteraction.State targetState) { - if (!CarSpawner.Instance.useCarPooling || !CarSpawner.Instance.carLiveryToTrainCarPool.TryGetValue(livery, out List trainCarList)) - return Instantiate(livery); + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Coupled: {coupler.IsCoupled()}"); - int count = trainCarList.Count; - if (count <= 0) - return Instantiate(livery); + if (coupler.IsCoupled() && targetState == ChainCouplerInteraction.State.Attached_Tight) + { + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Coupled, attaching tight"); + coupler.state = ChainCouplerInteraction.State.Parked; + return; + } - int index = count - 1; - TrainCar trainCar = trainCarList[index]; - trainCarList.RemoveAt(index); - CarSpawner.Instance.trainCarPoolHashSet.Remove(trainCar); + coupler.state = targetState; + if (coupler.state == ChainCouplerInteraction.State.Attached_Tight) + { + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Not coupled, attaching tight"); + coupler.CoupleTo(otherCoupler, false); + coupler.SetChainTight(true); + } + else if (coupler.state == ChainCouplerInteraction.State.Attached_Loose) + { + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Unknown coupled, attaching loose"); + coupler.CoupleTo(otherCoupler, false); + coupler.SetChainTight(false); + } - if (trainCar != null) + if (!coupler.IsCoupled()) { - Transform trainCarTransform = trainCar.transform; - trainCarTransform.SetParent(null); - trainCarTransform.localScale = Vector3.one; - trainCar.gameObject.SetActive(false); // Enabled after NetworkedTrainCar has been added - - Transform interiorTransform = trainCar.interior.transform; - interiorTransform.SetParent(null); - interiorTransform.localScale = Vector3.one; - - trainCar.interior.gameObject.SetActive(true); - trainCar.rb.isKinematic = false; - return (trainCar, true); + //Multiplayer.LogDebug(() => $"SetCouplingState({coupler.train.ID}, {otherCoupler.train.ID}, {targetState}) Failed to couple, activating buffer collider"); + coupler.fakeBuffersCollider.enabled = true; } - Multiplayer.LogError($"Failed to get {livery.id} from pool!"); - return Instantiate(livery); } - private static (TrainCar, bool) Instantiate(TrainCarLivery livery) + private static void SetBrakeParams(BrakeSystemData brakeSystemData, TrainCar trainCar) { - bool wasActive = livery.prefab.activeSelf; - livery.prefab.SetActive(false); - (TrainCar, bool) result = (Object.Instantiate(livery.prefab).GetComponent(), false); - livery.prefab.SetActive(wasActive); - return result; + BrakeSystem bs = trainCar.brakeSystem; + + if (bs == null) + { + Multiplayer.LogWarning($"NetworkedCarSpawner.SetBrakeParams() Brake system is null! netId: {trainCar?.GetNetId()}, trainCar: {trainCar?.ID}"); + return; + } + + if(bs.hasHandbrake) + bs.SetHandbrakePosition(brakeSystemData.HandBrakePosition); + if(bs.hasTrainBrake) + bs.trainBrakePosition = brakeSystemData.TrainBrakePosition; + + bs.SetBrakePipePressure(brakeSystemData.BrakePipePressure); + bs.SetAuxReservoirPressure(brakeSystemData.AuxResPressure); + bs.SetMainReservoirPressure(brakeSystemData.MainResPressure); + bs.SetControlReservoirPressure(brakeSystemData.ControlResPressure); + bs.ForceCylinderPressure(brakeSystemData.BrakeCylPressure); + + } + + private static void BlockLoco(TrainCar trainCar) + { + trainCar.blockInteriorLoading = true; + trainCar.preventFastTravelWithCar = true; + trainCar.preventFastTravelDestination = true; + + if (trainCar.FastTravelDestination != null) + { + trainCar.FastTravelDestination.showOnMap = false; + trainCar.FastTravelDestination.RefreshMarkerVisibility(); + } + + trainCar.preventDebtDisplay = true; + trainCar.preventRerail = true; + trainCar.preventDelete = true; + trainCar.preventService = true; + trainCar.preventCouple = true; } } diff --git a/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs new file mode 100644 index 0000000..bd772c4 --- /dev/null +++ b/Multiplayer/Components/Networking/Train/NetworkedRigidbody.cs @@ -0,0 +1,61 @@ +using Multiplayer.Networking.Data.Train; +using System; +using System.Collections; +using UnityEngine; +using static Multiplayer.Networking.Data.Train.RigidbodySnapshot; + +namespace Multiplayer.Components.Networking.Train; + +public class NetworkedRigidbody : TickedQueue +{ + private const int MAX_FRAMES = 60; + private Rigidbody rigidbody; + + protected override void OnEnable() + { + StartCoroutine(WaitForRB()); + } + + protected IEnumerator WaitForRB() + { + int counter = 0; + + while (rigidbody == null && counter < MAX_FRAMES) + { + rigidbody = GetComponent(); + if (rigidbody == null) + { + counter++; + yield return new WaitForEndOfFrame(); + } + } + + base.OnEnable(); + + if (rigidbody == null) + { + gameObject.TryGetComponent(out TrainCar car); + + Multiplayer.LogError($"{gameObject.name} ({car?.ID}): {nameof(NetworkedBogie)} requires a {nameof(Bogie)} component on the same GameObject! Waited {counter} iterations"); + } + } + + protected override void Process(RigidbodySnapshot snapshot, uint snapshotTick) + { + if (snapshot == null) + { + Multiplayer.LogError($"NetworkedRigidBody.Process() Snapshot NULL!"); + return; + } + + try + { + //Multiplayer.LogDebug(() => $"NetworkedRigidBody.Process() {(IncludedData)snapshot.IncludedDataFlags}, {snapshot.Position.ToString() ?? "null"}, {snapshot.Rotation.ToString() ?? "null"}, {snapshot.Velocity.ToString() ?? "null"}, {snapshot.AngularVelocity.ToString() ?? "null"}, tick: {snapshotTick}"); + snapshot.Apply(rigidbody); + } + catch (Exception ex) + { + Multiplayer.LogError($"NetworkedRigidBody.Process() {ex.Message}\r\n {ex.StackTrace}"); + } + } +} diff --git a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs index 436649c..5964288 100644 --- a/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs +++ b/Multiplayer/Components/Networking/Train/NetworkedTrainCar.cs @@ -1,15 +1,21 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; +using DV.Customization.Paint; +using DV.MultipleUnit; using DV.Simulation.Brake; using DV.Simulation.Cars; using DV.ThingTypes; +using JetBrains.Annotations; using LocoSim.Definitions; using LocoSim.Implementations; using Multiplayer.Components.Networking.Player; -using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; +using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Common.Train; +using Multiplayer.Networking.TransportLayers; using Multiplayer.Utils; using UnityEngine; @@ -19,8 +25,10 @@ public class NetworkedTrainCar : IdMonoBehaviour { #region Lookup Cache - private static readonly Dictionary trainCarsToNetworkedTrainCars = new(); - private static readonly Dictionary hoseToCoupler = new(); + private static readonly Dictionary trainCarsToNetworkedTrainCars = []; + private static readonly Dictionary trainCarIdToNetworkedTrainCars = []; + private static readonly Dictionary trainCarIdToTrainCars = []; + private static readonly Dictionary hoseToCoupler = []; public static bool Get(ushort netId, out NetworkedTrainCar obj) { @@ -36,14 +44,18 @@ public static bool GetTrainCar(ushort netId, out TrainCar obj) return b; } - public static Coupler GetCoupler(HoseAndCock hoseAndCock) + public static bool TryGetCoupler(HoseAndCock hoseAndCock, out Coupler coupler) { - return hoseToCoupler[hoseAndCock]; + return hoseToCoupler.TryGetValue(hoseAndCock, out coupler); } - public static NetworkedTrainCar GetFromTrainCar(TrainCar trainCar) + public static bool GetFromTrainId(string carId, out NetworkedTrainCar networkedTrainCar) { - return trainCarsToNetworkedTrainCars[trainCar]; + return trainCarIdToNetworkedTrainCars.TryGetValue(carId, out networkedTrainCar); + } + public static bool GetTrainCarFromTrainId(string carId, out TrainCar trainCar) + { + return trainCarIdToTrainCars.TryGetValue(carId, out trainCar); } public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar networkedTrainCar) @@ -53,7 +65,14 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n #endregion + private const int MAX_COUPLER_ITERATIONS = 10; + private const float MAX_FIREBOX_DELTA = 0.1f; + private const float MAX_PORT_DELTA = 0.001f; + + + public string CurrentID { get; private set; } public TrainCar TrainCar; + public uint TicksSinceSync = uint.MaxValue; public bool HasPlayers => PlayerManager.Car == TrainCar || GetComponentInChildren() != null; private Bogie bogie1; @@ -62,30 +81,46 @@ public static bool TryGetFromTrainCar(TrainCar trainCar, out NetworkedTrainCar n private bool hasSimFlow; private SimulationFlow simulationFlow; + public FireboxSimController firebox; private HashSet dirtyPorts; private Dictionary lastSentPortValues; private HashSet dirtyFuses; + private float lastSentFireboxValue; + private bool handbrakeDirty; + private bool mainResPressureDirty; + private bool brakeOverheatDirty; + public bool BogieTracksDirty; - public int Bogie1TrackDirection; - public int Bogie2TrackDirection; private bool cargoDirty; private bool cargoIsLoading; public byte CargoModelIndex = byte.MaxValue; private bool healthDirty; private bool sendCouplers; + private bool sendCables; + private bool fireboxDirty; public bool IsDestroying; + //Coupler interaction + private bool frontInteracting = false; + private bool rearInteracting = false; + + private int frontInteractionPeer; + private int rearInteractionPeer; #region Client - private bool client_Initialized; + public bool Client_Initialized {get; private set;} public TickedQueue Client_trainSpeedQueue; public TickedQueue Client_trainRigidbodyQueue; - private TickedQueue client_bogie1Queue; - private TickedQueue client_bogie2Queue; + public TickedQueue client_bogie1Queue; + public TickedQueue client_bogie2Queue; + + private Coupler couplerInteraction; + private ChainCouplerInteraction.State originalState; + private Coupler originalCoupledTo; #endregion protected override bool IsIdServerAuthoritative => true; @@ -97,6 +132,8 @@ protected override void Awake() TrainCar = GetComponent(); trainCarsToNetworkedTrainCars[TrainCar] = this; + TrainCar.LogicCarInitialized += OnLogicCarInitialised; + bogie1 = TrainCar.Bogies[0]; bogie2 = TrainCar.Bogies[1]; @@ -112,13 +149,22 @@ protected override void Awake() } } - private void Start() + [UsedImplicitly] + public void Start() { brakeSystem = TrainCar.brakeSystem; foreach (Coupler coupler in TrainCar.couplers) + { hoseToCoupler[coupler.hoseAndCock] = coupler; + Multiplayer.LogDebug(() => $"TrainCar.Start() [{TrainCar?.ID}, {NetId}], Coupler exists: {coupler != null}, Is front: {coupler.isFrontCoupler}, ChainScript exists: {coupler.ChainScript != null}"); + + //Locos with tenders and tenders only have one chainscript each, no trainscript is used for the hitch between the loco and tender + if(coupler.ChainScript != null) + coupler.ChainScript.StateChanged += (state) => { Client_CouplerStateChange(state, coupler); }; + } + SimController simController = GetComponent(); if (simController != null) { @@ -130,43 +176,105 @@ private void Start() foreach (KeyValuePair kvp in simulationFlow.fullPortIdToPort) if (kvp.Value.valueType == PortValueType.CONTROL || NetworkLifecycle.Instance.IsHost()) kvp.Value.ValueUpdatedInternally += _ => { Common_OnPortUpdated(kvp.Value); }; - + dirtyFuses = new HashSet(simulationFlow.fullFuseIdToFuse.Count); foreach (KeyValuePair kvp in simulationFlow.fullFuseIdToFuse) kvp.Value.StateUpdated += _ => { Common_OnFuseUpdated(kvp.Value); }; + + if (simController.firebox != null) + { + firebox = simController.firebox; + firebox.fireboxCoalControlPort.ValueUpdatedInternally += Client_OnAddCoal; //Player adding coal + firebox.fireboxIgnitionPort.ValueUpdatedInternally += Client_OnIgnite; //Player igniting firebox + } } - + brakeSystem.HandbrakePositionChanged += Common_OnHandbrakePositionChanged; brakeSystem.BrakeCylinderReleased += Common_OnBrakeCylinderReleased; + + if (TrainCar.PaintExterior != null) + TrainCar.PaintExterior.OnThemeChanged += Common_OnPaintThemeChange; + if (TrainCar.PaintInterior != null) + TrainCar.PaintInterior.OnThemeChanged += Common_OnPaintThemeChange; + NetworkLifecycle.Instance.OnTick += Common_OnTick; if (NetworkLifecycle.Instance.IsHost()) { NetworkLifecycle.Instance.OnTick += Server_OnTick; + NetworkLifecycle.Instance.Server.PlayerDisconnect += Server_OnPlayerDisconnect; + bogie1.TrackChanged += Server_BogieTrackChanged; bogie2.TrackChanged += Server_BogieTrackChanged; TrainCar.CarDamage.CarEffectiveHealthStateUpdate += Server_CarHealthUpdate; + + brakeSystem.MainResPressureChanged += Server_MainResUpdate; + brakeSystem.heatController.OverheatingActiveStateChanged += Server_BrakeHeatUpdate; + + if (firebox != null) + { + firebox.fireboxContentsPort.ValueUpdatedInternally += Common_OnFireboxUpdate; + firebox.fireOnPort.ValueUpdatedInternally += Common_OnFireboxUpdate; + } + StartCoroutine(Server_WaitForLogicCar()); } - } - private void OnDisable() + NetworkLifecycle.Instance?.Client.SendTrainSyncRequest(NetId); + } + public void OnDisable() { if (UnloadWatcher.isQuitting) return; + NetworkLifecycle.Instance.OnTick -= Common_OnTick; NetworkLifecycle.Instance.OnTick -= Server_OnTick; - if (UnloadWatcher.isUnloading) - return; + //if (UnloadWatcher.isUnloading) + // return; + trainCarsToNetworkedTrainCars.Remove(TrainCar); + + trainCarIdToNetworkedTrainCars.Remove(CurrentID); + trainCarIdToTrainCars.Remove(CurrentID); + foreach (Coupler coupler in TrainCar.couplers) hoseToCoupler.Remove(coupler.hoseAndCock); - brakeSystem.HandbrakePositionChanged -= Common_OnHandbrakePositionChanged; - brakeSystem.BrakeCylinderReleased -= Common_OnBrakeCylinderReleased; + + if (firebox != null) + { + firebox.fireboxCoalControlPort.ValueUpdatedInternally -= Client_OnAddCoal; //Player adding coal + firebox.fireboxIgnitionPort.ValueUpdatedInternally -= Client_OnIgnite; //Player igniting firebox + } + + if (brakeSystem != null) + { + brakeSystem.HandbrakePositionChanged -= Common_OnHandbrakePositionChanged; + brakeSystem.BrakeCylinderReleased -= Common_OnBrakeCylinderReleased; + } + + if(TrainCar.PaintExterior != null) + TrainCar.PaintExterior.OnThemeChanged -= Common_OnPaintThemeChange; + if (TrainCar.PaintInterior != null) + TrainCar.PaintInterior.OnThemeChanged -= Common_OnPaintThemeChange; + if (NetworkLifecycle.Instance.IsHost()) { bogie1.TrackChanged -= Server_BogieTrackChanged; bogie2.TrackChanged -= Server_BogieTrackChanged; + TrainCar.CarDamage.CarEffectiveHealthStateUpdate -= Server_CarHealthUpdate; + + if(brakeSystem != null) + { + brakeSystem.MainResPressureChanged -= Server_MainResUpdate; + brakeSystem.heatController.OverheatingActiveStateChanged -= Server_BrakeHeatUpdate; + } + + if (firebox != null) + { + firebox.fireboxContentsPort.ValueUpdatedInternally -= Common_OnFireboxUpdate; + firebox.fireOnPort.ValueUpdatedInternally -= Common_OnFireboxUpdate; + } + if (TrainCar.logicCar != null) { TrainCar.logicCar.CargoLoaded -= Server_OnCargoLoaded; @@ -174,35 +282,61 @@ private void OnDisable() } } + CurrentID = string.Empty; Destroy(this); } #region Server + private void OnLogicCarInitialised() + { + //Multiplayer.LogWarning("OnLogicCarInitialised"); + if (TrainCar.logicCar != null) + { + CurrentID = TrainCar.ID; + trainCarIdToNetworkedTrainCars[CurrentID] = this; + trainCarIdToTrainCars[CurrentID] = TrainCar; + + TrainCar.LogicCarInitialized -= OnLogicCarInitialised; + } + else + { + Multiplayer.LogWarning("OnLogicCarInitialised Car Not Initialised!"); + } + + } private IEnumerator Server_WaitForLogicCar() { while (TrainCar.logicCar == null) yield return null; + TrainCar.logicCar.CargoLoaded += Server_OnCargoLoaded; TrainCar.logicCar.CargoUnloaded += Server_OnCargoUnloaded; - NetworkLifecycle.Instance.Server.SendSpawnTrainCar(this); + + Server_DirtyAllState(); } public void Server_DirtyAllState() { handbrakeDirty = true; + mainResPressureDirty = true; cargoDirty = true; cargoIsLoading = true; healthDirty = true; BogieTracksDirty = true; sendCouplers = true; + sendCables = true; + fireboxDirty = firebox != null; //only dirty if exists + if (!hasSimFlow) return; foreach (string portId in simulationFlow.fullPortIdToPort.Keys) { dirtyPorts.Add(portId); - if (simulationFlow.TryGetPort(portId, out Port port)) - lastSentPortValues[portId] = port.value; + //if (simulationFlow.TryGetPort(portId, out Port port)) + //{ + // lastSentPortValues[portId] = port.value; + //} } foreach (string fuseId in simulationFlow.fullFuseIdToFuse.Keys) @@ -214,11 +348,18 @@ public bool Server_ValidateClientSimFlowPacket(ServerPlayer player, CommonTrainP // Only allow control ports to be updated by clients if (hasSimFlow) foreach (string portId in packet.PortIds) - if (simulationFlow.TryGetPort(portId, out Port port) && port.valueType != PortValueType.CONTROL) + if (simulationFlow.TryGetPort(portId, out Port port)) { - NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a non-control port!"); - Common_DirtyPorts(packet.PortIds); - return false; + if (port.valueType != PortValueType.CONTROL) + { + NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} tried to send a non-control port! ({portId} on [{TrainCar?.ID}, {NetId}])"); + Common_DirtyPorts(packet.PortIds); + return false; + } + } + else + { + NetworkLifecycle.Instance.Server.LogWarning($"Player {player.Username} sent portId: {portId}, value type: {port.valueType}, but the port was not found"); } // Only allow the player to update ports on the car they are in/near @@ -259,21 +400,72 @@ private void Server_CarHealthUpdate(float health) healthDirty = true; } + private void Server_MainResUpdate(float normalizedPressure, float pressure) + { + mainResPressureDirty = true; + } + + private void Server_BrakeHeatUpdate(bool overheatActive) + { + brakeOverheatDirty = true; + } + + private void Server_FireboxUpdate(float normalizedPressure, float pressure) + { + fireboxDirty = true; + } + private void Server_OnTick(uint tick) { if (UnloadWatcher.isUnloading) return; - Server_SendCouplers(); + + Server_SendBrakeStates(); + Server_SendFireBoxState(); + //Server_SendCouplers(); + Server_SendCables(); Server_SendCargoState(); Server_SendHealthState(); + + TicksSinceSync++; //keep track of last full sync + } + + private void Server_SendBrakeStates() + { + if (!mainResPressureDirty && !brakeOverheatDirty) + return; + + mainResPressureDirty = false; + var hc = brakeSystem.heatController; + NetworkLifecycle.Instance.Server.SendBrakeState( + NetId, + brakeSystem.mainReservoirPressure, brakeSystem.brakePipePressure, brakeSystem.brakeCylinderPressure, + hc.overheatPercentage, hc.overheatReductionFactor, hc.temperature + ); + } + + private void Server_SendFireBoxState() + { + if (!fireboxDirty || firebox == null) + return; + + fireboxDirty = false; + NetworkLifecycle.Instance.Server.SendFireboxState(NetId, firebox.fireboxContentsPort.value, firebox.IsFireOn); } private void Server_SendCouplers() { if (!sendCouplers) return; + sendCouplers = false; + if(TrainCar.frontCoupler.IsCoupled()) + NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.frontCoupler,TrainCar.frontCoupler.coupledTo,false, false); + + if(TrainCar.rearCoupler.IsCoupled()) + NetworkLifecycle.Instance.Client.SendTrainCouple(TrainCar.rearCoupler,TrainCar.rearCoupler.coupledTo,false, false); + if (TrainCar.frontCoupler.hoseAndCock.IsHoseConnected) NetworkLifecycle.Instance.Client.SendHoseConnected(TrainCar.frontCoupler, TrainCar.frontCoupler.coupledTo, false); @@ -283,6 +475,21 @@ private void Server_SendCouplers() NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.frontCoupler, TrainCar.frontCoupler.IsCockOpen); NetworkLifecycle.Instance.Client.SendCockState(NetId, TrainCar.rearCoupler, TrainCar.rearCoupler.IsCockOpen); } + private void Server_SendCables() + { + if (!sendCables) + return; + sendCables = false; + + if(TrainCar.muModule == null) + return; + + if (TrainCar.muModule.frontCable.IsConnected) + NetworkLifecycle.Instance.Client.SendMuConnected(TrainCar.muModule.frontCable, TrainCar.muModule.frontCable.connectedTo, false); + + if (TrainCar.muModule.rearCable.IsConnected) + NetworkLifecycle.Instance.Client.SendMuConnected(TrainCar.muModule.rearCable, TrainCar.muModule.rearCable.connectedTo, false); + } private void Server_SendCargoState() { @@ -302,6 +509,67 @@ private void Server_SendHealthState() NetworkLifecycle.Instance.Server.SendCarHealthUpdate(NetId, TrainCar.CarDamage.currentHealth); } + public bool Server_ValidateCouplerInteraction(CommonCouplerInteractionPacket packet, ITransportPeer peer) + { + Multiplayer.LogDebug(() => + $"Server_ValidateCouplerInteraction([[{(CouplerInteractionType)packet.Flags}], {CurrentID}, {packet.NetId}], {peer.Id}) " + + $"isFront: {packet.IsFrontCoupler}, frontInteracting: {frontInteracting}, frontInteractionPeer: {frontInteractionPeer}, " + + $"rearInteracting: {rearInteracting}, rearInteractionPeer: {rearInteractionPeer}" + ); + //Ensure no one else is interacting + if (packet.IsFrontCoupler && frontInteracting && peer.Id != frontInteractionPeer || + packet.IsFrontCoupler == false && rearInteracting && peer.Id != rearInteractionPeer) + { + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) Failed to validate!"); + return false; + } + + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) No one interacting"); + + if (((CouplerInteractionType)packet.Flags).HasFlag(CouplerInteractionType.Start)) + { + if (packet.IsFrontCoupler) + { + frontInteracting = true; + frontInteractionPeer = peer.Id; + } + else + { + rearInteracting = true; + rearInteractionPeer = peer.Id; + } + } + else + { + if (packet.IsFrontCoupler) + frontInteracting = false; + else + rearInteracting = false; + } + + //todo: Additional checks for player location/proximity + + Multiplayer.LogDebug(() => $"Server_ValidateCouplerInteraction([{packet.Flags}, {CurrentID}, {packet.NetId}], {peer.Id}) Validation passed!"); + return true; + } + + private void Server_OnPlayerDisconnect(uint id) + { + //todo: resove player disconnection during chain interaction + if (frontInteractionPeer == id || rearInteractionPeer == id) + { + Multiplayer.LogWarning($"Server_OnPlayerDisconnect() Coupler interaction in unknown state [{CurrentID}, {NetId}] isFront: {frontInteractionPeer == id}"); + if (frontInteractionPeer == id) + { + frontInteracting = false ; + //NetworkLifecycle.Instance.Client.SendCouplerInteraction(cou, coupler, otherCoupler); + } + else + { + rearInteracting = false; + } + } + } #endregion #region Common @@ -310,6 +578,7 @@ private void Common_OnTick(uint tick) { if (UnloadWatcher.isUnloading) return; + Common_SendHandbrakePosition(); Common_SendFuses(); Common_SendPorts(); @@ -321,6 +590,7 @@ private void Common_SendHandbrakePosition() return; if (!TrainCar.brakeSystem.hasHandbrake) return; + handbrakeDirty = false; NetworkLifecycle.Instance.Client.SendHandbrakePositionChanged(NetId, brakeSystem.handbrakePosition); } @@ -334,6 +604,8 @@ public void Common_DirtyPorts(string[] portIds) { if (!simulationFlow.TryGetPort(portId, out Port _)) { + + Multiplayer.LogWarning($"Tried to dirty port {portId} on UNKNOWN but it doesn't exist!"); Multiplayer.LogWarning($"Tried to dirty port {portId} on {TrainCar.ID} but it doesn't exist!"); continue; } @@ -351,6 +623,7 @@ public void Common_DirtyFuses(string[] fuseIds) { if (!simulationFlow.TryGetFuse(fuseId, out Fuse _)) { + Multiplayer.LogWarning($"Tried to dirty port {fuseId} on UNKOWN but it doesn't exist!"); Multiplayer.LogWarning($"Tried to dirty port {fuseId} on {TrainCar.ID} but it doesn't exist!"); continue; } @@ -369,9 +642,18 @@ private void Common_SendPorts() float[] portValues = new float[portIds.Length]; foreach (string portId in dirtyPorts) { - float value = simulationFlow.fullPortIdToPort[portId].Value; - portValues[i++] = value; - lastSentPortValues[portId] = value; + if(simulationFlow.TryGetPort(portId, out Port port)) + { + float value = port.Value; + portValues[i] = value; + lastSentPortValues[portId] = value; + } + else + { + Multiplayer.LogWarning($"Failed to send port \"{portId}\" for [{CurrentID}, {NetId}]"); + } + + i++; } dirtyPorts.Clear(); @@ -387,8 +669,16 @@ private void Common_SendFuses() int i = 0; string[] fuseIds = dirtyFuses.ToArray(); bool[] fuseValues = new bool[fuseIds.Length]; + foreach (string fuseId in dirtyFuses) - fuseValues[i++] = simulationFlow.fullFuseIdToFuse[fuseId].State; + { + if(simulationFlow.TryGetFuse(fuseId, out Fuse fuse)) + fuseValues[i] = fuse.State; + else + Multiplayer.LogWarning($"SendFuses() [{CurrentID}, {NetId}] Failed to find fuse \"{fuseId}\""); + + i++; + } dirtyFuses.Clear(); @@ -409,21 +699,65 @@ private void Common_OnBrakeCylinderReleased() NetworkLifecycle.Instance.Client.SendBrakeCylinderReleased(NetId); } + private void Common_OnFireboxUpdate(float newFireboxValue) + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + var delta = Math.Abs(lastSentFireboxValue - newFireboxValue); + if (delta > MAX_FIREBOX_DELTA || (newFireboxValue == 0 && lastSentFireboxValue != 0)) + { + fireboxDirty = true; + lastSentFireboxValue = newFireboxValue; + } + + } + private void Common_OnPortUpdated(Port port) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; if (float.IsNaN(port.prevValue) && float.IsNaN(port.Value)) return; - if (lastSentPortValues.TryGetValue(port.id, out float value) && Mathf.Abs(value - port.Value) < 0.001f) + + bool hasLastSent = lastSentPortValues.TryGetValue(port.id, out float lastSentValue); + float delta = Mathf.Abs(lastSentValue - port.Value); + + if (port.valueType == PortValueType.STATE) + { + if (!hasLastSent || lastSentValue != port.Value) + { + dirtyPorts.Add(port.id); + } + } + else + { + if (!hasLastSent || delta > MAX_PORT_DELTA || (port.Value == 0 && lastSentValue != 0)) + { + dirtyPorts.Add(port.id); + } + } + } + + private void Common_OnPaintThemeChange(TrainCarPaint paintController) + { + if(paintController == null) return; - dirtyPorts.Add(port.id); + + Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() target: {paintController.TargetArea}, theme: {paintController.CurrentTheme.name}"); + + byte target = (byte)paintController.TargetArea; + var theme = PaintThemeLookup.Instance.GetThemeIndex(paintController.CurrentTheme); + + Multiplayer.LogDebug(() => $"Common_OnPaintThemeChange() sending [{CurrentID},{NetId}], target: {paintController.TargetArea}, theme: [{paintController.CurrentTheme.name},{theme}]"); + NetworkLifecycle.Instance?.Client.SendPaintThemeChangePacket(NetId,target,theme); } private void Common_OnFuseUpdated(Fuse fuse) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; + dirtyFuses.Add(fuse.id); } @@ -434,13 +768,21 @@ public void Common_UpdatePorts(CommonTrainPortsPacket packet) for (int i = 0; i < packet.PortIds.Length; i++) { - Port port = simulationFlow.fullPortIdToPort[packet.PortIds[i]]; - float value = packet.PortValues[i]; - if (port.type == PortType.EXTERNAL_IN) - port.ExternalValueUpdate(value); + if (simulationFlow.TryGetPort(packet.PortIds[i], out Port port)) + { + float value = packet.PortValues[i]; + + if (port.type == PortType.EXTERNAL_IN) + port.ExternalValueUpdate(value); + else + port.Value = value; + } else - port.Value = value; + { + Multiplayer.LogWarning($"Common_UpdatePorts() [{CurrentID}, {NetId}] Failed to find port \"{packet.PortIds[i]}\", Value: {packet.PortValues[i]}"); + } } + } public void Common_UpdateFuses(CommonTrainFusesPacket packet) @@ -449,9 +791,385 @@ public void Common_UpdateFuses(CommonTrainFusesPacket packet) return; for (int i = 0; i < packet.FuseIds.Length; i++) - simulationFlow.fullFuseIdToFuse[packet.FuseIds[i]].ChangeState(packet.FuseValues[i]); + if (simulationFlow.TryGetFuse(packet.FuseIds[i], out Fuse fuse)) + fuse.ChangeState(packet.FuseValues[i]); + else + Multiplayer.LogWarning($"UpdateFuses() [{CurrentID}, {NetId}] Failed to find fuse \"{packet.FuseIds[i]}\", Value: {packet.FuseValues[i]}"); } + public void Common_ReceiveCouplerInteraction(CommonCouplerInteractionPacket packet) + { + CouplerInteractionType flags = (CouplerInteractionType)packet.Flags; + Coupler coupler = packet.IsFrontCoupler ? TrainCar?.frontCoupler : TrainCar?.rearCoupler; + TrainCar otherCar = null; + Coupler otherCoupler = null; + + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() couplerNetId: {NetId}, coupler is front: {packet.IsFrontCoupler}, flags: {flags}, otherCouplerNetId: {packet.OtherNetId}, otherCoupler is front: {packet.IsFrontOtherCoupler}"); + + if (coupler == null) + { + Multiplayer.LogWarning($"Common_ReceiveCouplerInteraction() did not find coupler for [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}"); + return; + } + + if (packet.OtherNetId != 0) + { + if (GetTrainCar(packet.OtherNetId, out otherCar)) + otherCoupler = packet.IsFrontOtherCoupler ? otherCar?.frontCoupler : otherCar?.rearCoupler; + } + + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, otherCouplerNetId: {packet.OtherNetId}"); + + if (flags == CouplerInteractionType.NoAction) + { + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() Interaction rejected! [{CurrentID}, {NetId}]"); + //our interaction was denied + coupler.ChainScript?.knobGizmo?.ForceEndInteraction(); + couplerInteraction = null; + + if (coupler.ChainScript.state == originalState) + return; + + switch (originalState) + { + case ChainCouplerInteraction.State.Parked: + StartCoroutine(ParkCoupler(coupler)); + break; + case ChainCouplerInteraction.State.Dangling: + if (coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Tight) + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + + StartCoroutine(DangleCoupler(coupler)); + break; + case ChainCouplerInteraction.State.Attached_Loose: + if(coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Tight) + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + else + StartCoroutine(LooseAttachCoupler(coupler, originalCoupledTo)); + break; + case ChainCouplerInteraction.State.Attached_Tight: + if (coupler.ChainScript.state != ChainCouplerInteraction.State.Attached_Loose) + StartCoroutine(LooseAttachCoupler(coupler, originalCoupledTo)); + + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + break; + default: + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() Unable to return to last state! {originalState}"); + break; + } + return; + } + if (flags == CouplerInteractionType.Start && coupler != couplerInteraction) + { + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() Interaction started [{CurrentID}, {NetId}] isFront: {coupler.isFrontCoupler}"); + //We've received a start signal for a coupler we aren't interacting with + //Another player must be interacting, so let's block us from tampering with it + if (coupler?.ChainScript?.knobGizmo) + coupler.ChainScript.knobGizmo.InteractionAllowed = false; + if(coupler?.ChainScript?.screwButtonBase) + coupler.ChainScript.screwButtonBase.InteractionAllowed = false; + + return; + } + + if (coupler.ChainScript.state == ChainCouplerInteraction.State.Being_Dragged) + { + Multiplayer.LogDebug(() => $"Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, otherCouplerNetId: {packet.OtherNetId} Being Dragged!"); + coupler.ChainScript?.knobGizmo?.ForceEndInteraction(); + } + + if (flags.HasFlag(CouplerInteractionType.CouplerCouple) && packet.OtherNetId != 0) + { + Multiplayer.LogDebug(() => $"1 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} "); + if (otherCar != null) + { + Multiplayer.LogDebug(() => $"2 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); + StartCoroutine(LooseAttachCoupler(coupler, otherCoupler)); + } + } + + if (flags.HasFlag(CouplerInteractionType.CouplerPark)) + { + Multiplayer.LogDebug(() => $"3 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, current state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + + if (coupler.ChainScript.state != ChainCouplerInteraction.State.Attached_Tight) + StartCoroutine(ParkCoupler(coupler)); + else + Multiplayer.LogWarning(() => $"Received Park interaction for [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, but coupler is in the wrong state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + + Multiplayer.LogDebug(() => $"4 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} restorestate: {coupler.state}, current state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + } + + if (flags.HasFlag(CouplerInteractionType.CouplerDrop)) + { + Multiplayer.LogDebug(() => $"5 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags} restorestate: {coupler.state}, current state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + + if (coupler.ChainScript.state != ChainCouplerInteraction.State.Attached_Tight) + StartCoroutine(DangleCoupler(coupler)); + else + Multiplayer.LogWarning(() => $"Received Dangle interaction for [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, but coupler is in the wrong state: {coupler.state}, Chain state:{coupler.ChainScript.state}, isCoupled: {coupler.IsCoupled()}"); + } + + if (flags.HasFlag(CouplerInteractionType.CouplerLoosen)) + { + Multiplayer.LogDebug(() => $"6 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], flags: {flags} current state: {coupler.ChainScript.state}"); + if (coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Tight) + { + Multiplayer.LogDebug(() => $"7 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + } + else if(coupler.ChainScript.CurrentState == ChainCouplerInteraction.State.Disabled && coupler.state == ChainCouplerInteraction.State.Attached_Tight) + { + //if it's disabled we'll use the internal routines and the state will restore when this player sees the coupling next + coupler.SetChainTight(false); + } + } + + if (flags.HasFlag(CouplerInteractionType.CouplerTighten)) + { + Multiplayer.LogDebug(() => $"8 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], flags: {flags} current state: {coupler.ChainScript.state}"); + if (coupler.ChainScript.state == ChainCouplerInteraction.State.Attached_Loose) + { + Multiplayer.LogDebug(() => $"9 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Screw_Used); + } + else if (coupler.ChainScript.CurrentState == ChainCouplerInteraction.State.Disabled && coupler.state == ChainCouplerInteraction.State.Attached_Loose) + { + //if it's disabled we'll use the internal routines and the state will restore when this player sees the coupling next + coupler.SetChainTight(true); + } + } + + if (flags.HasFlag(CouplerInteractionType.CoupleViaUI)) + { + //if hose connect also requested, then we want everything to connect, otherwise only connect the chain + bool chainInteraction = !flags.HasFlag(CouplerInteractionType.HoseConnect); + + Multiplayer.LogDebug(() => $"10 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: [{flags}], other coupler: {otherCoupler != null}, chainInteraction: {chainInteraction}"); + if(otherCoupler != null) + { + Multiplayer.LogDebug(() => $"10A Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler state: {coupler.state}, other coupler state: {otherCoupler.state}, coupler coupledTo: {coupler?.coupledTo?.train?.ID}, other coupledTo: {otherCoupler?.coupledTo?.train?.ID}, chainInteraction: {chainInteraction}"); + var car = coupler.CoupleTo(otherCoupler, viaChainInteraction: chainInteraction); + + /* fix for bug in vanilla game */ + coupler.SetChainTight(true); + if (coupler.ChainScript.enabled) + { + coupler.ChainScript.enabled = false; + coupler.ChainScript.enabled = true; + } + /* end fix for bug */ + + Multiplayer.LogDebug(() => $"10B Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], result: {car != null}"); + //todo: rework hose and MU interactions + } + } + + if (flags.HasFlag(CouplerInteractionType.UncoupleViaUI)) + { + //if hose connect also requested, then we want everything to disconnect, otherwise only disconnect the chain + bool chainInteraction = !flags.HasFlag(CouplerInteractionType.HoseDisconnect); + + Multiplayer.LogDebug(() => $"11 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, chainInteraction: {chainInteraction}"); + CouplerLogic.Uncouple(coupler,viaChainInteraction: chainInteraction); + + /* fix for bug in vanilla game */ + coupler.state = ChainCouplerInteraction.State.Parked; + if (coupler.ChainScript.enabled) + { + coupler.ChainScript.enabled = false; + coupler.ChainScript.enabled = true; + } + /* end fix for bug */ + + //todo: rework hose and MU interactions + } + + if (flags.HasFlag(CouplerInteractionType.CoupleViaRemote)) + { + Multiplayer.LogDebug(() => $"12 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}, other coupler: {otherCoupler != null}"); + + if (TryGetComponent(out var couplingHandler)) + couplingHandler.Couple(); + } + + if (flags.HasFlag(CouplerInteractionType.UncoupleViaRemote)) + { + Multiplayer.LogDebug(() => $"13 Common_ReceiveCouplerInteraction() [{TrainCar?.ID}, {NetId}], coupler is front: {packet.IsFrontCoupler}, flags: {flags}"); + if (coupler != null) + { + coupler.Uncouple(true, false, false, false); + MultipleUnitModule.DisconnectCablesIfMultipleUnitSupported(coupler.train, coupler.isFrontCoupler, !coupler.isFrontCoupler); + } + } + + //presumably the interaction is now complete, release control to player + if (coupler?.ChainScript?.knobGizmo) + coupler.ChainScript.knobGizmo.InteractionAllowed = true; + if (coupler?.ChainScript?.screwButtonBase) + coupler.ChainScript.screwButtonBase.InteractionAllowed = true; + } + + private IEnumerator LooseAttachCoupler(Coupler coupler, Coupler otherCoupler) + { + if (coupler == null || coupler.ChainScript == null || + otherCoupler == null || otherCoupler.ChainScript == null || + otherCoupler.ChainScript.ownAttachPoint == null) + { + Multiplayer.LogDebug(() => $"LooseAttachCoupler() [{TrainCar?.ID}], Null reference! Coupler: {coupler != null}, chainscript: {coupler?.ChainScript != null}, other coupler: {otherCoupler != null}, other chainscript: {otherCoupler?.ChainScript != null}, other attach point: {otherCoupler?.ChainScript?.ownAttachPoint}"); + yield break; + } + + ChainCouplerInteraction ccInteraction = coupler.ChainScript; + + if(ccInteraction.CurrentState == ChainCouplerInteraction.State.Disabled) + { + //since it's disabled FSM events won't fire. Force a coupling if required, otherwise set state ready for player visibility trigger + + if (coupler.coupledTo == null) + coupler.CoupleTo(otherCoupler, true, true); + else + coupler.state = ChainCouplerInteraction.State.Attached_Loose; + + yield break; + } + + //Simulate player pickup + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Picked_Up_By_Player); + + //Set the knob position to the other coupler's hook + Vector3 targetHookPos = otherCoupler.ChainScript.ownAttachPoint.transform.position; + coupler.ChainScript.knob.transform.position = targetHookPos; + + //allow the follower and IK solver to update + coupler.ChainScript.Update_Being_Dragged(); + + //we need to allow the IK solver to calculate the chain ring anchor's position over a number of iterations + int x = 0; + float distance = float.MaxValue; + //game checks for Vector3.Distance(this.chainRingAnchor.position, this.closestAttachPoint.transform.position) < attachDistanceThreshold; + while (distance >= ChainCouplerInteraction.attachDistanceThreshold && x < MAX_COUPLER_ITERATIONS) + { + distance = Vector3.Distance(ccInteraction.chainRingAnchor.position, targetHookPos); + + x++; + yield return new WaitForSeconds(ccInteraction.ROTATION_SMOOTH_DURATION); + } + + //Drop the chain + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Dropped_By_Player); + } + + private IEnumerator ParkCoupler(Coupler coupler) + { + ChainCouplerInteraction ccInteraction = coupler.ChainScript; + + if (ccInteraction.CurrentState == ChainCouplerInteraction.State.Disabled) + { + //since it's disabled FSM events won't fire, but state will be restored when the coupling is visible to the current player + if(coupler.state == ChainCouplerInteraction.State.Attached_Loose && coupler.coupledTo != null) + coupler.Uncouple(true, false, false, true); + + coupler.state = ChainCouplerInteraction.State.Parked; + + yield break; + } + + //Simulate player pickup + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Picked_Up_By_Player); + + //Set the knob position + Vector3 parkPos = coupler.ChainScript.parkedAnchor.position; + + coupler.ChainScript.knob.transform.position = parkPos; + + //allow the follower and IK solver to update + coupler.ChainScript.Update_Being_Dragged(); + + //we need to allow the IK solver to calculate the chain ring anchor's position over a number of iterations + int x = 0; + float distance = float.MaxValue; + //game checks for Vector3.Distance(this.chainRingAnchor.position, this.parkedAnchor.position) < parkDistanceThreshold; + //need to make sure we are closer than the threshold before dropping + while (distance > ChainCouplerInteraction.parkDistanceThreshold && x < MAX_COUPLER_ITERATIONS) + { + distance = Vector3.Distance(ccInteraction.chainRingAnchor.position, ccInteraction.parkedAnchor.position); + + x++; + yield return new WaitForSeconds(ccInteraction.ROTATION_SMOOTH_DURATION); + } + + //Drop the chain + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Dropped_By_Player); + } + private IEnumerator DangleCoupler(Coupler coupler) + { + ChainCouplerInteraction ccInteraction = coupler.ChainScript; + + if (ccInteraction.CurrentState == ChainCouplerInteraction.State.Disabled) + { + //since it's disabled FSM events won't fire, but state will be restored when the coupling is visible to the current player + if (coupler.state == ChainCouplerInteraction.State.Attached_Loose && coupler.coupledTo != null) + coupler.Uncouple(true, false, false, true); + + coupler.state = ChainCouplerInteraction.State.Dangling; + + yield break; + } + + //Simulate player pickup + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Picked_Up_By_Player); + + Vector3 parkPos = coupler.ChainScript.parkedAnchor.position; + + //Set the knob position + coupler.ChainScript.knob.transform.position = parkPos + Vector3.down; //ensure we are not near the park anchor or other car's anchor + + //allow the follower and IK solver to update + coupler.ChainScript.Update_Being_Dragged(); + + //we need to allow the IK solver to calculate the chain ring anchor's position over a number of iterations + int x = 0; + float distance = float.MinValue; + //game checks for Vector3.Distance(this.chainRingAnchor.position, this.parkedAnchor.position) < parkDistanceThreshold; + //to determine if it should be parked or dangled, need to make sure we are at least at the threshold before dropping + while (distance <= ChainCouplerInteraction.parkDistanceThreshold && x < MAX_COUPLER_ITERATIONS) + { + distance = Vector3.Distance(ccInteraction.chainRingAnchor.position, ccInteraction.parkedAnchor.position); + + x++; + yield return new WaitForSeconds(ccInteraction.ROTATION_SMOOTH_DURATION); + } + + //Drop the chain + coupler.ChainScript.fsm.Fire(ChainCouplerInteraction.Trigger.Dropped_By_Player); + } + + public void Common_ReceivePaintThemeUpdate(TrainCarPaint.Target target, PaintTheme paint) + { + TrainCarPaint targetPaint = null; + + if (target == TrainCarPaint.Target.Interior) + { + Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], targeting Interior"); + targetPaint = TrainCar.PaintInterior; + } + else if (target == TrainCarPaint.Target.Exterior) + { + Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], targeting Exterior"); + targetPaint = TrainCar.PaintExterior; + } + + if (targetPaint == null || !targetPaint.IsSupported(paint)) + { + Multiplayer.LogWarning($"Received Paint Theme update for [{CurrentID}, {NetId}], but {paint?.assetName} is not supported"); + return; + } + + targetPaint.currentTheme = paint; + targetPaint.UpdateTheme(); + TrainCar.OnPaintThemeChanged(targetPaint); + } #endregion #region Client @@ -462,30 +1180,182 @@ private IEnumerator Client_InitLater() yield return null; while ((client_bogie2Queue = bogie2.GetComponent()) == null) yield return null; - client_Initialized = true; + + Client_Initialized = true; } public void Client_ReceiveTrainPhysicsUpdate(in TrainsetMovementPart movementPart, uint tick) { - if (!client_Initialized) + if (!Client_Initialized) return; + if (TrainCar.isEligibleForSleep) TrainCar.ForceOptimizationState(false); - if (movementPart.IsRigidbodySnapshot) + if (movementPart.typeFlag == TrainsetMovementPart.MovementType.RigidBody) { + //Multiplayer.LogDebug(() => $"Client_ReceiveTrainPhysicsUpdate({TrainCar.ID}, {tick}): is RigidBody"); TrainCar.Derail(); TrainCar.stress.ResetTrainStress(); + if (TrainCar.rb != null) + TrainCar.rb.constraints = RigidbodyConstraints.FreezeAll; + Client_trainRigidbodyQueue.ReceiveSnapshot(movementPart.RigidbodySnapshot, tick); } else { + //move the car to the correct position first - maybe? + if (movementPart.typeFlag.HasFlag(TrainsetMovementPart.MovementType.Position)) + { + TrainCar.transform.position = movementPart.Position + WorldMover.currentMove; + TrainCar.transform.rotation = movementPart.Rotation; + + //clear the queues? + Client_trainSpeedQueue.Clear(); + Client_trainRigidbodyQueue.Clear(); + client_bogie1Queue.Clear(); + client_bogie2Queue.Clear(); + + TrainCar.stress.ResetTrainStress(); + } + Client_trainSpeedQueue.ReceiveSnapshot(movementPart.Speed, tick); TrainCar.stress.slowBuildUpStress = movementPart.SlowBuildUpStress; client_bogie1Queue.ReceiveSnapshot(movementPart.Bogie1, tick); client_bogie2Queue.ReceiveSnapshot(movementPart.Bogie2, tick); + + } + + if (!TrainCar.derailed && TrainCar.rb != null) + TrainCar.rb.constraints = RigidbodyConstraints.None; + } + + public void Client_ReceiveBrakeStateUpdate(ClientboundBrakeStateUpdatePacket packet) + { + if (brakeSystem == null) + return; + + if (!hasSimFlow) + return; + + brakeSystem.SetMainReservoirPressure(packet.MainReservoirPressure); + + brakeSystem.brakePipePressure = packet.BrakePipePressure; + brakeSystem.brakeset.pipePressure = packet.BrakePipePressure; + + brakeSystem.brakeCylinderPressure = packet.BrakeCylinderPressure; + + if (brakeSystem.heatController == null) + return; + + brakeSystem.heatController.overheatPercentage = packet.OverheatPercent; + brakeSystem.heatController.overheatReductionFactor = packet.OverheatReductionFactor; + brakeSystem.heatController.temperature = packet.Temperature; } + private void Client_OnAddCoal(float coalMassDelta) + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + if (coalMassDelta <= 0) + return; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnAddCoal({TrainCar.ID}): coalMassDelta: {coalMassDelta}"); + NetworkLifecycle.Instance.Client.SendAddCoal(NetId, coalMassDelta); + } + + private void Client_OnIgnite(float ignition) + { + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + if (ignition == 0f) + return; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"Common_OnIgnite({TrainCar.ID})"); + NetworkLifecycle.Instance.Client.SendFireboxIgnition(NetId); + } + + public void Client_ReceiveFireboxStateUpdate(float fireboxContents, bool isOn) + { + if (firebox == null) + return; + + if (!hasSimFlow) + return; + + firebox.fireboxContentsPort.Value = fireboxContents; + firebox.fireOnPort.Value = isOn ? 1f : 0f; + } + + public void Client_CouplerStateChange(ChainCouplerInteraction.State state, Coupler coupler) + { + Multiplayer.LogDebug(() => $"1 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}], coupler is front: {coupler?.isFrontCoupler}"); + + //if we are processing a packet, then these state changes are likely triggered by a received update, not player interaction + //in future, maybe patch OnGrab() or add logic to add/remove action subscriptions + if (NetworkLifecycle.Instance.IsProcessingPacket) + return; + + CouplerInteractionType interactionFlags = CouplerInteractionType.NoAction; + Coupler otherCoupler = null; + + switch (state) + { + case ChainCouplerInteraction.State.Being_Dragged: + couplerInteraction = coupler; + originalState = coupler.state; + originalCoupledTo = coupler.coupledTo; + interactionFlags = CouplerInteractionType.Start; + Multiplayer.LogDebug(() => $"3 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + break; + + case ChainCouplerInteraction.State.Attached_Loose: + if (couplerInteraction != null) + { + //couldn't find an appropriate constant in the game code, other than the default value + //at B99.3 this distance is 1.5f for both default and constant/magic number + otherCoupler = coupler.GetFirstCouplerInRange(); + Multiplayer.LogDebug(() => $"4 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}] coupledTo: {coupler?.coupledTo?.train?.ID}, first Coupler: {otherCoupler?.train?.ID}"); + interactionFlags = CouplerInteractionType.CouplerCouple; + } + break; + + case ChainCouplerInteraction.State.Parked: + if (couplerInteraction != null) + { + Multiplayer.LogDebug(() => $"6 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + interactionFlags = CouplerInteractionType.CouplerPark; + } + break; + + case ChainCouplerInteraction.State.Dangling: + if (couplerInteraction != null) + { + Multiplayer.LogDebug(() => $"7 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + interactionFlags = CouplerInteractionType.CouplerDrop; + } + break; + + default: + //nothing to do + break; + } + + if (interactionFlags != CouplerInteractionType.NoAction) + { + Multiplayer.LogDebug(() => $"8 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}], coupler is front: {coupler?.isFrontCoupler}, Sending: {interactionFlags}"); + NetworkLifecycle.Instance.Client.SendCouplerInteraction(interactionFlags, coupler, otherCoupler); + + //finished interaction, clear flag + if (interactionFlags != CouplerInteractionType.Start) + couplerInteraction = null; + + return; + } + Multiplayer.LogDebug(() => $"9 Client_CouplerStateChange({state}) trainCar: [{TrainCar?.ID}, {NetId}]"); + } #endregion } diff --git a/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs new file mode 100644 index 0000000..1e8dd87 --- /dev/null +++ b/Multiplayer/Components/Networking/Train/PaintThemeLookup.cs @@ -0,0 +1,106 @@ +using DV.Customization.Paint; +using DV.Utils; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using JetBrains.Annotations; + + +namespace Multiplayer.Components.Networking.Train; + +public class PaintThemeLookup : SingletonBehaviour +{ + private readonly Dictionary themeIndices = []; + private string[] themeNames; + + protected override void Awake() + { + base.Awake(); + themeNames = Resources.LoadAll("").Where(x => x is PaintTheme) + .Select(x => x.name.ToLower()) + .ToArray(); + + for (sbyte i = 0; i < themeNames.Length; i++) + { + themeIndices.Add(themeNames[i], i); + } + + Multiplayer.LogDebug(() => + { + return $"Registered Paint Themes:\r\n{string.Join("\r\n", themeNames.Select((name, index) => $"{index}: {name}"))}"; + }); + } + + public PaintTheme GetPaintTheme(sbyte index) + { + PaintTheme theme = null; + + var themeName = GetThemeName(index); + + if (themeName != null) + PaintTheme.TryLoad(GetThemeName(index), out theme); + + return theme; + } + + public string GetThemeName(sbyte index) + { + return (index >= 0 && index < themeNames.Length) ? themeNames[index] : null; + } + + public sbyte GetThemeIndex(PaintTheme theme) + { + if(theme == null) + return -1; + + return GetThemeIndex(theme.assetName); + } + + public sbyte GetThemeIndex(string themeName) + { + return themeIndices.TryGetValue(themeName.ToLower(), out sbyte index) ? index : (sbyte)-1; + } + + /* + * Allow other mods to register custom themes + + public void RegisterTheme(string themeName) + { + themeName = themeName.ToLower(); + if (!themeIndices.ContainsKey(themeName)) + { + // Add to array + Array.Resize(ref themeNames, themeNames.Length + 1); + int newIndex = themeNames.Length - 1; + themeNames[newIndex] = themeName; + + // Add to dictionary + themeIndices.Add(themeName, newIndex); + } + } + + public void UnregisterTheme(string themeName) + { + themeName = themeName.ToLower(); + if (themeIndices.TryGetValue(themeName, out int index)) + { + // Remove from dictionary + themeIndices.Remove(themeName); + + // Remove from array and shift remaining elements + for (int i = index; i < themeNames.Length - 1; i++) + { + themeNames[i] = themeNames[i + 1]; + themeIndices[themeNames[i]] = i; // Update indices + } + Array.Resize(ref themeNames, themeNames.Length - 1); + } + } + */ + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(PaintThemeLookup)}]"; + } +} diff --git a/Multiplayer/Components/Networking/UI/ChatGUI.cs b/Multiplayer/Components/Networking/UI/ChatGUI.cs new file mode 100644 index 0000000..3522a6a --- /dev/null +++ b/Multiplayer/Components/Networking/UI/ChatGUI.cs @@ -0,0 +1,677 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DV; +using DV.UI; +using Multiplayer.Utils; +using TMPro; +using UnityEngine; +using UnityEngine.UI; +using System.Text.RegularExpressions; +using DV.Common; +using System.Collections; +using Multiplayer.Networking.Managers.Server; +using Multiplayer.Components.Networking.Player; +using static System.Net.Mime.MediaTypeNames; + + +namespace Multiplayer.Components.Networking.UI; + +//[RequireComponent(typeof(Canvas))] +//[RequireComponent(typeof(CanvasScaler))] +[RequireComponent(typeof(RectTransform))] +public class ChatGUI : MonoBehaviour +{ + private const float PANEL_LEFT_MARGIN = 20f; //How far to inset the chat window from the left edge of the screen + private const float PANEL_BOTTOM_MARGIN = 50f; //How far to inset the chat window from the bottom of the screen + private const float PANEL_FADE_DURATION = 1f; + private const float MESSAGE_INSET = 15f; //How far to inset the message text from the edge of chat the window + + private const int MESSAGE_MAX_HISTORY = 50; //Maximum messages to keep in the queue + private const int MESSAGE_TIMEOUT = 10; //Maximum time to show an incoming message before fade + private const int MESSAGE_MAX_LENGTH = 500; //Maximum length of a single message + private const int MESSAGE_RATE_LIMIT = 10; //Limit how quickly a user can send messages (also enforced server side) + + private const int SEND_MAX_HISTORY = 10; //How many previous messages to remember + + private GameObject messagePrefab; + + private List messageList = new List(); + private List sendHistory = new List(); + + private TMP_InputField chatInputIF; + private ScrollRect scrollRect; + private RectTransform chatPanel; + private CanvasGroup canvasGroup; + + private GameObject panelGO; + private GameObject textInputGO; + private GameObject scrollViewGO; + + private bool isOpen = false; + private bool showingMessage = false; + + private int sendHistoryIndex = -1; + private bool whispering = false; + private string lastRecipient; + + //private CustomFirstPersonController player; + //private HotbarController hotbarController; + + private float timeOut; //time-out counter for hiding the messages + //private float testTimeOut; + + private GameFeatureFlags.Flag denied; + + private void Awake() + { + Multiplayer.Log("ChatGUI Awake() called"); + + SetupOverlay(); //sizes and positions panel + + BuildUI(); //Creates input fields and scroll area + + panelGO.SetActive(false); //We don't need this to be visible when the game launches + textInputGO.SetActive(false); + + //Find the player and toolbar so we can block input + /* + player = GameObject.FindObjectOfType(); + if(player == null) + { + Multiplayer.Log("Failed to find CustomFirstPersonController"); + return; + } + + hotbarController = GameObject.FindObjectOfType(); + if (hotbarController == null) + { + Multiplayer.Log("Failed to find HotbarController"); + return; + } + */ + + } + + private void OnEnable() + { + chatInputIF.onSubmit.AddListener(Submit); + chatInputIF.onValueChanged.AddListener(ChatInputChange); + + } + + private void OnDisable() + { + chatInputIF.onSubmit.RemoveAllListeners(); + chatInputIF.onValueChanged.RemoveAllListeners(); + } + + private void Update() + { + //Handle keypresses to open/close the chat window + if (!isOpen && Input.GetKeyDown(KeyCode.Return) && !AppUtil.Instance.IsPauseMenuOpen) + { + isOpen = true; //whole panel is open + showingMessage = false; //We don't want to time out + + ShowPanel(); + textInputGO.SetActive(isOpen); + + sendHistoryIndex = sendHistory.Count; + + if (whispering) + { + chatInputIF.text = "/w " + lastRecipient + ' '; + chatInputIF.caretPosition = chatInputIF.text.Length; + } + + BlockInput(true); + } + else if (isOpen) + { + //Check for closing window + if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Return)) + { + isOpen = false; + if (!showingMessage) + { + textInputGO.SetActive(isOpen); + HidePanel(); + } + + BlockInput(false); + }else if (Input.GetKeyDown(KeyCode.UpArrow)) + { + sendHistoryIndex--; + if (sendHistory.Count > 0 && sendHistoryIndex < sendHistory.Count) + { + chatInputIF.text = sendHistory[sendHistoryIndex]; + chatInputIF.caretPosition = chatInputIF.text.Length; + } + }else if (Input.GetKeyDown(KeyCode.DownArrow)) + { + sendHistoryIndex++; + if (sendHistory.Count > 0 && sendHistoryIndex >= 0) + { + chatInputIF.text = sendHistory[sendHistoryIndex]; + chatInputIF.caretPosition = chatInputIF.text.Length; + } + } + } + + //Maintain focus on the text input field + if(isOpen && !chatInputIF.isFocused) + { + chatInputIF.ActivateInputField(); + } + + //After a message is sent/received, keep displaying it for the timeout period + //Would be nice to add a fadeout in future + if (showingMessage && !textInputGO.activeSelf) + { + timeOut += Time.deltaTime; + + if (timeOut >= MESSAGE_TIMEOUT) + { + showingMessage = false ; + //panelGO.SetActive(false); + HidePanel(); + } + } + + ////testTimeOut += Time.deltaTime; + //if (testTimeOut >= 60) + //{ + // testTimeOut = 0; + // ReceiveMessage("Morm: Test TimeOut"); + //} + } + + public void Submit(string text) + { + text = text.Trim(); + + if (text.Length > 0) + { + //Strip any injected formatting + text = Regex.Replace(text, "", string.Empty, RegexOptions.IgnoreCase); + + //check for whisper + if(CheckForWhisper(text, out string localMessage, out string recipient)) + { + whispering = true; + lastRecipient = recipient; + + if (localMessage == null || localMessage == string.Empty) + return; + + if (lastRecipient.Contains(" ")) + { + lastRecipient = '"' + lastRecipient + '"'; + } + + AddMessage("You (" + recipient + "): " + localMessage + ""); + } + else + { + whispering = false; + AddMessage("You: " + text + ""); + } + + //add to send history + if (sendHistory.Count >= SEND_MAX_HISTORY) + { + sendHistory.RemoveAt(0); + } + + //add to the history - if already there, we'll relocate it to the end + int exists = sendHistory.IndexOf(text); + if (exists != -1) + sendHistory.RemoveAt(exists); + + sendHistory.Add(text); + + //send to server + NetworkLifecycle.Instance.Client.SendChat(text); + + //reset any timeouts + timeOut = 0; + showingMessage = true; + } + + chatInputIF.text = ""; + + textInputGO.SetActive(false); + BlockInput(false); + + return; + } + + private void ChatInputChange(string message) + { + //Multiplayer.LogDebug(() => $"ChatInputChange({message})"); + + //allow the user to clear text + if(Input.GetKeyDown(KeyCode.Backspace) || Input.GetKeyDown(KeyCode.Delete)) + return; + + if (CheckForWhisper(message, out string localMessage, out string recipient)) + { + //Multiplayer.LogDebug(()=>$"ChatInputChange: message: \"{message}\", localMessage: \"{(localMessage == null ? "null" : localMessage)}" + + // $"\", recipient: \"{(recipient == null ? "null" : recipient)}\""); + + if (localMessage == null || localMessage == string.Empty) + { + + string closestMatch = NetworkLifecycle.Instance.Client.ClientPlayerManager.Players + .Where(player => player.Username.ToLower().StartsWith(recipient.ToLower())) + .OrderBy(player => player.Username.Length) + .ThenByDescending(player => player.Username) + .ToList() + .FirstOrDefault().Username; + + /* + Multiplayer.Log($"ChatInputChange: closesMatch: {(closestMatch == null? "null" : closestMatch.Username)}"); + + + if(closestMatch == null) + return; + + bool quoteFlag = false; + if (match.Contains(' ')) + { + match = '"' + match + '"'; + quoteFlag = true; + } + + Multiplayer.Log($"ChatInput: recipient {recipient}, qF: {quoteFlag}, match: {match}, compare {recipient == closestMatch}"); + */ + + //if we have a match, allow the client to type + if (closestMatch == null || recipient == closestMatch) + return; + + //update the textbox + chatInputIF.SetTextWithoutNotify("/w " + closestMatch); + + //Multiplayer.Log($"ChatInput: length {chatInputIF.text.Length}, anchor: {"/w ".Length + recipient.Length + (quoteFlag ? 1 : 0)}"); + + //select the trailing match chars + chatInputIF.caretPosition = chatInputIF.text.Length; // Set caret to end of text + //chatInputIF.selectionAnchorPosition = chatInputIF.text.Length - "/w ".Length - recipient.Length - (quoteFlag?1:0) + 1; + chatInputIF.selectionAnchorPosition = "/w ".Length + recipient.Length;// + (quoteFlag?1:0); + + + } + } + + } + + private bool CheckForWhisper(string message, out string localMessage, out string recipient) + { + recipient = ""; + localMessage = ""; + + + if (message.StartsWith("/") && message.Length > (ChatManager.COMMAND_WHISPER_SHORT.Length + 2)) + { + //Multiplayer.LogDebug(()=>"CheckForWhisper() starts with /"); + string command = message.Substring(1).Split(' ')[0]; + switch (command) + { + case ChatManager.COMMAND_WHISPER_SHORT: + localMessage = message.Substring(ChatManager.COMMAND_WHISPER_SHORT.Length + 2); + break; + case ChatManager.COMMAND_WHISPER: + localMessage = message.Substring(ChatManager.COMMAND_WHISPER.Length + 2); + break; + + //allow messages that are not whispers to go through + default: + localMessage = message; + return false; + } + + if (localMessage == null || localMessage == string.Empty) + { + localMessage = message; + return false; + } + + /* + //Check if name is in Quotes e.g. '/w "Mr Noname" my message' + if (localMessage.StartsWith("\"")) + { + Multiplayer.Log("CheckForWhisper() starts with \""); + int endQuote = localMessage.Substring(1).IndexOf('"'); + Multiplayer.Log($"CheckForWhisper() starts with \" - indexOf, eQ: {endQuote}"); + if (endQuote <=1) + { + recipient = localMessage.Substring(1); + localMessage = string.Empty;//message; + return true; + } + + Multiplayer.Log("CheckForWhisper() remove quote"); + recipient = localMessage.Substring(1, endQuote); + localMessage = localMessage.Substring(recipient.Length + 3); + } + else + { + Multiplayer.Log("CheckForWhisper() no quote"); + */ + recipient = localMessage.Split(' ')[0]; + if (localMessage.Length > (recipient.Length + 2)) + { + localMessage = localMessage.Substring(recipient.Length + 1); + } + else + { + localMessage = ""; + } + //} + + return true; + } + + localMessage = message; + return false; + } + + public void ReceiveMessage(string message) + { + + if (message.Trim().Length > 0) + { + //add locally + AddMessage(message); + } + + timeOut = 0; + showingMessage = true; + + ShowPanel(); + //panelGO.SetActive(true); + } + + private void AddMessage(string text) + { + if (messageList.Count >= MESSAGE_MAX_HISTORY) + { + GameObject.Destroy(messageList[0]); + messageList.RemoveAt(0); + } + + GameObject newMessage = Instantiate(messagePrefab, chatPanel); + newMessage.GetComponent().text = text; + messageList.Add(newMessage); + + scrollRect.verticalNormalizedPosition = 0f; //scroll to the bottom - maybe later we need some logic for this? + } + + + #region UI + + + public void ShowPanel() + { + StopCoroutine(FadeOut()); + panelGO.SetActive(true); + canvasGroup.alpha = 1f; + } + + public void HidePanel() + { + StartCoroutine(FadeOut()); + } + + private IEnumerator FadeOut() + { + float startAlpha = canvasGroup.alpha; + float elapsed = 0f; + + while (elapsed < PANEL_FADE_DURATION) + { + elapsed += Time.deltaTime; + canvasGroup.alpha = Mathf.Lerp(startAlpha, 0f, elapsed / PANEL_FADE_DURATION); + yield return null; + } + + canvasGroup.alpha = 0f; + panelGO.SetActive(false); + } + + private void SetupOverlay() + { + //Setup the host object + RectTransform myRT = this.transform.GetComponent(); + myRT.sizeDelta = new Vector2(Screen.width, Screen.height); + myRT.anchorMin = Vector2.zero; + myRT.anchorMax = Vector2.zero; + myRT.pivot = Vector2.zero; + myRT.anchoredPosition = Vector2.zero; + + + // Create a Panel + panelGO = new GameObject("OverlayPanel"); + panelGO.transform.SetParent(this.transform, false); + RectTransform rectTransform = panelGO.AddComponent(); + rectTransform.sizeDelta = new Vector2(Screen.width * 0.25f, Screen.height * 0.25f); + rectTransform.anchorMin = Vector2.zero; + rectTransform.anchorMax = Vector2.zero; + rectTransform.pivot = Vector2.zero; + rectTransform.anchoredPosition = new Vector2(PANEL_LEFT_MARGIN, PANEL_BOTTOM_MARGIN); + + canvasGroup = panelGO.AddComponent(); // Add CanvasGroup for fade effect + } + + private void BuildUI() + { + GameObject scrollViewPrefab = null; + GameObject inputPrefab; + + //get prefabs + PopupNotificationReferences popup = GameObject.FindObjectOfType(); + SaveLoadController saveLoad = GameObject.FindObjectOfType(); + + if (popup == null) + { + Multiplayer.Log("Could not find PopupNotificationReferences"); + return; + } + else + { + inputPrefab = popup.popupTextInput.FindChildByName("TextFieldTextIcon"); + } + + if (saveLoad == null) + { + //Multiplayer.Log("Could not find SaveLoadController, attempting to instantiate"); + AppUtil.Instance.PauseGame(); + + Multiplayer.Log("Paused"); + + saveLoad = FindObjectOfType().saveLoadController; + + if (saveLoad == null) + { + Multiplayer.LogError("Failed to get SaveLoadController"); + } + else + { + //Multiplayer.Log("Made a SaveLoadController!"); + scrollViewPrefab = saveLoad.FindChildByName("Scroll View"); + + if (scrollViewPrefab == null) + { + Multiplayer.LogError("Could not find scrollViewPrefab"); + + } + else + { + scrollViewPrefab = Instantiate(scrollViewPrefab); + } + } + + AppUtil.Instance.UnpauseGame(); + } + else + { + scrollViewPrefab = saveLoad.FindChildByName("Scroll View"); + } + + + if (inputPrefab == null) + { + Multiplayer.Log("Could not find inputPrefab"); + return; + } + if (scrollViewPrefab == null) + { + Multiplayer.Log("Could not find scrollViewPrefab"); + return; + } + + + //Add an input box + textInputGO = Instantiate(inputPrefab); + textInputGO.name = "Chat Input"; + textInputGO.transform.SetParent(panelGO.transform, false); + + //Remove redundant components + GameObject.Destroy(textInputGO.FindChildByName("icon")); + GameObject.Destroy(textInputGO.FindChildByName("image select")); + GameObject.Destroy(textInputGO.FindChildByName("image hover")); + GameObject.Destroy(textInputGO.FindChildByName("image click")); + + //Position input + RectTransform textInputRT = textInputGO.GetComponent(); + textInputRT.pivot = Vector3.zero; + textInputRT.anchorMin = Vector2.zero; + textInputRT.anchorMax = new Vector2(1f, 0); + + textInputRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Bottom, 0, 20f); + textInputRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, 1f); + + RectTransform panelRT = panelGO.GetComponent(); + textInputRT.sizeDelta = new Vector2 (panelRT.rect.width, 40f); + + //Setup input + chatInputIF = textInputGO.GetComponent(); + chatInputIF.onFocusSelectAll = false; + chatInputIF.characterLimit = MESSAGE_MAX_LENGTH; + chatInputIF.richText=false; + + //Setup placeholder + chatInputIF.placeholder.GetComponent().richText = false; + chatInputIF.placeholder.GetComponent().text = Locale.CHAT_PLACEHOLDER;// "Type a message and press Enter!"; //translate + //Setup input renderer + TMP_Text chatInputRenderer = textInputGO.FindChildByName("text [noloc]").GetComponent(); + chatInputRenderer.fontSize = 18; + chatInputRenderer.richText = false; + chatInputRenderer.parseCtrlCharacters = false; + + + + //Add a new scroll pane + scrollViewGO = Instantiate(scrollViewPrefab); + scrollViewGO.name = "Chat Scroll"; + scrollViewGO.transform.SetParent(panelGO.transform, false); + + //Position scroll pane + RectTransform scrollViewRT = scrollViewGO.GetComponent(); + scrollViewRT.pivot = Vector3.zero; + scrollViewRT.anchorMin = Vector2.zero; + scrollViewRT.anchorMax = new Vector2(1f, 0); + + scrollViewRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Bottom, textInputRT.rect.height, 20f); + scrollViewRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, 1f); + + scrollViewRT.sizeDelta = new Vector2(panelRT.rect.width, panelRT.rect.height - textInputRT.rect.height); + + + //Setup scroll pane + GameObject viewport = scrollViewGO.FindChildByName("Viewport"); + RectTransform viewportRT = viewport.GetComponent(); + scrollRect = scrollViewGO.GetComponent(); + + viewportRT.pivot = new Vector2(0.5f, 0.5f); + viewportRT.anchorMin = Vector2.zero; + viewportRT.anchorMax = Vector2.one; + viewportRT.offsetMin = Vector2.zero; + viewportRT.offsetMax = Vector2.zero; + + scrollRect.viewport = scrollViewRT; + + //set up content + GameObject.Destroy(scrollViewGO.FindChildByName("GRID VIEW").gameObject); + GameObject content = new GameObject("Content", typeof(RectTransform), typeof(ContentSizeFitter), typeof(VerticalLayoutGroup)); + content.transform.SetParent(viewport.transform, false); + + ContentSizeFitter contentSF = content.GetComponent(); + contentSF.verticalFit = ContentSizeFitter.FitMode.PreferredSize; + + VerticalLayoutGroup contentVLG = content.GetComponent(); + contentVLG.childAlignment = TextAnchor.LowerLeft; + contentVLG.childControlWidth = false; + contentVLG.childControlHeight = true; + contentVLG.childForceExpandWidth = true; + contentVLG.childForceExpandHeight = false; + + chatPanel = content.GetComponent(); + chatPanel.pivot = Vector2.zero; + chatPanel.anchorMin = Vector2.zero; + chatPanel.anchorMax = new Vector2(1f, 0f); + chatPanel.offsetMin = Vector2.zero; + chatPanel.offsetMax = Vector2.zero; + scrollRect.content = chatPanel; + + chatPanel.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, MESSAGE_INSET, chatPanel.rect.width - MESSAGE_INSET); + + //Realign vertical scroll bar + RectTransform scrollBarRT = scrollRect.verticalScrollbar.transform.GetComponent(); + scrollBarRT.SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, scrollViewRT.rect.height); + + + + //Build message prefab + messagePrefab = new GameObject("Message Text", typeof(TextMeshProUGUI)); + + RectTransform messagePrefabRT = messagePrefab.GetComponent(); + messagePrefabRT.pivot = new Vector2(0.5f, 0.5f); + messagePrefabRT.anchorMin = new Vector2(0f, 1f); + messagePrefabRT.anchorMax = new Vector2(0f, 1f); + messagePrefabRT.offsetMin = new Vector2(0f, 0f); + messagePrefabRT.offsetMax = Vector2.zero; + messagePrefabRT.sizeDelta = new Vector2(chatPanel.rect.width, messagePrefabRT.rect.height); + + TextMeshProUGUI messageTM = messagePrefab.GetComponent(); + messageTM.textWrappingMode = TextWrappingModes.Normal; + messageTM.fontSize = 18; + messageTM.text = "Morm: Hurry up!"; + } + + private void BlockInput(bool block) + { + //player.Locomotion.inputEnabled = !block; + //hotbarController.enabled = !block; + if (block) + { + denied = GameFeatureFlags.DeniedFlags; + + GameFeatureFlags.Deny(GameFeatureFlags.Flag.ALL); + CursorManager.Instance.RequestCursor(this, true); + //InputFocusManager.Instance.TakeKeyboardFocus(); + } + else + { + GameFeatureFlags.Allow(GameFeatureFlags.Flag.ALL); + GameFeatureFlags.Deny(denied); + CursorManager.Instance.RequestCursor(this, false); + + //InputFocusManager.Instance.ReleaseKeyboardFocus(); + } + } + + #endregion +} diff --git a/Multiplayer/Components/Networking/NetworkStatsGui.cs b/Multiplayer/Components/Networking/UI/NetworkStatsGui.cs similarity index 64% rename from Multiplayer/Components/Networking/NetworkStatsGui.cs rename to Multiplayer/Components/Networking/UI/NetworkStatsGui.cs index ab05efe..720b5f5 100644 --- a/Multiplayer/Components/Networking/NetworkStatsGui.cs +++ b/Multiplayer/Components/Networking/UI/NetworkStatsGui.cs @@ -5,7 +5,7 @@ using LiteNetLib; using UnityEngine; -namespace Multiplayer.Components.Networking; +namespace Multiplayer.Components.Networking.UI; public class NetworkStatsGui : MonoBehaviour { @@ -17,8 +17,8 @@ public class NetworkStatsGui : MonoBehaviour private long bytesSentPerSecond; private long packetsReceivedPerSecond; private long packetsSentPerSecond; - private Dictionary packetsWrittenByType; - private Dictionary bytesWrittenByType; + //private Dictionary packetsWrittenByType; + //private Dictionary bytesWrittenByType; private Coroutine updateCoro; @@ -47,8 +47,8 @@ private IEnumerator UpdateStats() bytesSentPerSecond = serverStats != null ? serverStats.BytesSent - clientStats.BytesReceived : clientStats.BytesReceived; packetsReceivedPerSecond = serverStats != null ? serverStats.PacketsReceived - clientStats.PacketsSent : clientStats.PacketsReceived; packetsSentPerSecond = serverStats != null ? serverStats.PacketsSent - clientStats.PacketsReceived : clientStats.PacketsReceived; - packetsWrittenByType = serverStats?.PacketsWrittenByType; - bytesWrittenByType = serverStats?.BytesWrittenByType; + //packetsWrittenByType = serverStats?.PacketsWrittenByType; //disabled for steamnetworking + //bytesWrittenByType = serverStats?.BytesWrittenByType; //disabled for steamnetworking serverStats?.Reset(); clientStats?.Reset(); yield return new WaitForSecondsRealtime(1); @@ -66,24 +66,24 @@ private void OnGUI() // Write clean IMGUI code challenge (impossible) private void DrawStats(int windowId) { - int statsListSize = Multiplayer.Settings.StatsListSize; + //int statsListSize = Multiplayer.Settings.StatsListSize; GUILayout.Label($"Send: {bytesSentPerSecond.Bytes().ToFullWords()}/s ({packetsSentPerSecond:N0} packets/s)"); GUILayout.Label($"Receive: {bytesReceivedPerSecond.Bytes().ToFullWords()}/s ({packetsReceivedPerSecond:N0} packets/s)"); - if (serverStats == null) + if (serverStats == null) return; - GUILayout.Space(5); - GUILayout.Label($"Top {statsListSize} sent packets"); - foreach (KeyValuePair kvp in packetsWrittenByType.OrderByDescending(k => k.Value).Take(statsListSize)) - GUILayout.Label($" • {kvp.Key}: {kvp.Value}/s"); - if (packetsWrittenByType.Count < statsListSize) - for (int i = 0; i < statsListSize - packetsWrittenByType.Count; i++) - GUILayout.Label(string.Empty); + //GUILayout.Space(5); + //GUILayout.Label($"Top {statsListSize} sent packets"); + //foreach (KeyValuePair kvp in packetsWrittenByType.OrderByDescending(k => k.Value).Take(statsListSize)) + // GUILayout.Label($" • {kvp.Key}: {kvp.Value}/s"); + //if (packetsWrittenByType.Count < statsListSize) + // for (int i = 0; i < statsListSize - packetsWrittenByType.Count; i++) + // GUILayout.Label(string.Empty); - GUILayout.Label($"Top {statsListSize} sent packets by size"); - foreach (KeyValuePair kvp in bytesWrittenByType.OrderByDescending(k => k.Value).Take(statsListSize)) - GUILayout.Label($" • {kvp.Key}: {kvp.Value.Bytes().ToFullWords()}/s"); + //GUILayout.Label($"Top {statsListSize} sent packets by size"); + //foreach (KeyValuePair kvp in bytesWrittenByType.OrderByDescending(k => k.Value).Take(statsListSize)) + // GUILayout.Label($" • {kvp.Key}: {kvp.Value.Bytes().ToFullWords()}/s"); } } diff --git a/Multiplayer/Components/Networking/PlayerListGUI.cs b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs similarity index 69% rename from Multiplayer/Components/Networking/PlayerListGUI.cs rename to Multiplayer/Components/Networking/UI/PlayerListGUI.cs index 8a516fa..0e344bc 100644 --- a/Multiplayer/Components/Networking/PlayerListGUI.cs +++ b/Multiplayer/Components/Networking/UI/PlayerListGUI.cs @@ -2,15 +2,22 @@ using Multiplayer.Components.Networking.Player; using UnityEngine; -namespace Multiplayer.Components.Networking; +namespace Multiplayer.Components.Networking.UI; public class PlayerListGUI : MonoBehaviour { private bool showPlayerList; + private string localPlayerUsername; public void RegisterListeners() { ScreenspaceMouse.Instance.ValueChanged += OnToggle; + localPlayerUsername = Multiplayer.Settings.GetUserName(); + } + public void UnRegisterListeners() + { + ScreenspaceMouse.Instance.ValueChanged -= OnToggle; + OnToggle(false); } private void OnToggle(bool status) @@ -26,19 +33,19 @@ private void OnGUI() GUILayout.Window(157031520, new Rect(Screen.width / 2.0f - 125, 25, 250, 0), DrawPlayerList, Locale.PLAYER_LIST__TITLE); } - private static void DrawPlayerList(int windowId) + private void DrawPlayerList(int windowId) { foreach (string player in GetPlayerList()) GUILayout.Label(player); } // todo: cache this? - private static IEnumerable GetPlayerList() + private IEnumerable GetPlayerList() { if (!NetworkLifecycle.Instance.IsClientRunning) return new[] { "Not in game" }; - IReadOnlyCollection players = NetworkLifecycle.Instance.Client.PlayerManager.Players; + IReadOnlyCollection players = NetworkLifecycle.Instance.Client.ClientPlayerManager.Players; string[] playerList = new string[players.Count + 1]; int i = 0; foreach (NetworkedPlayer player in players) @@ -48,7 +55,7 @@ private static IEnumerable GetPlayerList() } // The Player of the Client is not in the PlayerManager, so we need to add it separately - playerList[playerList.Length - 1] = $"{Multiplayer.Settings.Username} ({NetworkLifecycle.Instance.Client.Ping.ToString()}ms)"; + playerList[playerList.Length - 1] = $"{localPlayerUsername} ({NetworkLifecycle.Instance.Client.Ping}ms)"; return playerList; } } diff --git a/Multiplayer/Components/Networking/World/NetworkedItem.cs b/Multiplayer/Components/Networking/World/NetworkedItem.cs new file mode 100644 index 0000000..295e745 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedItem.cs @@ -0,0 +1,652 @@ +using DV.CabControls; +using DV.Interaction; +using DV.InventorySystem; +using DV.Items; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using Multiplayer.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public enum ItemState : byte +{ + Dropped, //belongs to the world + Thrown, //was thrown by player + InHand, //held by player + InInventory, //in player's inventory + Attached //attached to another object (e.g. EOT Lanterns) +} + +public class NetworkedItem : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary itemBaseToNetworkedItem = new(); + + public static List GetAll() + { + return itemBaseToNetworkedItem.Values.Where(val => val.Item != null).ToList(); + } + public static bool Get(ushort netId, out NetworkedItem obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedItem)rawObj; + return b; + } + + public static bool TryGet(ushort netId, out NetworkedItem obj) + { + bool b = TryGet(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedItem)rawObj; + return b; + } + + public static bool GetItem(ushort netId, out ItemBase obj) + { + bool b = Get(netId, out NetworkedItem networkedItem); + obj = b ? networkedItem.Item : null; + return b; + } + + public static bool TryGetNetworkedItem(ItemBase item, out NetworkedItem networkedItem) + { + return itemBaseToNetworkedItem.TryGetValue(item, out networkedItem); + } + #endregion + + private const float PositionThreshold = 0.1f; + private const float RotationThreshold = 0.1f; + + public ItemBase Item { get; private set; } + private GrabHandlerItem grabHandler; + private SnappableItem snappableItem; + private Component trackedItem; + private List trackedValues = new List(); + public bool UsefulItem { get; private set; } = false; + public Type TrackedItemType { get; private set; } + public uint LastDirtyTick { get; private set; } + private bool initialised; + private bool registrationComplete = false; + private Queue pendingSnapshots = new Queue(); + + //Track dirty states + private bool createdDirty = true; //if set, we created this item dirty and have not sent an update + private ItemState lastState; + private bool stateDirty; + private bool wasThrown; + + private Vector3 thrownPosition; + private Quaternion thrownRotation; + private Vector3 throwDirection; + + //Handle ownership + public sbyte OwnerId { get; private set; } = -1; // 0 means no owner + + //public void SetOwner(ushort playerId) + //{ + // if (OwnerId != playerId) + // { + // if (OwnerId != 0) + // { + // NetworkedItemManager.Instance.RemoveItemFromPlayerInventory(this); + // } + // OwnerId = playerId; + // if (playerId != 0) + // { + // NetworkedItemManager.Instance.AddItemToPlayerInventory(playerId, this); + // } + // } + //} + + protected override bool IsIdServerAuthoritative => true; + + protected override void Awake() + { + base.Awake(); + //Multiplayer.LogDebug(() => $"NetworkedItem.Awake() {name}"); + NetworkedItemManager.Instance.CheckInstance(); //Ensure the NetworkedItemManager is initialised + + Register(); + } + + protected void Start() + { + if (!initialised) + Register(); + + // Mark registration as complete for items that don't need tracked values + if (!registrationComplete && !UsefulItem) + registrationComplete = true; + } + + public T GetTrackedItem() where T : Component + { + return UsefulItem ? trackedItem as T : null; + } + + public void Initialize(T item, ushort netId = 0, bool createDirty = true) where T : Component + { + //Multiplayer.LogDebug(() => $"NetworkedItem.Initialize<{typeof(T)}>(netId: {netId}, name: {name}, createDirty: {createdDirty})"); + + if (netId != 0) + NetId = netId; + + trackedItem = item; + TrackedItemType = typeof(T); + UsefulItem = true; + + createdDirty = createDirty; + + if(Item == null) + Register(); + + } + + private bool Register() + { + if (initialised) + return false; + + try + { + + if (!TryGetComponent(out ItemBase itemBase)) + { + Multiplayer.LogError($"NetworkedItem.Register() Unable to find ItemBase for {name}"); + return false; + } + + Item = itemBase; + itemBaseToNetworkedItem[Item] = this; + + Item.Grabbed += OnGrabbed; + Item.Ungrabbed += OnUngrabbed; + + //Find special interaction components + TryGetComponent(out grabHandler); + TryGetComponent(out snappableItem); + + lastState = GetItemState(); + stateDirty = false; + + initialised = true; + return true; + } + catch (Exception ex) + { + Multiplayer.LogError($"NetworkedItem.Register() Unable to find ItemBase for {name}\r\n{ex.Message}"); + return false; + } + } + + private void OnUngrabbed(ControlImplBase obj) + { + //Multiplayer.LogDebug(() => $"NetworkedItem.OnUngrabbed() NetID: {NetId}, {name}"); + stateDirty = true; + } + + private void OnGrabbed(ControlImplBase obj) + { + //Multiplayer.LogDebug(() => $"NetworkedItem.OnGrabbed() NetID: {NetId}, {name}"); + stateDirty = true; + } + + public void OnThrow(Vector3 direction) + { + //block a received throw from + if(wasThrown) + { + wasThrown = false; + return; + } + + throwDirection = direction; + thrownPosition = Item.transform.position - WorldMover.currentMove; + thrownRotation = Item.transform.rotation; + + //Multiplayer.LogDebug(() => $"NetworkedItem.OnThrow() netId: {NetId}, Name: {name}, Raw Position: {Item.transform.position}, Position: {thrownPosition}, Rotation: {thrownRotation}, Direction: {throwDirection}"); + + wasThrown = true; + stateDirty = true; + } + + + #region Item Value Tracking + public void RegisterTrackedValue(string key, Func valueGetter, Action valueSetter, Func thresholdComparer = null, bool serverAuthoritative = false) + { + //Multiplayer.LogDebug(() => $"NetworkedItem.RegisterTrackedValue(\"{key}\", {valueGetter != null}, {valueSetter != null}, {thresholdComparer != null}, {serverAuthoritative}) itemNetId {NetId}, item name: {name}"); + trackedValues.Add(new TrackedValue(key, valueGetter, valueSetter, thresholdComparer, serverAuthoritative)); + } + + public void FinaliseTrackedValues() + { + //Multiplayer.LogDebug(() => $"NetworkedItem.FinaliseTrackedValues() itemNetId: {NetId}, item name: {name}"); + + while (pendingSnapshots.Count > 0) + { + Multiplayer.LogDebug(() => $"NetworkedItem.FinaliseTrackedValues() itemNetId: {NetId}, item name: {name}. Dequeuing"); + ApplySnapshot(pendingSnapshots.Dequeue()); + } + + registrationComplete = true; + + } + + private bool HasDirtyValues() + { + //clients should only send values that are not server authoritative + if(!NetworkLifecycle.Instance.IsHost()) + return trackedValues.Any(tv => ((dynamic)tv).IsDirty && !((dynamic)tv).ServerAuthoritative); + else + return trackedValues.Any(tv => ((dynamic)tv).IsDirty); + } + + private Dictionary GetDirtyStateData() + { + var dirtyData = new Dictionary(); + foreach (var trackedValue in trackedValues) + { + if (((dynamic)trackedValue).IsDirty) + { + dirtyData[((dynamic)trackedValue).Key] = ((dynamic)trackedValue).GetValueAsObject(); + } + } + return dirtyData; + } + private Dictionary GetAllStateData() + { + var data = new Dictionary(); + foreach (var trackedValue in trackedValues) + { + data[((dynamic)trackedValue).Key] = ((dynamic)trackedValue).GetValueAsObject(); + } + return data; + } + + private void MarkValuesClean() + { + foreach (var trackedValue in trackedValues) + { + ((dynamic)trackedValue).MarkClean(); + } + } + + #endregion + + public ItemUpdateData GetSnapshot() + { + ItemUpdateData snapshot; + ItemUpdateData.ItemUpdateType updateType = ItemUpdateData.ItemUpdateType.None; + + bool hasDirtyVals = HasDirtyValues(); + + if (Item == null && Register() == false) + return null; + + if (!stateDirty && !hasDirtyVals) + return null; + + ItemState currentState = GetItemState(); + + if (!createdDirty) + { + if(lastState != currentState) + updateType |= ItemUpdateData.ItemUpdateType.ItemState; + + if (hasDirtyVals) + { + Multiplayer.LogDebug(GetDirtyValuesDebugString); + updateType |= ItemUpdateData.ItemUpdateType.ObjectState; + } + } + else + { + updateType = ItemUpdateData.ItemUpdateType.Create; + } + + //no changes this snapshot + if (updateType == ItemUpdateData.ItemUpdateType.None) + return null; + + lastState = currentState; + LastDirtyTick = NetworkLifecycle.Instance.Tick; + snapshot = CreateUpdateData(updateType); + + createdDirty = false; + stateDirty = false; + wasThrown = false; + + MarkValuesClean(); + + return snapshot; + } + + public void ReceiveSnapshot(ItemUpdateData snapshot) + { + if(snapshot == null || snapshot.UpdateType == ItemUpdateData.ItemUpdateType.None) + return; + + if (!registrationComplete) + { + Multiplayer.Log($"NetworkedItem.ReceiveSnapshot() netId: {snapshot?.ItemNetId}, ItemUpdateType: {snapshot?.UpdateType}. Queuing"); + pendingSnapshots.Enqueue(snapshot); + return; + } + + ApplySnapshot(snapshot); + } + + private void ApplySnapshot(ItemUpdateData snapshot) + { + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ItemState) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.FullSync) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create)) + { + Multiplayer.Log($"NetworkedItem.ApplySnapshot() netId: {snapshot?.ItemNetId}, ItemUpdateType: {snapshot?.UpdateType}, ItemState: {snapshot?.ItemState}, Active state: {gameObject.activeInHierarchy}"); + + switch (snapshot.ItemState) + { + case ItemState.Dropped: + case ItemState.Thrown: + HandleDroppedOrThrownState(snapshot); + break; + + case ItemState.InHand: + case ItemState.InInventory: + HandleInventoryOrHandState(snapshot); + break; + + case ItemState.Attached: + HandleAttachedState(snapshot); + break; + + default: + throw new Exception($"NetworkedItem.ApplySnapshot() Item state not implemented: {snapshot?.ItemState}"); + + } + } + + Multiplayer.Log($"NetworkedItem.ApplySnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType} About to process states"); + + if (snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.Create) || snapshot.UpdateType.HasFlag(ItemUpdateData.ItemUpdateType.ObjectState)) + { + Multiplayer.Log($"NetworkedItem.ApplySnapshot() netID: {snapshot?.ItemNetId}, States: {snapshot?.States?.Count}"); + + if (trackedItem != null && snapshot.States != null) + { + ApplyTrackedValues(snapshot.States); + } + } + + Multiplayer.Log($"NetworkedItem.ApplySnapshot() netID: {snapshot?.ItemNetId}, ItemUpdateType {snapshot?.UpdateType} states processed"); + + //mark values as clean + createdDirty = false; + stateDirty = false; + + MarkValuesClean(); + return; + } + + public ItemUpdateData CreateUpdateData(ItemUpdateData.ItemUpdateType updateType) + { + //Multiplayer.LogDebug(() => $"NetworkedItem.CreateUpdateData({updateType}) NetId: {NetId}, name: {name}"); + + Vector3 position; + Quaternion rotation; + Dictionary states; + ushort carId =0; + bool frontCoupler = true; + + if (wasThrown) + { + position = thrownPosition; + rotation = thrownRotation; + } + else + { + position = transform.position - WorldMover.currentMove; + rotation = transform.rotation; + } + + if (updateType.HasFlag(ItemUpdateData.ItemUpdateType.Create) || updateType.HasFlag(ItemUpdateData.ItemUpdateType.FullSync)) + { + states = GetAllStateData(); + } + else + { + states = GetDirtyStateData(); + } + + if(lastState == ItemState.Attached) + { + ItemSnapPointCoupler itemSnapPointCoupler = snappableItem.SnappedTo as ItemSnapPointCoupler; + + if (itemSnapPointCoupler != null) + { + carId = itemSnapPointCoupler.Car.GetNetId(); + frontCoupler = itemSnapPointCoupler.IsFront; + } + } + + var updateData = new ItemUpdateData + { + UpdateType = updateType, + ItemNetId = NetId, + PrefabName = Item.InventorySpecs.ItemPrefabName, + ItemState = lastState, + ItemPosition = position, + ItemRotation = rotation, + ThrowDirection = throwDirection, + CarNetId = carId, + AttachedFront = frontCoupler, + States = states, + }; + + return updateData; + } + + private ItemState GetItemState() + { + //Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, Parent: {Item.transform.parent} WorldMover: {WorldMover.OriginShiftParent}, wasThrown: {wasThrown}, isGrabbed: {Item.IsGrabbed()} Inventory.Contains(): {Inventory.Instance.Contains(this.gameObject, false)} Storage.Contains: {StorageController.Instance.StorageInventory.ContainsItem(Item)}"); + + + if (Item.transform.parent == WorldMover.OriginShiftParent && !wasThrown) + { + Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, Parent: {Item.transform.parent} WorldMover: {WorldMover.OriginShiftParent}, wasThrown: {wasThrown}"); + return ItemState.Dropped; + } + + if (wasThrown) + { + Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, Parent: {Item.transform.parent} WorldMover: {WorldMover.OriginShiftParent}, wasThrown: {wasThrown}"); + return ItemState.Thrown; + } + + if (Item.IsGrabbed()) + return ItemState.InHand; + + if (Inventory.Instance.Contains(this.gameObject, false)) + return ItemState.InInventory; + + if(snappableItem != null && snappableItem.IsSnapped) + { + Multiplayer.LogDebug(() => $"GetItemState() NetId: {NetId}, {name}, snapped! {this.transform.parent}"); + return ItemState.Attached; + } + + //do we need a condition to check if it's attached to something else (last attach vs current attach)? + return ItemState.Dropped; + + } + + private void ApplyTrackedValues(Dictionary newValues) + { + Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Null checks"); + + if (newValues == null || newValues.Count == 0) + return; + + + Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Registration complete: {registrationComplete}"); + + foreach (var newValue in newValues) + { + var trackedValue = trackedValues.Find(tv => ((dynamic)tv).Key == newValue.Key); + if (trackedValue != null) + { + if (!NetworkLifecycle.Instance.IsHost() || !((dynamic)trackedValue).ServerAuthoritative) + { + try + { + ((dynamic)trackedValue).SetValueFromObject(newValue.Value); + Multiplayer.LogDebug(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}, Updated tracked value: {newValue.Key}, value: {newValue.Value} "); + } + catch (Exception ex) + { + Multiplayer.LogError($"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Error updating tracked value {newValue.Key}: {ex.Message}"); + } + } + else + { + Multiplayer.LogWarning(() => $"NetworkedItem.ApplyTrackedValues() itemNetId: {NetId}, item name: {name}. Skipped server-authoritative value update from client: {newValue.Key}"); + } + } + else + { + Multiplayer.LogWarning($"Tracked value not found: {newValue.Key}\r\n {String.Join(", ", trackedValues.Select(val => ((dynamic)val).Key))}"); + } + } + } + + #region Item State Update Handlers + + private void HandleDroppedOrThrownState(ItemUpdateData snapshot) + { + //resolve attachment + if (Item.IsSnapped) + { + Item.SnappableItem.SnappedTo.UnsnapItem(false); + } + + //resolve ownership + if (NetworkLifecycle.Instance.IsHost()) + if (NetworkLifecycle.Instance.Server.TryGetServerPlayer(snapshot.Player, out ServerPlayer player) && player.OwnsItem(NetId)) + player.RemoveOwnedItem(NetId); + + //activate and relocate item + gameObject.SetActive(true); + transform.position = snapshot.ItemPosition + WorldMover.currentMove; + transform.rotation = snapshot.ItemRotation; + OwnerId = 0; + + //handle throwing of the item + if (snapshot.ItemState == ItemState.Thrown) + { + Multiplayer.LogDebug(()=>$"NetworkedItem.HandleDroppedOrThrownState() ItemNetId: {snapshot?.ItemNetId} Thrown. Position: {transform.position}, Direction: {snapshot?.ThrowDirection}"); + + wasThrown = true; + grabHandler?.Throw(snapshot.ThrowDirection); + } + else + { + Multiplayer.LogDebug(() => $"NetworkedItem.HandleDroppedOrThrownState() ItemNetId: {snapshot?.ItemNetId} Dropped. Position: {transform.position}"); + } + } + + private void HandleAttachedState(ItemUpdateData snapshot) + { + //resovle ownership + if (NetworkLifecycle.Instance.IsHost()) + if (NetworkLifecycle.Instance.Server.TryGetServerPlayer(snapshot.Player, out ServerPlayer player) && player.OwnsItem(NetId)) + player.RemoveOwnedItem(NetId); + + //handle attaching the item + gameObject.SetActive(true); + Multiplayer.LogDebug(() => $"NetworkedItem.HandleAttachedState() ItemNetId: {snapshot?.ItemNetId} attempting attachment to car {snapshot.CarNetId}, at the front {snapshot.AttachedFront}"); + + if (!NetworkedTrainCar.GetTrainCar(snapshot.CarNetId, out TrainCar trainCar)) + { + Multiplayer.LogWarning($"NetworkedItem.HandleAttachedState() CarNetId: {snapshot?.CarNetId} not found for ItemNetId: {snapshot?.ItemNetId}"); + return; + } + + //Try to find the coupler snap point for the car and correct end to snap to + var snapPoint = trainCar?.physicsLod?.GetCouplerSnapPoints() + .FirstOrDefault(sp => sp.IsFront == snapshot.AttachedFront); + + if (snapPoint == null) + { + Multiplayer.LogWarning($"NetworkedItem.HandleAttachedState() ItemNetId: {snapshot?.ItemNetId}. No valid snap point found for car {snapshot.CarNetId}"); + return; + } + + //Attempt attachment to car + Item.ItemRigidbody.isKinematic = false; + if (!snapPoint.SnapItem(Item, false)) + { + Multiplayer.LogWarning($"NetworkedItem.HandleAttachedState() Attachment failed for item {snapshot?.ItemNetId} to car {snapshot.CarNetId}"); + } + } + + private void HandleInventoryOrHandState(ItemUpdateData snapshot) + { + if (Item.IsSnapped) + { + Item.SnappableItem.SnappedTo.UnsnapItem(false); + } + + if (NetworkLifecycle.Instance.IsHost()) + if(NetworkLifecycle.Instance.Server.TryGetServerPlayer(snapshot.Player, out ServerPlayer player) && !player.OwnsItem(NetId)) + player.AddOwnedItem(NetId); + + //todo add to player model's hand + this.gameObject.SetActive(false); + } + #endregion + + protected override void OnDestroy() + { + if (UnloadWatcher.isQuitting || UnloadWatcher.isUnloading) + return; + + if (NetworkLifecycle.Instance.IsHost()) + { + NetworkedItemManager.Instance.AddDirtyItemSnapshot(this, CreateUpdateData(ItemUpdateData.ItemUpdateType.Destroy)); + } + + if (Item != null) + { + Item.Grabbed -= OnGrabbed; + Item.Ungrabbed -= OnUngrabbed; + itemBaseToNetworkedItem.Remove(Item); + } + else + { + Multiplayer.LogWarning($"NetworkedItem.OnDestroy({name}, {NetId}) Item is null!"); + } + + base.OnDestroy(); + + } + + public string GetDirtyValuesDebugString() + { + var dirtyValues = trackedValues.Where(tv => ((dynamic)tv).IsDirty).ToList(); + if (dirtyValues.Count == 0) + { + return "No dirty values"; + } + + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"Dirty values for NetworkedItem: {name}, NetId: {NetId}:"); + foreach (var value in dirtyValues) + { + sb.AppendLine(((dynamic)value).GetDebugString()); + } + return sb.ToString(); + } +} diff --git a/Multiplayer/Components/Networking/World/NetworkedItemManager.cs b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs new file mode 100644 index 0000000..6e22d15 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedItemManager.cs @@ -0,0 +1,568 @@ +using System.Collections.Generic; +using System.Linq; +using DV.Utils; +using UnityEngine; +using JetBrains.Annotations; +using Multiplayer.Networking.Data; +using Multiplayer.Components.Networking.World; +using System; +using Multiplayer.Utils; +using DV; +using DV.Interaction; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedItemManager : SingletonBehaviour +{ + /* + * Server + */ + + //Culling distance for items + public const float MAX_DISTANCE_TO_ITEM = 100f; + public const float MAX_DISTANCE_TO_ITEM_SQR = MAX_DISTANCE_TO_ITEM * MAX_DISTANCE_TO_ITEM; + public const float NEARBY_REMOVAL_DELAY = 3f; // 3 seconds delay + public const float REACH_DISTANCE_BUFFER = 0.5f; + public float MAX_REACH_DISTANCE = 4f + REACH_DISTANCE_BUFFER; //from the game, but we should try to look up the value + + //caches for item snapshots + private List DestroyedItems = new List(); + + //Item ownership + //private Dictionary playerInventories = new Dictionary(); + //private Dictionary itemToPlayerMap = new Dictionary(); + + + /* + * Client + */ + + //cache for client-sided items & spawns + private Dictionary> CachedItems = new Dictionary>(); //Client cached items + private Dictionary ItemPrefabs = new Dictionary(); //Item prefabs + private bool ClientInitialised = false; + + + /* + * Common + */ + private Queue> ReceivedSnapshots = new Queue>(); + + + protected override void Awake() + { + base.Awake(); + if (!NetworkLifecycle.Instance.IsHost()) + return; + + //B99 temporary patch NetworkLifecycle.Instance.Server.PlayerDisconnect += PlayerDisconnected; + + try + { + MAX_REACH_DISTANCE = GrabberRaycasterDV.RAYCAST_MAX_DIST + REACH_DISTANCE_BUFFER; + } + catch (Exception ex) + { + NetworkLifecycle.Instance.Server.LogWarning($"NatworkedItemManager.Awake() Failed to find GrabberRaycasterDV\r\n{ex.Message}"); + } + } + + private void PlayerDisconnected(uint netID) + { + throw new NotImplementedException(); + } + + protected void Start() + { + NetworkLifecycle.Instance.OnTick += Common_OnTick; + + BuildPrefabLookup(); + } + + protected override void OnDestroy() + { + base.OnDestroy(); + if (UnloadWatcher.isQuitting) + return; + + NetworkLifecycle.Instance.OnTick -= Common_OnTick; + } + + public void AddDirtyItemSnapshot(NetworkedItem netItem, ItemUpdateData snapshot) + { + DestroyedItems.Add(snapshot); + + foreach(var player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if(player.KnownItems.ContainsKey(netItem)) + player.KnownItems.Remove(netItem); + + if(player.NearbyItems.ContainsKey(netItem)) + player.NearbyItems.Remove(netItem); + } + } + + public void ReceiveSnapshots(List snapshots, ServerPlayer sender) + { + if (snapshots == null) + return; + + foreach (var snapshot in snapshots) + { + ReceivedSnapshots.Enqueue(new (snapshot, sender)); + } + + Multiplayer.LogDebug(() => $"NetworkItemManager.ReceiveSnapshots() count: {ReceivedSnapshots.Count}, from: "); + } + + #region Common + + private void Common_OnTick(uint tick) + { + ProcessReceived(); + + if (NetworkLifecycle.Instance.IsHost()) + { + UpdatePlayerItemLists(); + ProcessChanged(tick); + } + else + { + ProcessClientChanges(tick); + } + } + + private void ProcessReceived() + { + while (ReceivedSnapshots.Count > 0) + { + var snapshotInfo = ReceivedSnapshots.Dequeue(); + ItemUpdateData snapshot = snapshotInfo.Item1; + try + { + //Multiplayer.LogDebug(() => $"ProcessReceived: {snapshot.UpdateType}"); + + if (snapshot == null || snapshot.UpdateType == ItemUpdateData.ItemUpdateType.None) + { + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Invalid Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + continue; + } + + if (NetworkLifecycle.Instance.IsHost()) + { + ProcessReceivedAsHost(snapshot, snapshotInfo.Item2); + } + else + { + ProcessReceivedAsClient(snapshot); + } + } + catch (Exception ex) + { + Multiplayer.LogError($"NetworkedItemManager.ProcessReceived() Error! {ex.Message}\r\n{ex.StackTrace}"); + } + } + } + + #endregion + + #region Server + + private void UpdatePlayerItemLists() + { + float currentTime = Time.time; + + List allItems = NetworkedItem.GetAll(); + + foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if (!player.IsLoaded) + continue; + + foreach (var item in allItems) + { + if (item == null) + { + NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Null item found in allItems!"); + continue; + } + + float sqrDistance = (player.WorldPosition - item.transform.position).sqrMagnitude; + + if (sqrDistance <= MAX_DISTANCE_TO_ITEM_SQR) + { + //NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Adding for player: {player?.Username}, Nearby Item: {item?.NetId}, {item?.name}"); + player.NearbyItems[item] = currentTime; + } + } + + // Remove items that are no longer nearby + for (int i = 0; i < player.NearbyItems.Count; i++) + { + var kvp = player.NearbyItems.ElementAt(i); + + if (currentTime - kvp.Value > NEARBY_REMOVAL_DELAY) + { + //NetworkLifecycle.Instance.Server.LogDebug(() => $"UpdatePlayerItemLists() Removing for player: {player?.Username}, Nearby Item: {kvp.Key?.NetId}, {kvp.Key?.name}"); + player.NearbyItems.Remove(kvp.Key); + } + } + } + } + + private void ProcessChanged(uint tick) + { + List dirtyItems = new List(); + float timeStamp = Time.time; + + foreach (var item in NetworkedItem.GetAll()) + { + ItemUpdateData snapshot = item.GetSnapshot(); + if (snapshot != null) + dirtyItems.Add(snapshot); + } + + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) DirtyItems: {dirtyItems.Count}"); + + foreach (var player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if (!player.IsLoaded) + continue; + + List playerUpdates = new List(); + + // Process nearby items + foreach (var nearbyItem in player.NearbyItems.Keys) + { + if (!player.KnownItems.ContainsKey(nearbyItem)) + { + // This is a new item for the player + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) New item for: {player.Username}, itemNetID{nearbyItem.NetId}"); + + ItemUpdateData snapshot = nearbyItem.CreateUpdateData(ItemUpdateData.ItemUpdateType.Create); + player.KnownItems[nearbyItem] = tick; + + //prevent propagation of creates for special items + if(!DoNotCreateItem(nearbyItem.GetType())) + playerUpdates.Add(snapshot); + } + else + { + // Check if this item is in the dirty items list + var dirtyUpdate = dirtyItems.FirstOrDefault(di => di.ItemNetId == nearbyItem.NetId); + + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) Item exists for: {player.Username}, {dirtyUpdate != null}"); + + if (dirtyUpdate == null) + { + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) Item exists for: {player.Username}, LastDirtyTick: {player.KnownItems[nearbyItem] < nearbyItem.LastDirtyTick}"); + if (player.KnownItems[nearbyItem] < nearbyItem.LastDirtyTick) + { + dirtyUpdate = nearbyItem.CreateUpdateData(ItemUpdateData.ItemUpdateType.FullSync); + } + } + + if (dirtyUpdate != null) + { + Multiplayer.LogDebug(() => $"ProcessChanged({tick}) Update Type: {dirtyUpdate.UpdateType}, Item State: {dirtyUpdate.ItemState}"); + playerUpdates.Add(dirtyUpdate); + player.KnownItems[nearbyItem] = tick; + } + } + } + + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) Adding {DestroyedItems.Count()} DestroyedItems for: {player.Username}"); + + playerUpdates.AddRange(DestroyedItems); + + if (playerUpdates.Count > 0) + { + //NetworkLifecycle.Instance.Server.LogDebug(() => $"ProcessChanged({tick}) Sending {playerUpdates.Count()} to player: {player.Username}"); + NetworkLifecycle.Instance.Server.SendItemsChangePacket(playerUpdates, player); + } + } + + DestroyedItems.Clear(); + } + + private void ProcessReceivedAsHost(ItemUpdateData snapshot, ServerPlayer player) + { + if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) + { + NetworkLifecycle.Instance.Server.LogError($"NetworkedItemManager.ProcessReceivedAsHost() Host received Create snapshot! ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + return; + } + + if (NetworkedItem.TryGet(snapshot.ItemNetId, out NetworkedItem netItem)) + { + if (ValidatePlayerAction(snapshot, player)) //Ensure the player can do this + { + NetworkLifecycle.Instance.Server.LogWarning($"NetworkedItemManager.ProcessReceivedAsHost() ItemNetId: {snapshot.ItemNetId}, snapshot type: {snapshot.UpdateType}"); + netItem.ReceiveSnapshot(snapshot); + } + else + { + NetworkLifecycle.Instance.Server.LogWarning($"NetworkedItemManager.ProcessReceivedAsHost() Player action validation failed for ItemNetId: {snapshot.ItemNetId}"); + } + } + else + { + NetworkLifecycle.Instance.Server.LogError($"NetworkedItemManager.ProcessReceivedAsHost() NetworkedItem not found! Update Type: {snapshot.UpdateType}, ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + } + } + + private bool ValidatePlayerAction(ItemUpdateData snapshot, ServerPlayer player) + { + return true; + // Must have valid item + if (!NetworkedItem.TryGet(snapshot.ItemNetId, out NetworkedItem networkedItem)) + return false; + + Multiplayer.LogDebug(() => $"ValidatePlayerAction() ItemId: {snapshot.ItemNetId}, name: {networkedItem.name} Update Type: {snapshot.UpdateType}, Item State: {snapshot.ItemState}, Player: {player.Username}"); + + switch (snapshot.ItemState) + { + case ItemState.InHand: + case ItemState.InInventory: + // Check if someone else owns it + GetItemOwner(snapshot.ItemNetId, out ServerPlayer currentOwner); + Multiplayer.LogDebug(() => $"ValidatePlayerAction() ItemId: {snapshot.ItemNetId}, name: {networkedItem.name} Update Type: {snapshot.UpdateType}, Item State: {snapshot.ItemState}, Player: {player?.Username}, Current Owner: {currentOwner?.Username}"); + + if (currentOwner != null && currentOwner != player) + return false; + + // Check pickup distance + float distance = Vector3.Distance(player.WorldPosition, networkedItem.transform.position); + if (distance > MAX_REACH_DISTANCE) + return false; + + Multiplayer.LogDebug(() => $"ValidatePlayerAction() ItemId: {snapshot.ItemNetId}, name: {networkedItem.name} Update Type: {snapshot.UpdateType}, Item State: {snapshot.ItemState}, Player: {player.Username}, Distance check: {distance}"); + break; + + case ItemState.Dropped: + case ItemState.Thrown: + case ItemState.Attached: //needs additional checks for distance to coupler + // Only owner can drop/throw + if (!player.OwnsItem(snapshot.ItemNetId)) + return false; + break; + } + + return true; + } + + private bool GetItemOwner(ushort itemNetId, out ServerPlayer owner) + { + owner = NetworkLifecycle.Instance.Server.ServerPlayers.FirstOrDefault(p => p.OwnsItem(itemNetId)); + return owner != null; + } + #endregion + + #region Client + + private void ProcessClientChanges(uint tick) + { + List changedItems = new List(); + + if(!ClientInitialised) + return; + + foreach (var item in NetworkedItem.GetAll()) + { + ItemUpdateData snapshot = item.GetSnapshot(); + if (snapshot != null) + { + changedItems.Add(snapshot); + } + } + + if (changedItems.Count > 0) + { + NetworkLifecycle.Instance.Client.SendItemsChangePacket(changedItems); + } + } + + private void ProcessReceivedAsClient(ItemUpdateData snapshot) + { + NetworkedItem.TryGet(snapshot.ItemNetId, out NetworkedItem netItem); + + NetworkLifecycle.Instance.Client.LogDebug(() => $"NetworkedItemManager.ProcessReceivedAsClient() Update Type: {snapshot?.UpdateType}, ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Create) + { + //if the item already exists we need to remove it + if (netItem != null) + SendToCache(netItem); + + CreateItem(snapshot); + } + else if (snapshot.UpdateType == ItemUpdateData.ItemUpdateType.Destroy) + { + SendToCache(netItem); + } + else if (netItem != null) + { + netItem.ReceiveSnapshot(snapshot); + } + else + { + NetworkLifecycle.Instance.Client.LogError($"NetworkedItemManager.ProcessReceivedAsClient() NetworkedItem not found on client! Update Type: {snapshot.UpdateType}, ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + } + } + #endregion + + #region Item Cache And Management + private void CreateItem(ItemUpdateData snapshot) + { + if(snapshot == null || snapshot.ItemNetId == 0) + { + Multiplayer.LogError($"NetworkedItemManager.CreateItem() Invalid snapshot! ItemNetId: {snapshot?.ItemNetId}, prefabName: {snapshot?.PrefabName}"); + return; + } + + NetworkedItem newItem = GetFromCache(snapshot.PrefabName); + + if(newItem == null) + { + //GameObject prefabObj = Resources.Load(snapshot.PrefabName) as GameObject; + + if (!ItemPrefabs.TryGetValue(snapshot.PrefabName, out InventoryItemSpec spec)) + { + Multiplayer.LogError($"NetworkedItemManager.CreateItem() Unable to load prefab for ItemNetId: {snapshot.ItemNetId}, prefabName: {snapshot.PrefabName}"); + return; + } + + //create a new item + GameObject gameObject = Instantiate(spec.gameObject, snapshot.ItemPosition + WorldMover.currentMove, snapshot.ItemRotation); + + //Make sure we have a NetworkedItem + newItem = gameObject.GetOrAddComponent(); + } + + newItem.gameObject.SetActive(true); + newItem.NetId = snapshot.ItemNetId; + + newItem.ReceiveSnapshot(snapshot); + } + + private void BuildPrefabLookup() + { + NetworkLifecycle.Instance.Client.LogDebug(() => $"BuildPrefabLookup()"); + + foreach (var item in Globals.G.Items.items) + { + if (!ItemPrefabs.ContainsKey(item.ItemPrefabName)) + { + ItemPrefabs[item.itemPrefabName] = item; + } + } + } + public void CacheWorldItems() + { + //B99 temporary patch + return; + if (NetworkLifecycle.Instance.IsHost()) + return; + + // Remove all spawned world items and place them into a cache for later use + var items = NetworkedItem.GetAll().ToList(); + foreach (var item in items) + { + try + { + if (item.Item != null && !item.Item.IsEssential() && !item.Item.IsGrabbed() && !StorageController.Instance.StorageInventory.ContainsItem(item.Item)) + { + SendToCache(item); + } + else + { + NetworkLifecycle.Instance.Client.LogDebug(() => $"CacheWorldItems() Not caching: {item.Item.InventorySpecs.previewPrefab} is in Inventory: {StorageController.Instance.StorageInventory.ContainsItem(item.Item)}"); + } + } + catch (Exception ex) + { + NetworkLifecycle.Instance.Client.LogDebug(() => $"Error Caching Spawned Item: {ex.Message}"); + } + } + + ClientInitialised = true; + } + + private NetworkedItem GetFromCache(string prefabName) + { + if (CachedItems.TryGetValue(prefabName, out var items) && items.Count > 0) + { + + var cachedItem = items[items.Count - 1]; + items.RemoveAt(items.Count - 1); + return cachedItem; + } + + return null; + } + + private void SendToCache(NetworkedItem netItem) + { + string prefabName = netItem?.Item?.InventorySpecs?.itemPrefabName; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"Caching Spawned Item: {prefabName ?? ""}"); + + netItem.gameObject.SetActive(false); + RespawnOnDrop respawn = netItem.Item.GetComponent(); + + Destroy(respawn); + + //NetworkLifecycle.Instance.Client.LogDebug(() => $"Caching Spawned Item: {prefabName ?? ""}: checkWhileDisabled {respawn.checkWhileDisabled}, ignoreDistanceFromSpawnPosition {respawn.ignoreDistanceFromSpawnPosition}, respawnOnDropThroughFloor {respawn.respawnOnDropThroughFloor}"); + + //respawn.checkWhileDisabled = false; + //respawn.ignoreDistanceFromSpawnPosition = true; + //respawn.respawnOnDropThroughFloor = false; + + if (SingletonBehaviour.Instance.StorageWorld.ContainsItem(netItem.Item)) + { + SingletonBehaviour.Instance.RemoveItemFromWorldStorage(netItem.Item); + } + + if (SingletonBehaviour.Instance.StorageInventory.ContainsItem(netItem.Item)) + { + SingletonBehaviour.Instance.RemoveItemFromStorageItemList(netItem.Item); + } + + if (SingletonBehaviour.Instance.StorageLostAndFound.ContainsItem(netItem.Item)) + { + SingletonBehaviour.Instance.RemoveItemFromStorageItemList(netItem.Item); + } + + netItem.Item.InventorySpecs.BelongsToPlayer = false; + netItem.NetId = 0; + + if (!CachedItems.ContainsKey(prefabName)) + { + CachedItems[prefabName] = new List(); + } + CachedItems[prefabName].Add(netItem); + } + + #endregion + + public bool DoNotCreateItem(Type itemType) + { + if ( + itemType == typeof(JobOverview) || + itemType == typeof(JobBooklet) || + itemType == typeof(JobReport) || + itemType == typeof(JobExpiredReport) || + itemType == typeof(JobMissingLicenseReport) + ) + { + return true; + } + + return false; + } + + [UsedImplicitly] + public new static string AllowAutoCreate() + { + return $"[{nameof(NetworkedItemManager)}]"; + } +} diff --git a/Multiplayer/Components/Networking/World/NetworkedJunction.cs b/Multiplayer/Components/Networking/World/NetworkedJunction.cs index c4b965f..cafbf66 100644 --- a/Multiplayer/Components/Networking/World/NetworkedJunction.cs +++ b/Multiplayer/Components/Networking/World/NetworkedJunction.cs @@ -28,8 +28,11 @@ private void Junction_Switched(Junction.SwitchMode switchMode, int branch) public void Switch(byte mode, byte selectedBranch) { - Junction.selectedBranch = selectedBranch - 1; // Junction#Switch increments this before processing - Junction.Switch((Junction.SwitchMode)mode); + //Junction.selectedBranch = (byte)(selectedBranch - 1); // Junction#Switch increments this before processing + //Junction.Switch((Junction.SwitchMode)mode); + + //B99 + Junction.Switch((Junction.SwitchMode)mode, selectedBranch); } public static bool Get(ushort netId, out NetworkedJunction obj) diff --git a/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs b/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs deleted file mode 100644 index 8e1c499..0000000 --- a/Multiplayer/Components/Networking/World/NetworkedRigidbody.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Multiplayer.Networking.Data; -using UnityEngine; - -namespace Multiplayer.Components.Networking.World; - -public class NetworkedRigidbody : TickedQueue -{ - private Rigidbody rigidbody; - - protected override void OnEnable() - { - rigidbody = GetComponent(); - if (rigidbody == null) - { - Multiplayer.LogError($"{gameObject.name}: {nameof(NetworkedRigidbody)} requires a {nameof(Rigidbody)} component on the same GameObject!"); - return; - } - - base.OnEnable(); - } - - protected override void Process(RigidbodySnapshot snapshot, uint snapshotTick) - { - snapshot.Apply(rigidbody); - } -} diff --git a/Multiplayer/Components/Networking/World/NetworkedStationController.cs b/Multiplayer/Components/Networking/World/NetworkedStationController.cs new file mode 100644 index 0000000..4d08c82 --- /dev/null +++ b/Multiplayer/Components/Networking/World/NetworkedStationController.cs @@ -0,0 +1,559 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using DV.Booklets; +using DV.Logic.Job; +using DV.ServicePenalty; +using DV.Utils; +using DV.ThingTypes; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Components.Networking.World; + +public class NetworkedStationController : IdMonoBehaviour +{ + #region Lookup Cache + private static readonly Dictionary stationControllerToNetworkedStationController = []; + private static readonly Dictionary stationIdToNetworkedStationController = []; + private static readonly Dictionary stationIdToStationController = []; + private static readonly Dictionary stationToNetworkedStationController = []; + private static readonly Dictionary jobValidatorToNetworkedStation = []; + private static readonly List jobValidators = []; + + public static bool Get(ushort netId, out NetworkedStationController obj) + { + bool b = Get(netId, out IdMonoBehaviour rawObj); + obj = (NetworkedStationController)rawObj; + return b; + } + + public static DictionaryGetAll() + { + Dictionary result = []; + + foreach (var kvp in stationIdToNetworkedStationController ) + { + //Multiplayer.Log($"GetAll() adding {kvp.Value.NetId}, {kvp.Key}"); + result.Add(kvp.Value.NetId, kvp.Key); + } + return result; + } + + public static bool GetStationController(ushort netId, out StationController obj) + { + bool b = Get(netId, out NetworkedStationController networkedStationController); + obj = b ? networkedStationController.StationController : null; + return b; + } + public static bool GetFromStationId(string stationId, out NetworkedStationController networkedStationController) + { + return stationIdToNetworkedStationController.TryGetValue(stationId, out networkedStationController); + } + + public static bool GetFromStation(Station station, out NetworkedStationController networkedStationController) + { + return stationToNetworkedStationController.TryGetValue(station, out networkedStationController); + } + public static bool GetStationControllerFromStationId(string stationId, out StationController stationController) + { + return stationIdToStationController.TryGetValue(stationId, out stationController); + } + + public static bool GetFromStationController(StationController stationController, out NetworkedStationController networkedStationController) + { + return stationControllerToNetworkedStationController.TryGetValue(stationController, out networkedStationController); + } + + public static bool GetFromJobValidator(JobValidator jobValidator, out NetworkedStationController networkedStationController) + { + if (jobValidator == null) + { + networkedStationController = null; + return false; + } + + return jobValidatorToNetworkedStation.TryGetValue(jobValidator, out networkedStationController); + } + + public static void RegisterStationController(NetworkedStationController networkedStationController, StationController stationController) + { + string stationID = stationController.logicStation.ID; + + stationControllerToNetworkedStationController.Add(stationController,networkedStationController); + stationIdToNetworkedStationController.Add(stationID, networkedStationController); + stationIdToStationController.Add(stationID, stationController); + stationToNetworkedStationController.Add(stationController.logicStation, networkedStationController); + } + + public static void QueueJobValidator(JobValidator jobValidator) + { + //Multiplayer.Log($"QueueJobValidator() {jobValidator.transform.parent.name}"); + + jobValidators.Add(jobValidator); + } + + private static void RegisterJobValidator(JobValidator jobValidator, NetworkedStationController stationController) + { + //Multiplayer.Log($"RegisterJobValidator() {jobValidator.transform.parent.name}, {stationController.name}"); + stationController.JobValidator = jobValidator; + jobValidatorToNetworkedStation[jobValidator] = stationController; + } + #endregion + + const int MAX_FRAMES = 120; + + protected override bool IsIdServerAuthoritative => true; + + public StationController StationController; + + public JobValidator JobValidator; + + public HashSet NetworkedJobs { get; } = []; + private readonly List NewJobs = []; + private readonly List DirtyJobs = []; + + private List availableJobs; + private List takenJobs; + private List abandonedJobs; + private List completedJobs; + + + protected override void Awake() + { + base.Awake(); + StationController = GetComponent(); + StartCoroutine(WaitForLogicStation()); + } + + protected void Start() + { + if (NetworkLifecycle.Instance.IsHost()) + { + NetworkLifecycle.Instance.OnTick += Server_OnTick; + } + } + + protected void OnDisable() + { + + if (UnloadWatcher.isQuitting) + return; + + NetworkLifecycle.Instance.OnTick -= Server_OnTick; + + string stationId = StationController.logicStation.ID; + + stationControllerToNetworkedStationController.Remove(StationController); + stationIdToNetworkedStationController.Remove(stationId); + stationIdToStationController.Remove(stationId); + stationToNetworkedStationController.Remove(StationController.logicStation); + jobValidatorToNetworkedStation.Remove(JobValidator); + jobValidators.Remove(this.JobValidator); + + Destroy(this); + } + + private IEnumerator WaitForLogicStation() + { + while (StationController.logicStation == null) + yield return null; + + RegisterStationController(this, StationController); + + availableJobs = StationController.logicStation.availableJobs; + takenJobs = StationController.logicStation.takenJobs; + abandonedJobs = StationController.logicStation.abandonedJobs; + completedJobs = StationController.logicStation.completedJobs; + + //Multiplayer.Log($"NetworkedStation.Awake({StationController.logicStation.ID})"); + + foreach (JobValidator validator in jobValidators) + { + string stationName = validator.transform.parent.name ?? ""; + stationName += "_office_anchor"; + + if(this.transform.parent.name.Equals(stationName, StringComparison.OrdinalIgnoreCase)) + { + JobValidator = validator; + RegisterJobValidator(validator, this); + jobValidators.Remove(validator); + break; + } + } + } + + #region Server + //Adding job + public void AddJob(Job job) + { + NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); + networkedJob.Initialize(job, this); + NetworkedJobs.Add(networkedJob); + + NewJobs.Add(networkedJob); + + //Setup handlers + networkedJob.OnJobDirty += OnJobDirty; + } + + private void OnJobDirty(NetworkedJob job) + { + if (!DirtyJobs.Contains(job)) + DirtyJobs.Add(job); + } + + private void Server_OnTick(uint tick) + { + //Send new jobs + if (NewJobs.Count > 0) + { + NetworkLifecycle.Instance.Server.SendJobsCreatePacket(this, NewJobs.ToArray()); + NewJobs.Clear(); + } + + //Send jobs with a changed status + if (DirtyJobs.Count > 0) + { + //todo send packet with updates + NetworkLifecycle.Instance.Server.SendJobsUpdatePacket(NetId, DirtyJobs.ToArray()); + DirtyJobs.Clear(); + } + } + #endregion Server + + #region Client + public void AddJobs(JobData[] jobs) + { + + foreach (JobData jobData in jobs) + { + //Cars may still be loading, we shouldn't spawn the job until they are ready + if (CheckCarsLoaded(jobData)) + { + Multiplayer.LogDebug(() => $"AddJobs() calling AddJob({jobData.ID})"); + AddJob(jobData); + } + else + { + Multiplayer.LogDebug(() => $"AddJobs() Delaying({jobData.ID})"); + StartCoroutine(DelayCreateJob(jobData)); + } + } + } + + private void AddJob(JobData jobData) + { + Job newJob = CreateJobFromJobData(jobData); + var carNetIds = jobData.GetCars(); + + NetworkedJob networkedJob = CreateNetworkedJob(newJob, jobData.NetID, carNetIds); + + NetworkedJobs.Add(networkedJob); + + if (networkedJob.Job.State == JobState.Available) + { + StationController.logicStation.AddJobToStation(newJob); + StationController.processedNewJobs.Add(newJob); + + if (jobData.ItemNetID != 0) + { + GenerateOverview(networkedJob, jobData.ItemNetID, jobData.ItemPosition); + } + } + else if (networkedJob.Job.State == JobState.InProgress) + { + takenJobs.Add(newJob); + } + else + { + //we don't need to update anything, so we'll return + //Maybe item sync will require knowledge of the job for expired/failed/completed reports, but we currently only sync these for connected players + return; + } + + + Multiplayer.LogDebug(() => $"AddJob({jobData.ID}) Starting plate update {newJob.ID} count: {jobData.GetCars().Count}"); + StartCoroutine(UpdateCarPlates(carNetIds, newJob.ID)); + + Multiplayer.Log($"Added NetworkedJob {newJob.ID} to NetworkedStationController {StationController.logicStation.ID}"); + } + + private Job CreateJobFromJobData(JobData jobData) + { + + List tasks = jobData.Tasks.Select(taskData => taskData.ToTask()).ToList(); + StationsChainData chainData = new(jobData.ChainData.ChainOriginYardId, jobData.ChainData.ChainDestinationYardId); + + Job newJob = new(tasks, jobData.JobType, jobData.TimeLimit, jobData.InitialWage, chainData, jobData.ID, jobData.RequiredLicenses) + { + startTime = jobData.StartTime, + finishTime = jobData.FinishTime, + State = jobData.State + }; + + return newJob; + } + + private IEnumerator DelayCreateJob(JobData jobData) + { + int frameCounter = 0; + + Multiplayer.LogDebug(()=>$"DelayCreateJob({jobData.NetID}) job type: {jobData.JobType}"); + + yield return new WaitForEndOfFrame(); + + while (frameCounter < MAX_FRAMES) + { + if (CheckCarsLoaded(jobData)) + { + Multiplayer.LogDebug(() => $"DelayCreateJob({jobData.NetID}) job type: {jobData.JobType}. Successfully created cars!"); + AddJob(jobData); + yield break; + } + + frameCounter++; + yield return new WaitForEndOfFrame(); + } + + Multiplayer.LogWarning($"Timeout waiting for cars to load for job {jobData.NetID}"); + } + + private bool CheckCarsLoaded(JobData jobData) + { + //extract all cars from the job and verify they have been initialised + foreach (var carNetId in jobData.GetCars()) + { + if (!NetworkedTrainCar.Get(carNetId, out NetworkedTrainCar car) || !car.Client_Initialized) + { + //car not spawned or not yet initialised + return false; + } + } + + return true; + } + + private NetworkedJob CreateNetworkedJob(Job job, ushort netId, List carNetIds) + { + NetworkedJob networkedJob = new GameObject($"NetworkedJob {job.ID}").AddComponent(); + networkedJob.NetId = netId; + networkedJob.Initialize(job, this); + networkedJob.OnJobDirty += OnJobDirty; + networkedJob.JobCars = carNetIds; + return networkedJob; + } + + public void UpdateJobs(JobUpdateStruct[] jobs) + { + foreach (JobUpdateStruct job in jobs) + { + if (!NetworkedJob.Get(job.JobNetID, out NetworkedJob netJob)) + continue; + + UpdateJobState(netJob, job); + UpdateJobOverview(netJob, job); + + netJob.Job.startTime = job.StartTime; + netJob.Job.finishTime = job.FinishTime; + } + } + + private void UpdateJobState(NetworkedJob netJob, JobUpdateStruct job) + { + if (netJob.Job.State != job.JobState) + { + netJob.Job.State = job.JobState; + HandleJobStateChange(netJob, job); + } + } + + private void UpdateJobOverview(NetworkedJob netJob, JobUpdateStruct job) + { + Multiplayer.Log($"UpdateJobOverview({netJob.Job.ID}) State: {job.JobState}, ItemNetId: {job.ItemNetID}"); + if (job.JobState == DV.ThingTypes.JobState.Available && job.ItemNetID != 0) + { + if (netJob.JobOverview == null) + GenerateOverview(netJob, job.ItemNetID, job.ItemPositionData); + /* + else + netJob.JobOverview.NetId = job.ItemNetID; + */ + } + } + + private void HandleJobStateChange(NetworkedJob netJob, JobUpdateStruct updateData) + { + JobValidator validator = null; + NetworkedItem netItem; + string jobIdStr = $"[{netJob?.Job?.ID}, {netJob.NetId}]"; + + NetworkLifecycle.Instance.Client.LogDebug(() => $"HandleJobStateChange({jobIdStr}) Current state: {netJob?.Job?.State}, New state: {updateData.JobState}, ValidationStationNetId: {updateData.ValidationStationId}, ItemNetId: {updateData.ItemNetID}"); + + bool shouldPrint = updateData.JobState == JobState.InProgress || updateData.JobState == JobState.Completed; + bool canPrint = true; + + if (shouldPrint) + { + if (updateData.ValidationStationId != 0 && Get(updateData.ValidationStationId, out var netStation)) + { + validator = netStation.JobValidator; + } + else + { + NetworkLifecycle.Instance.Client.LogError($"HandleJobStateChange({jobIdStr}) Validator not found or data missing! Validator ID: {updateData.ValidationStationId}"); + canPrint = false; + } + + if (updateData.ItemNetID == 0) + { + NetworkLifecycle.Instance.Client.LogError($"HandleJobStateChange({jobIdStr}) Missing item data!"); + canPrint = false; + } + } + + + bool printed = false; + switch (netJob.Job.State) + { + case JobState.InProgress: + availableJobs.Remove(netJob.Job); + takenJobs.Add(netJob.Job); + + if (canPrint) + { + JobBooklet jobBooklet = BookletCreator.CreateJobBooklet(netJob.Job, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent, true); + netItem = jobBooklet.GetOrAddComponent(); + netItem.Initialize(jobBooklet, updateData.ItemNetID, false); + netJob.JobBooklet = netItem; + printed = true; + } + + netJob.JobOverview?.GetTrackedItem()?.DestroyJobOverview(); + + break; + + case JobState.Completed: + takenJobs.Remove(netJob.Job); + completedJobs.Add(netJob.Job); + + if (canPrint) + { + DisplayableDebt displayableDebt = SingletonBehaviour.Instance.LastStagedJobDebt; + JobReport jobReport = BookletCreator.CreateJobReport(netJob.Job, displayableDebt, validator.bookletPrinter.spawnAnchor.position, validator.bookletPrinter.spawnAnchor.rotation, WorldMover.OriginShiftParent); + netItem = jobReport.GetOrAddComponent(); + netItem.Initialize(jobReport, updateData.ItemNetID, false); + netJob.AddReport(netItem); + printed = true; + } + + StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); + netJob.JobBooklet?.GetTrackedItem()?.DestroyJobBooklet(); + + break; + + case JobState.Abandoned: + takenJobs.Remove(netJob.Job); + abandonedJobs.Add(netJob.Job); + StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); + break; + + case JobState.Expired: + //if (availableJobs.Contains(netJob.Job)) + // availableJobs.Remove(netJob.Job); + + netJob.Job.ExpireJob(); + StationController.ClearAvailableJobOverviewGOs(); //todo: better logic when players can hold items + StartCoroutine(UpdateCarPlates(netJob.JobCars, string.Empty)); + break; + + default: + NetworkLifecycle.Instance.Client.LogError($"HandleJobStateChange({jobIdStr}) Unrecognised Job State: {updateData.JobState}"); + break; + } + + if (printed) + { + netJob.ValidatorResponseReceived = true; + netJob.ValidationAccepted = true; + validator.jobValidatedSound.Play(validator.bookletPrinter.spawnAnchor.position, 1f, 1f, 0f, 1f, 500f, default, null, validator.transform, false, 0f, null); + validator.bookletPrinter.Print(false); + } + } + public void RemoveJob(NetworkedJob job) + { + if (availableJobs.Contains(job.Job)) + availableJobs.Remove(job.Job); + + if (takenJobs.Contains(job.Job)) + takenJobs.Remove(job.Job); + + if (completedJobs.Contains(job.Job)) + completedJobs.Remove(job.Job); + + if (abandonedJobs.Contains(job.Job)) + abandonedJobs.Remove(job.Job); + + job.JobOverview?.GetTrackedItem()?.DestroyJobOverview(); + job.JobBooklet?.GetTrackedItem()?.DestroyJobBooklet(); + + job.ClearReports(); + + NetworkedJobs.Remove(job); + GameObject.Destroy(job); + } + + public static IEnumerator UpdateCarPlates(List carNetIds, string jobId) + { + + Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) carNetIds: {carNetIds?.Count}"); + + if (carNetIds == null || string.IsNullOrEmpty(jobId)) + yield break; + + foreach (ushort carNetId in carNetIds) + { + int frameCounter = 0; + TrainCar trainCar = null; + + while (frameCounter < MAX_FRAMES) + { + + if (NetworkedTrainCar.GetTrainCar(carNetId, out trainCar) && + trainCar != null && + trainCar.trainPlatesCtrl?.trainCarPlates != null && + trainCar.trainPlatesCtrl.trainCarPlates.Count > 0) + { + //Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Calling Update"); + trainCar.UpdateJobIdOnCarPlates(jobId); + break; + } + + Multiplayer.LogDebug(() => $"UpdateCarPlates({jobId}) car: {carNetId}, frameCount: {frameCounter}. Incrementing frames"); + frameCounter++; + yield return new WaitForEndOfFrame(); + } + + if (frameCounter >= MAX_FRAMES) + { + Multiplayer.LogError($"Failed to update plates for car [{trainCar?.ID}, {carNetId}] (Job: {jobId}) after {frameCounter} frames"); + } + } + } + + private void GenerateOverview(NetworkedJob networkedJob, ushort itemNetId, ItemPositionData posData) + { + Multiplayer.Log($"GenerateOverview({networkedJob.Job.ID}, {itemNetId}) Position: {posData.Position}, Less currentMove: {posData.Position + WorldMover.currentMove} "); + JobOverview jobOverview = BookletCreator_JobOverview.Create(networkedJob.Job, posData.Position + WorldMover.currentMove, posData.Rotation,WorldMover.OriginShiftParent); + + NetworkedItem netItem = jobOverview.GetOrAddComponent(); + netItem.Initialize(jobOverview, itemNetId, false); + networkedJob.JobOverview = netItem; + StationController.spawnedJobOverviews.Add(jobOverview); + } + #endregion +} diff --git a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs index ad2b0dd..ac5832d 100644 --- a/Multiplayer/Components/Networking/World/NetworkedTurntable.cs +++ b/Multiplayer/Components/Networking/World/NetworkedTurntable.cs @@ -31,7 +31,7 @@ protected override void OnDestroy() private void OnTick(uint tick) { - if (Mathf.Approximately(lastYRotation, TurntableRailTrack.targetYRotation)) + if (Mathf.Approximately(lastYRotation, TurntableRailTrack.targetYRotation) || UnloadWatcher.isUnloading) return; lastYRotation = TurntableRailTrack.targetYRotation; diff --git a/Multiplayer/Components/SaveGame/Client_GameSession.cs b/Multiplayer/Components/SaveGame/Client_GameSession.cs new file mode 100644 index 0000000..7b8a3f8 --- /dev/null +++ b/Multiplayer/Components/SaveGame/Client_GameSession.cs @@ -0,0 +1,97 @@ +using DV.Common; +using DV.JObjectExtstensions; +using DV.Scenarios.Common; +using DV.UserManagement; +using DV.UserManagement.Data; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace Multiplayer.Components.SaveGame; + +public class Client_GameSession : IGameSession, IThing, IDisposable +{ + private string _gameMode; + private JObject _gameData = new JObject(); + public static void SetCurrent(IGameSession session) + { + try + { + PropertyInfo currentSession = typeof(User).GetProperty("CurrentSession"); + currentSession?.SetValue(UserManager.Instance.CurrentUser, session); + } + catch (Exception ex) + { + Multiplayer.Log($"Client_GameSession.SetCurrent() failed: \r\n{ex.ToString()}"); + } + } + public Client_GameSession(string GameMode, IDifficulty difficulty) + { + _gameMode = GameMode; + _gameData.SetBool("Difficulty_picked", true); + Saves = new ReadOnlyObservableCollection(new ObservableCollection()); + + this.SetDifficulty(difficulty); + } + + string IGameSession.GameMode => _gameMode; + + string IGameSession.World => null; + + int IGameSession.SessionID => int.MaxValue; + + JObject IGameSession.GameData => _gameData; + + IUserProfile IGameSession.Owner => null; + + string IGameSession.BasePath => null; + + public ReadOnlyObservableCollection Saves { get; private set; } + + ISaveGame IGameSession.LatestSave => null; + + string IThing.Name { get => "Multiplayer Session"; set => throw new NotImplementedException(); } + + int IThing.DataVersion => 1; //might need to extract this from the Vanilla GameSession + + public void Save() + { + //do nothing + } + + void IGameSession.DeleteSaveGame(ISaveGame save) + { + //do nothing + } + + void IDisposable.Dispose() + { + //do nothing + } + + int IGameSession.GetSavesCountByType(SaveType type) + { + return 0; + } + + void IGameSession.MakeCurrent() + { + //do nothing + } + + ISaveGame IGameSession.SaveGame(SaveType type, JObject data, Texture2D thumbnail, List<(int Type, byte[] Data)> customChunks, ISaveGame overwrite) + { + return null; + } + + int IGameSession.TrimSaves(SaveType type, int maxCount, ISaveGame excluded) + { + return 0; + } +} diff --git a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs index af0b5df..5ee3a8f 100644 --- a/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs +++ b/Multiplayer/Components/SaveGame/NetworkedSaveGameManager.cs @@ -6,7 +6,7 @@ using JetBrains.Annotations; using Multiplayer.Components.Networking; using Multiplayer.Networking.Data; -using Multiplayer.Networking.Listeners; +using Multiplayer.Networking.Managers.Server; using Newtonsoft.Json.Linq; namespace Multiplayer.Components.SaveGame; @@ -24,6 +24,7 @@ protected override void Awake() Inventory.Instance.MoneyChanged += Server_OnMoneyChanged; LicenseManager.Instance.LicenseAcquired += Server_OnLicenseAcquired; LicenseManager.Instance.JobLicenseAcquired += Server_OnJobLicenseAcquired; + LicenseManager.Instance.GarageUnlocked += Server_OnGarageUnlocked; } protected override void OnDestroy() @@ -63,16 +64,17 @@ private static void Server_OnGarageUnlocked(GarageType_v2 garage) public void Server_UpdateInternalData(SaveGameData data) { - JObject root = data.GetJObject(ROOT_KEY) ?? new JObject(); - JObject players = root.GetJObject(PLAYERS_KEY) ?? new JObject(); + JObject root = data.GetJObject(ROOT_KEY) ?? []; + JObject players = root.GetJObject(PLAYERS_KEY) ?? []; foreach (ServerPlayer player in NetworkLifecycle.Instance.Server.ServerPlayers) { if (player.Id == NetworkServer.SelfId || !player.IsLoaded) continue; - JObject playerData = new(); + JObject playerData = []; playerData.SetVector3(SaveGameKeys.Player_position, player.AbsoluteWorldPosition); playerData.SetFloat(SaveGameKeys.Player_rotation, player.WorldRotationY); + //store inventory see StorageSerializer.SaveStorage() players.SetJObject(player.Guid.ToString(), playerData); } diff --git a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs index c764c93..2f14c15 100644 --- a/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs +++ b/Multiplayer/Components/SaveGame/StartGameData_ServerSave.cs @@ -1,6 +1,14 @@ using System; using System.Collections; +using System.ComponentModel; +using System.Linq; +using DV; +using DV.CabControls; +using DV.Common; using DV.UserManagement; +using DV.Utils; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; using Multiplayer.Networking.Packets.Clientbound; using Multiplayer.Patches.SaveGame; using Newtonsoft.Json.Linq; @@ -14,6 +22,8 @@ public class StartGameData_ServerSave : AStartGameData private ClientboundSaveGameDataPacket packet; + public override bool IsStartingNewSession => false; + public void SetFromPacket(ClientboundSaveGameDataPacket packet) { this.packet = packet.Clone(); @@ -35,6 +45,24 @@ public void SetFromPacket(ClientboundSaveGameDataPacket packet) saveGameData.SetBool(SaveGameKeys.Damage_Popup_Shown, true); CareerManagerDebtControllerPatch.HasDebt = packet.HasDebt; + + Multiplayer.LogDebug(() => + { + string unlockedGen = string.Join(", ", UnlockablesManager.Instance.UnlockedGeneralLicenses); + string packetGen = string.Join(", ", packet.AcquiredGeneralLicenses); + + string unlockedJob = string.Join(", ", UnlockablesManager.Instance.UnlockedJobLicenses); + string packetJob = string.Join(", ", packet.AcquiredJobLicenses); + + return $"StartGameData_ServerSave.SetFromPacket() UnlockedGen: {{{unlockedGen}}}, PacketGen: {{{packetGen}}}, UnlockedJob: {{{unlockedJob}}}, PacketJob: {{{packetJob}}}"; + }); + + + //For clients we need to have a session - new users may not have a session and this may also be causing problems with licenses syncing + if (NetworkLifecycle.Instance.IsHost()) + return; + + Client_GameSession.SetCurrent(new Client_GameSession(packet.GameMode, DifficultyToUse)); } public override void Initialize() @@ -51,6 +79,7 @@ public override SaveGameData GetSaveGameData() public override IEnumerator DoLoad(Transform playerContainer) { + Transform playerTransform = playerContainer.transform; playerTransform.position = PlayerManager.IsPlayerPositionValid(packet.Position) ? packet.Position : LevelInfo.Instance.defaultSpawnPosition; playerTransform.eulerAngles = new Vector3(0, packet.Rotation, 0); @@ -59,8 +88,8 @@ public override IEnumerator DoLoad(Transform playerContainer) if (saveGameData.GetString(SaveGameKeys.Game_mode) == "FreeRoam") LicenseManager.Instance.GrabAllGameModeSpecificUnlockables(SaveGameKeys.Game_mode); - else - StartingItemsController.Instance.AddStartingItems(saveGameData, true); + //else + StartingItemsController.Instance.AddStartingItems(saveGameData, true); // if (packet.Debt_existing_locos != null) // LocoDebtController.Instance.LoadExistingLocosDebtsSaveData(packet.Debt_existing_locos.Select(JObject.Parse).ToArray()); @@ -93,3 +122,4 @@ public override bool ShouldCreateSaveGameAfterLoad() public override void MakeCurrent(){} } + diff --git a/Multiplayer/Components/Util/HyperlinkHandler.cs b/Multiplayer/Components/Util/HyperlinkHandler.cs new file mode 100644 index 0000000..91cc380 --- /dev/null +++ b/Multiplayer/Components/Util/HyperlinkHandler.cs @@ -0,0 +1,130 @@ +using UnityEngine; +using TMPro; +using UnityEngine.EventSystems; +using System.Text.RegularExpressions; + +namespace Multiplayer.Components.Util +{ + public class HyperlinkHandler : MonoBehaviour, IPointerClickHandler + { + public static readonly Color DEFAULT_COLOR = Color.blue; + public static readonly Color DEFAULT_HOVER_COLOR = new Color(0x00, 0x59, 0xFF, 0xFF); + + public Color linkColor = DEFAULT_COLOR; + public Color linkHoverColor = DEFAULT_HOVER_COLOR; + + public TextMeshProUGUI textComponent; + private Canvas canvas; + private Camera canvasCamera; + + private int hoveredLinkIndex = -1; + private bool underlineLinks = true; + + void Start() + { + if (textComponent == null) + { + textComponent = GetComponent(); + } + + canvas = GetComponentInParent(); + if (canvas != null) + { + canvasCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera; + } + + ApplyLinkStyling(); + } + + void Update() + { + int linkIndex = TMP_TextUtilities.FindIntersectingLink(textComponent, Input.mousePosition, canvasCamera); + + if (linkIndex != -1 && linkIndex != hoveredLinkIndex) + { + // Mouse is over a new link + if (hoveredLinkIndex != -1) + { + // Remove hover style from the previously hovered link + RemoveHoverStyle(hoveredLinkIndex); + } + ApplyHoverStyle(linkIndex); + hoveredLinkIndex = linkIndex; + } + else if (linkIndex == -1 && hoveredLinkIndex != -1) + { + // Mouse is no longer over any link + RemoveHoverStyle(hoveredLinkIndex); + hoveredLinkIndex = -1; + } + } + + public void OnPointerClick(PointerEventData eventData) + { + int linkIndex = TMP_TextUtilities.FindIntersectingLink(textComponent, Input.mousePosition, canvasCamera); + + if (linkIndex != -1) + { + TMP_LinkInfo linkInfo = textComponent.textInfo.linkInfo[linkIndex]; + string url = linkInfo.GetLinkID(); + Application.OpenURL(url); + } + } + + private void ApplyLinkStyling() + { + string text = textComponent.text; + string pattern = @"(.*?)<\/link>"; + string replacement = underlineLinks + ? $"$2" + : $"$2"; + + text = Regex.Replace(text, pattern, replacement); + textComponent.text = text; + } + + private void ApplyHoverStyle(int linkIndex) + { + TMP_LinkInfo linkInfo = textComponent.textInfo.linkInfo[linkIndex]; + SetLinkColor(linkInfo, linkHoverColor); + } + + private void RemoveHoverStyle(int linkIndex) + { + TMP_LinkInfo linkInfo = textComponent.textInfo.linkInfo[linkIndex]; + SetLinkColor(linkInfo, linkColor); + } + + private void SetLinkColor(TMP_LinkInfo linkInfo, Color32 color) + { + var meshInfo = textComponent.textInfo.meshInfo[0]; + + for (int i = 0; i < linkInfo.linkTextLength; i++) + { + int characterIndex = linkInfo.linkTextfirstCharacterIndex + i; + + // Check if the character is within bounds and is visible + if (characterIndex >= textComponent.textInfo.characterCount || + !textComponent.textInfo.characterInfo[characterIndex].isVisible) + continue; + + int materialIndex = textComponent.textInfo.characterInfo[characterIndex].materialReferenceIndex; + int vertexIndex = textComponent.textInfo.characterInfo[characterIndex].vertexIndex; + + // Ensure we're using the correct mesh info + meshInfo = textComponent.textInfo.meshInfo[materialIndex]; + + meshInfo.colors32[vertexIndex] = color; + meshInfo.colors32[vertexIndex + 1] = color; + meshInfo.colors32[vertexIndex + 2] = color; + meshInfo.colors32[vertexIndex + 3] = color; + } + + // Mark the vertex data as dirty for all used materials + for (int i = 0; i < textComponent.textInfo.materialCount; i++) + { + textComponent.UpdateVertexData(TMP_VertexDataUpdateFlags.Colors32); + } + } + } +} diff --git a/Multiplayer/Locale.cs b/Multiplayer/Locale.cs index e6d1544..7a99dd0 100644 --- a/Multiplayer/Locale.cs +++ b/Multiplayer/Locale.cs @@ -2,152 +2,236 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; +using dnlib.DotNet; +using DV.Rain; +using Humanizer; +using System.Xml.Linq; using I2.Loc; using Multiplayer.Utils; +using static VLB.Consts; -namespace Multiplayer; - -public static class Locale +namespace Multiplayer { - private const string DEFAULT_LOCALE_FILE = "locale.csv"; - - private const string DEFAULT_LANGUAGE = "English"; - public const string MISSING_TRANSLATION = "[ MISSING TRANSLATION ]"; - public const string PREFIX = "multiplayer/"; - - private const string PREFIX_MAIN_MENU = $"{PREFIX}mm"; - private const string PREFIX_SERVER_BROWSER = $"{PREFIX}sb"; - private const string PREFIX_DISCONN_REASON = $"{PREFIX}dr"; - private const string PREFIX_CAREER_MANAGER = $"{PREFIX}carman"; - private const string PREFIX_PLAYER_LIST = $"{PREFIX}plist"; - private const string PREFIX_LOADING_INFO = $"{PREFIX}linfo"; - - #region Main Menu - - public static string MAIN_MENU__JOIN_SERVER => Get(MAIN_MENU__JOIN_SERVER_KEY); - public const string MAIN_MENU__JOIN_SERVER_KEY = $"{PREFIX_MAIN_MENU}/join_server"; - - #endregion - - #region Server Browser - - public static string SERVER_BROWSER__TITLE => Get(SERVER_BROWSER__TITLE_KEY); - public const string SERVER_BROWSER__TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/title"; - - public static string SERVER_BROWSER__IP => Get(SERVER_BROWSER__IP_KEY); - private const string SERVER_BROWSER__IP_KEY = $"{PREFIX_SERVER_BROWSER}/ip"; - public static string SERVER_BROWSER__IP_INVALID => Get(SERVER_BROWSER__IP_INVALID_KEY); - private const string SERVER_BROWSER__IP_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/ip_invalid"; - public static string SERVER_BROWSER__PORT => Get(SERVER_BROWSER__PORT_KEY); - private const string SERVER_BROWSER__PORT_KEY = $"{PREFIX_SERVER_BROWSER}/port"; - public static string SERVER_BROWSER__PORT_INVALID => Get(SERVER_BROWSER__PORT_INVALID_KEY); - private const string SERVER_BROWSER__PORT_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/port_invalid"; - public static string SERVER_BROWSER__PASSWORD => Get(SERVER_BROWSER__PASSWORD_KEY); - private const string SERVER_BROWSER__PASSWORD_KEY = $"{PREFIX_SERVER_BROWSER}/password"; - - #endregion - - #region Disconnect Reason - - public static string DISCONN_REASON__INVALID_PASSWORD => Get(DISCONN_REASON__INVALID_PASSWORD_KEY); - public const string DISCONN_REASON__INVALID_PASSWORD_KEY = $"{PREFIX_DISCONN_REASON}/invalid_password"; - public static string DISCONN_REASON__GAME_VERSION => Get(DISCONN_REASON__GAME_VERSION_KEY); - public const string DISCONN_REASON__GAME_VERSION_KEY = $"{PREFIX_DISCONN_REASON}/game_version"; - public static string DISCONN_REASON__FULL_SERVER => Get(DISCONN_REASON__FULL_SERVER_KEY); - public const string DISCONN_REASON__FULL_SERVER_KEY = $"{PREFIX_DISCONN_REASON}/full_server"; - public static string DISCONN_REASON__MODS => Get(DISCONN_REASON__MODS_KEY); - public const string DISCONN_REASON__MODS_KEY = $"{PREFIX_DISCONN_REASON}/mods"; - public static string DISCONN_REASON__MODS_MISSING => Get(DISCONN_REASON__MODS_MISSING_KEY); - public const string DISCONN_REASON__MODS_MISSING_KEY = $"{PREFIX_DISCONN_REASON}/mods_missing"; - public static string DISCONN_REASON__MODS_EXTRA => Get(DISCONN_REASON__MODS_EXTRA_KEY); - public const string DISCONN_REASON__MODS_EXTRA_KEY = $"{PREFIX_DISCONN_REASON}/mods_extra"; - - #endregion - - #region Career Manager - - public static string CAREER_MANAGER__FEES_HOST_ONLY => Get(CAREER_MANAGER__FEES_HOST_ONLY_KEY); - private const string CAREER_MANAGER__FEES_HOST_ONLY_KEY = $"{PREFIX_CAREER_MANAGER}/fees_host_only"; - - #endregion - - #region Player List - - public static string PLAYER_LIST__TITLE => Get(PLAYER_LIST__TITLE_KEY); - private const string PLAYER_LIST__TITLE_KEY = $"{PREFIX_PLAYER_LIST}/title"; - - #endregion - - #region Loading Info - - public static string LOADING_INFO__WAIT_FOR_SERVER => Get(LOADING_INFO__WAIT_FOR_SERVER_KEY); - private const string LOADING_INFO__WAIT_FOR_SERVER_KEY = $"{PREFIX_LOADING_INFO}/wait_for_server"; - - public static string LOADING_INFO__SYNC_WORLD_STATE => Get(LOADING_INFO__SYNC_WORLD_STATE_KEY); - private const string LOADING_INFO__SYNC_WORLD_STATE_KEY = $"{PREFIX_LOADING_INFO}/sync_world_state"; - - #endregion - - private static bool initializeAttempted; - private static ReadOnlyDictionary> csv; - - public static void Load(string localeDir) + public static class Locale { - initializeAttempted = true; - string path = Path.Combine(localeDir, DEFAULT_LOCALE_FILE); - if (!File.Exists(path)) + private const string DEFAULT_LOCALE_FILE = "locale.csv"; + private const string DEFAULT_LANGUAGE = "English"; + public const string MISSING_TRANSLATION = "[ MISSING TRANSLATION ]"; + public const string PREFIX = "multiplayer/"; + + private const string PREFIX_MAIN_MENU = $"{PREFIX}mm"; + private const string PREFIX_SERVER_BROWSER = $"{PREFIX}sb"; + private const string PREFIX_SERVER_HOST = $"{PREFIX}host"; + private const string PREFIX_DISCONN_REASON = $"{PREFIX}dr"; + private const string PREFIX_CAREER_MANAGER = $"{PREFIX}carman"; + private const string PREFIX_PLAYER_LIST = $"{PREFIX}plist"; + private const string PREFIX_LOADING_INFO = $"{PREFIX}linfo"; + private const string PREFIX_CHAT_INFO = $"{PREFIX}chat"; + private const string PREFIX_PAUSE_MENU = $"{PREFIX}pm"; + + #region Main Menu + public static string MAIN_MENU__JOIN_SERVER => Get(MAIN_MENU__JOIN_SERVER_KEY); + public const string MAIN_MENU__JOIN_SERVER_KEY = $"{PREFIX_MAIN_MENU}/join_server"; + #endregion + + #region Server Browser + public static string SERVER_BROWSER__TITLE => Get(SERVER_BROWSER__TITLE_KEY); + public const string SERVER_BROWSER__TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/title"; + public static string SERVER_BROWSER__MANUAL_CONNECT => Get(SERVER_BROWSER__MANUAL_CONNECT_KEY); + public const string SERVER_BROWSER__MANUAL_CONNECT_KEY = $"{PREFIX_SERVER_BROWSER}/manual_connect"; + public static string SERVER_BROWSER__HOST => Get(SERVER_BROWSER__HOST_KEY); + public const string SERVER_BROWSER__HOST_KEY = $"{PREFIX_SERVER_BROWSER}/host"; + public static string SERVER_BROWSER__REFRESH => Get(SERVER_BROWSER__REFRESH_KEY); + public const string SERVER_BROWSER__REFRESH_KEY = $"{PREFIX_SERVER_BROWSER}/refresh"; + public static string SERVER_BROWSER__JOIN => Get(SERVER_BROWSER__JOIN_KEY); + public const string SERVER_BROWSER__JOIN_KEY = $"{PREFIX_SERVER_BROWSER}/join_game"; + public static string SERVER_BROWSER__IP => Get(SERVER_BROWSER__IP_KEY); + private const string SERVER_BROWSER__IP_KEY = $"{PREFIX_SERVER_BROWSER}/ip"; + public static string SERVER_BROWSER__IP_INVALID => Get(SERVER_BROWSER__IP_INVALID_KEY); + private const string SERVER_BROWSER__IP_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/ip_invalid"; + public static string SERVER_BROWSER__PORT => Get(SERVER_BROWSER__PORT_KEY); + private const string SERVER_BROWSER__PORT_KEY = $"{PREFIX_SERVER_BROWSER}/port"; + public static string SERVER_BROWSER__PORT_INVALID => Get(SERVER_BROWSER__PORT_INVALID_KEY); + private const string SERVER_BROWSER__PORT_INVALID_KEY = $"{PREFIX_SERVER_BROWSER}/port_invalid"; + public static string SERVER_BROWSER__PASSWORD => Get(SERVER_BROWSER__PASSWORD_KEY); + private const string SERVER_BROWSER__PASSWORD_KEY = $"{PREFIX_SERVER_BROWSER}/password"; + public static string SERVER_BROWSER__PLAYERS => Get(SERVER_BROWSER__PLAYERS_KEY); + private const string SERVER_BROWSER__PLAYERS_KEY = $"{PREFIX_SERVER_BROWSER}/players"; + public static string SERVER_BROWSER__PASSWORD_REQUIRED => Get(SERVER_BROWSER__PASSWORD_REQUIRED_KEY); + private const string SERVER_BROWSER__PASSWORD_REQUIRED_KEY = $"{PREFIX_SERVER_BROWSER}/password_required"; + public static string SERVER_BROWSER__MODS_REQUIRED => Get(SERVER_BROWSER__MODS_REQUIRED_KEY); + private const string SERVER_BROWSER__MODS_REQUIRED_KEY = $"{PREFIX_SERVER_BROWSER}/mods_required"; + public static string SERVER_BROWSER__GAME_VERSION => Get(SERVER_BROWSER__GAME_VERSION_KEY); + private const string SERVER_BROWSER__GAME_VERSION_KEY = $"{PREFIX_SERVER_BROWSER}/game_version"; + public static string SERVER_BROWSER__MOD_VERSION => Get(SERVER_BROWSER__MOD_VERSION_KEY); + private const string SERVER_BROWSER__MOD_VERSION_KEY = $"{PREFIX_SERVER_BROWSER}/mod_version"; + public static string SERVER_BROWSER__YES => Get(SERVER_BROWSER__YES_KEY); + private const string SERVER_BROWSER__YES_KEY = $"{PREFIX_SERVER_BROWSER}/yes"; + public static string SERVER_BROWSER__NO => Get(SERVER_BROWSER__NO_KEY); + private const string SERVER_BROWSER__NO_KEY = $"{PREFIX_SERVER_BROWSER}/no"; + public static string SERVER_BROWSER__NO_SERVERS => Get(SERVER_BROWSER__NO_SERVERS_KEY); + public const string SERVER_BROWSER__NO_SERVERS_KEY = $"{PREFIX_SERVER_BROWSER}/no_servers"; + public static string SERVER_BROWSER__INFO_TITLE => Get(SERVER_BROWSER__INFO_TITLE_KEY); + public const string SERVER_BROWSER__INFO_TITLE_KEY = $"{PREFIX_SERVER_BROWSER}/info/title"; + public static string SERVER_BROWSER__INFO_CONTENT => Get(SERVER_BROWSER__INFO_CONTENT_KEY); + public const string SERVER_BROWSER__INFO_CONTENT_KEY = $"{PREFIX_SERVER_BROWSER}/info/content"; + #endregion + + #region Server Host + public static string SERVER_HOST__TITLE => Get(SERVER_HOST__TITLE_KEY); + public const string SERVER_HOST__TITLE_KEY = $"{PREFIX_SERVER_HOST}/title"; + public static string SERVER_HOST_PASSWORD => Get(SERVER_HOST_PASSWORD_KEY); + public const string SERVER_HOST_PASSWORD_KEY = $"{PREFIX_SERVER_HOST}/password"; + public static string SERVER_HOST_NAME => Get(SERVER_HOST_NAME_KEY); + public const string SERVER_HOST_NAME_KEY = $"{PREFIX_SERVER_HOST}/name"; + public static string SERVER_HOST_PUBLIC => Get(SERVER_HOST_PUBLIC_KEY); + public const string SERVER_HOST_PUBLIC_KEY = $"{PREFIX_SERVER_HOST}/public"; + public static string SERVER_HOST_DETAILS => Get(SERVER_HOST_DETAILS_KEY); + public const string SERVER_HOST_DETAILS_KEY = $"{PREFIX_SERVER_HOST}/details"; + public static string SERVER_HOST_MAX_PLAYERS => Get(SERVER_HOST_MAX_PLAYERS_KEY); + public const string SERVER_HOST_MAX_PLAYERS_KEY = $"{PREFIX_SERVER_HOST}/max_players"; + public static string SERVER_HOST_START => Get(SERVER_HOST_START_KEY); + public const string SERVER_HOST_START_KEY = $"{PREFIX_SERVER_HOST}/start"; + + public static string SERVER_HOST__INSTRUCTIONS_FIRST => Get(SERVER_HOST__INSTRUCTIONS_FIRST_KEY); + public const string SERVER_HOST__INSTRUCTIONS_FIRST_KEY = $"{PREFIX_SERVER_HOST}/instructions/first"; + public static string SERVER_HOST__MOD_WARNING => Get(SERVER_HOST__MOD_WARNING_KEY); + + public const string SERVER_HOST__MOD_WARNING_KEY = $"{PREFIX_SERVER_HOST}/instructions/mod_warning"; + public static string SERVER_HOST__RECOMMEND => Get(SERVER_HOST__RECOMMEND_KEY); + public const string SERVER_HOST__RECOMMEND_KEY = $"{PREFIX_SERVER_HOST}/instructions/recommend"; + public static string SERVER_HOST__SIGNOFF => Get(SERVER_HOST__SIGNOFF_KEY); + public const string SERVER_HOST__SIGNOFF_KEY = $"{PREFIX_SERVER_HOST}/instructions/signoff"; + + + + #endregion + #region Disconnect Reason + public static string DISCONN_REASON__INVALID_PASSWORD => Get(DISCONN_REASON__INVALID_PASSWORD_KEY); + public const string DISCONN_REASON__INVALID_PASSWORD_KEY = $"{PREFIX_DISCONN_REASON}/invalid_password"; + + public static string DISCONN_REASON__GAME_VERSION => Get(DISCONN_REASON__GAME_VERSION_KEY); + public const string DISCONN_REASON__GAME_VERSION_KEY = $"{PREFIX_DISCONN_REASON}/game_version"; + + public static string DISCONN_REASON__FULL_SERVER => Get(DISCONN_REASON__FULL_SERVER_KEY); + public const string DISCONN_REASON__FULL_SERVER_KEY = $"{PREFIX_DISCONN_REASON}/full_server"; + + public static string DISCONN_REASON__MODS => Get(DISCONN_REASON__MODS_KEY); + public const string DISCONN_REASON__MODS_KEY = $"{PREFIX_DISCONN_REASON}/mods"; + + public static string DISCONN_REASON__MOD_LIST => Get(DISCONN_REASON__MOD_LIST_KEY); + public const string DISCONN_REASON__MOD_LIST_KEY = $"{PREFIX_DISCONN_REASON}/mod_list"; + + public static string DISCONN_REASON__MODS_MISSING => Get(DISCONN_REASON__MODS_MISSING_KEY); + public const string DISCONN_REASON__MODS_MISSING_KEY = $"{PREFIX_DISCONN_REASON}/mods_missing"; + + public static string DISCONN_REASON__MODS_EXTRA => Get(DISCONN_REASON__MODS_EXTRA_KEY); + public const string DISCONN_REASON__MODS_EXTRA_KEY = $"{PREFIX_DISCONN_REASON}/mods_extra"; + #endregion + + #region Career Manager + public static string CAREER_MANAGER__FEES_HOST_ONLY => Get(CAREER_MANAGER__FEES_HOST_ONLY_KEY); + private const string CAREER_MANAGER__FEES_HOST_ONLY_KEY = $"{PREFIX_CAREER_MANAGER}/fees_host_only"; + #endregion + + #region Player List + public static string PLAYER_LIST__TITLE => Get(PLAYER_LIST__TITLE_KEY); + private const string PLAYER_LIST__TITLE_KEY = $"{PREFIX_PLAYER_LIST}/title"; + #endregion + + #region Loading Info + public static string LOADING_INFO__WAIT_FOR_SERVER => Get(LOADING_INFO__WAIT_FOR_SERVER_KEY); + private const string LOADING_INFO__WAIT_FOR_SERVER_KEY = $"{PREFIX_LOADING_INFO}/wait_for_server"; + + public static string LOADING_INFO__SYNC_WORLD_STATE => Get(LOADING_INFO__SYNC_WORLD_STATE_KEY); + private const string LOADING_INFO__SYNC_WORLD_STATE_KEY = $"{PREFIX_LOADING_INFO}/sync_world_state"; + #endregion + + #region Chat + public static string CHAT_PLACEHOLDER => Get(CHAT_PLACEHOLDER_KEY); + public const string CHAT_PLACEHOLDER_KEY = $"{PREFIX_CHAT_INFO}/placeholder"; + public static string CHAT_HELP_AVAILABLE => Get(CHAT_HELP_AVAILABLE_KEY); + public const string CHAT_HELP_AVAILABLE_KEY = $"{PREFIX_CHAT_INFO}/help/available"; + public static string CHAT_HELP_SERVER_MSG => Get(CHAT_HELP_SERVER_MSG_KEY); + public const string CHAT_HELP_SERVER_MSG_KEY = $"{PREFIX_CHAT_INFO}/help/servermsg"; + public static string CHAT_HELP_WHISPER_MSG => Get(CHAT_HELP_WHISPER_MSG_KEY); + public const string CHAT_HELP_WHISPER_MSG_KEY = $"{PREFIX_CHAT_INFO}/help/whispermsg"; + public static string CHAT_HELP_HELP => Get(CHAT_HELP_HELP_KEY); + public const string CHAT_HELP_HELP_KEY = $"{PREFIX_CHAT_INFO}/help/help"; + public static string CHAT_HELP_MSG => Get(CHAT_HELP_MSG_KEY); + public const string CHAT_HELP_MSG_KEY = $"{PREFIX_CHAT_INFO}/help/msg"; + public static string CHAT_HELP_PLAYER_NAME => Get(CHAT_HELP_PLAYER_NAME_KEY); + public const string CHAT_HELP_PLAYER_NAME_KEY = $"{PREFIX_CHAT_INFO}/help/playername"; + #endregion + + #region Pause Menu + public static string PAUSE_MENU_DISCONNECT => Get(PAUSE_MENU_DISCONNECT_KEY); + public const string PAUSE_MENU_DISCONNECT_KEY = $"{PREFIX_PAUSE_MENU}/disconnect_msg"; + + public static string PAUSE_MENU_QUIT => Get(PAUSE_MENU_QUIT_KEY); + public const string PAUSE_MENU_QUIT_KEY = $"{PREFIX_PAUSE_MENU}/quit_msg"; + #endregion + + private static bool initializeAttempted; + private static ReadOnlyDictionary> csv; + + public static void Load(string localeDir) { - Multiplayer.LogError($"Failed to find locale file at '{path}'! Please make sure it's there."); - return; + initializeAttempted = true; + string path = Path.Combine(localeDir, DEFAULT_LOCALE_FILE); + if (!File.Exists(path)) + { + Multiplayer.LogError($"Failed to find locale file at '{path}'! Please make sure it's there."); + return; + } + + csv = Csv.Parse(File.ReadAllText(path)); + //Multiplayer.LogDebug(() => $"Locale dump: {Csv.Dump(csv)}"); } - csv = Csv.Parse(File.ReadAllText(path)); - Multiplayer.LogDebug(() => $"Locale dump:{Csv.Dump(csv)}"); - } + public static string Get(string key, string overrideLanguage = null) + { + if (!initializeAttempted) + throw new InvalidOperationException("Not initialized"); - public static string Get(string key, string overrideLanguage = null) - { - if (!initializeAttempted) - throw new InvalidOperationException("Not initialized"); + if (csv == null) + return MISSING_TRANSLATION; - if (csv == null) - return MISSING_TRANSLATION; + string locale = overrideLanguage ?? LocalizationManager.CurrentLanguage; + if (!csv.ContainsKey(locale)) + { + if (locale == DEFAULT_LANGUAGE) + { + Multiplayer.LogError($"Failed to find locale language {locale}! Something is broken, this shouldn't happen. Dumping CSV data:"); + Multiplayer.LogError($"\n{Csv.Dump(csv)}"); + return MISSING_TRANSLATION; + } + + locale = DEFAULT_LANGUAGE; + Multiplayer.LogWarning($"Failed to find locale language {locale}"); + } - string locale = overrideLanguage ?? LocalizationManager.CurrentLanguage; - if (!csv.ContainsKey(locale)) - { - if (locale == DEFAULT_LANGUAGE) + Dictionary localeDict = csv[locale]; + string actualKey = key.StartsWith(PREFIX) ? key.Substring(PREFIX.Length) : key; + if (localeDict.TryGetValue(actualKey, out string value)) { - Multiplayer.LogError($"Failed to find locale language {locale}! Something is broken, this shouldn't happen. Dumping CSV data:"); - Multiplayer.LogError($"\n{Csv.Dump(csv)}"); - return MISSING_TRANSLATION; + if (string.IsNullOrEmpty(value)) + return overrideLanguage == null && locale != DEFAULT_LANGUAGE ? Get(actualKey, DEFAULT_LANGUAGE) : MISSING_TRANSLATION; + return value; } - locale = DEFAULT_LANGUAGE; - Multiplayer.LogWarning($"Failed to find locale language {locale}"); + Multiplayer.LogDebug(() => $"Failed to find locale key '{actualKey}'!"); + return MISSING_TRANSLATION; } - Dictionary localeDict = csv[locale]; - string actualKey = key.StartsWith(PREFIX) ? key.Substring(PREFIX.Length) : key; - if (localeDict.TryGetValue(actualKey, out string value)) + public static string Get(string key, params object[] placeholders) { - if (value == string.Empty) - return overrideLanguage == null && locale != DEFAULT_LANGUAGE ? Get(actualKey, DEFAULT_LANGUAGE) : MISSING_TRANSLATION; - return value; + return string.Format(Get(key), placeholders); } - Multiplayer.LogDebug(() => $"Failed to find locale key '{actualKey}'!"); - return MISSING_TRANSLATION; - } - - public static string Get(string key, params object[] placeholders) - { - return string.Format(Get(key), placeholders); - } - - public static string Get(string key, params string[] placeholders) - { - // ReSharper disable once CoVariantArrayConversion - return Get(key, (object[])placeholders); + public static string Get(string key, params string[] placeholders) + { + return Get(key, (object[])placeholders); + } } } diff --git a/Multiplayer/Multiplayer.cs b/Multiplayer/Multiplayer.cs index 87ca8b0..8433096 100644 --- a/Multiplayer/Multiplayer.cs +++ b/Multiplayer/Multiplayer.cs @@ -1,7 +1,12 @@ -using System; +using System; using System.IO; +using System.Linq; +using System.Reflection; +using DV; +using DV.UIFramework; using HarmonyLib; using JetBrains.Annotations; +using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; using Multiplayer.Editor; using Multiplayer.Patches.Mods; @@ -9,6 +14,7 @@ using UnityChan; using UnityEngine; using UnityModManagerNet; +using Steamworks; namespace Multiplayer; @@ -16,19 +22,35 @@ public static class Multiplayer { private const string LOG_FILE = "multiplayer.log"; - private static UnityModManager.ModEntry ModEntry; + public static UnityModManager.ModEntry ModEntry; public static Settings Settings; private static AssetBundle assetBundle; public static AssetIndex AssetIndex { get; private set; } + public static string Ver { + get { + AssemblyInformationalVersionAttribute info = (AssemblyInformationalVersionAttribute)typeof(Multiplayer).Assembly. + GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false) + .FirstOrDefault(); + + if (info == null || Settings.ForceJson) + return ModEntry.Info.Version; + + return info.InformationalVersion.Split('+')[0]; + } + } + + + public static bool specLog = false; [UsedImplicitly] private static bool Load(UnityModManager.ModEntry modEntry) { ModEntry = modEntry; - Settings = Settings.Load(modEntry); + Settings = Settings.Load(modEntry);//Settings.Load(modEntry); ModEntry.OnGUI = Settings.Draw; ModEntry.OnSaveGUI = Settings.Save; + ModEntry.OnLateUpdate = LateUpdate; Harmony harmony = null; @@ -38,6 +60,8 @@ private static bool Load(UnityModManager.ModEntry modEntry) Locale.Load(ModEntry.Path); + Log($"Multiplayer JSON Version: {ModEntry.Info.Version}, Internal Version: {Ver}\r\nGame version: {BuildInfo.BUILD_VERSION_MAJOR.ToString()}\r\nBuilbot version: {BuildInfo.BUILDBOT_INFO.ToString()}"); + Log("Patching..."); harmony = new Harmony(ModEntry.Info.Id); harmony.PatchAll(); @@ -50,6 +74,13 @@ private static bool Load(UnityModManager.ModEntry modEntry) RemoteDispatchPatch.Patch(harmony, remoteDispatch.Assembly); } + //UnityModManager.ModEntry passengerJobs = UnityModManager.FindMod("PassengerJobs"); + //if (passengerJobs?.Enabled == true) + //{ + // Log("Found PassengerJobs, initialising..."); + // PassengerJobsMod.Init(); + //} + if (!LoadAssets()) return false; @@ -99,6 +130,48 @@ public static bool LoadAssets() return true; } + private static void LateUpdate(UnityModManager.ModEntry modEntry, float deltaTime) + { + if (ModEntry.NewestVersion != null && ModEntry.NewestVersion.ToString() != "") + { + Log($"Multiplayer Latest Version: {ModEntry.NewestVersion}"); + + ModEntry.OnLateUpdate -= Multiplayer.LateUpdate; + + if (ModEntry.NewestVersion > ModEntry.Version) + { + if (MainMenuThingsAndStuff.Instance != null) + { + Popup update = MainMenuThingsAndStuff.Instance.ShowOkPopup(); + + if (update == null) + return; + + update.labelTMPro.text = "Multiplayer Mod Update Available!\r\n\r\n"+ + $"Latest version:\t\t{ModEntry.NewestVersion}\r\n" + + $"Installed version:\t\t{ModEntry.Version}\r\n\r\n" + + "Run Unity Mod Manager Installer to apply the update."; + + Vector3 currPos = update.labelTMPro.transform.localPosition; + Vector2 size = update.labelTMPro.rectTransform.sizeDelta; + + float delta = size.y - update.labelTMPro.preferredHeight; + currPos.y -= delta *2 ; + size.y = update.labelTMPro.preferredHeight; + + update.labelTMPro.transform.localPosition = currPos; + update.labelTMPro.rectTransform.sizeDelta = size; + + currPos = update.positiveButton.transform.localPosition; + currPos.y += delta * 2; + update.positiveButton.transform.localPosition = currPos; + + + } + } + } + } + #region Logging public static void LogDebug(Func resolver) @@ -130,7 +203,7 @@ public static void LogException(object msg, Exception e) private static void WriteLog(string msg) { - string str = $"[{DateTime.Now:HH:mm:ss.fff}] {msg}"; + string str = $"[{DateTime.Now.ToUniversalTime():HH:mm:ss.fff}] {msg}"; if (Settings.EnableLogFile) File.AppendAllLines(LOG_FILE, new[] { str }); ModEntry.Logger.Log(str); diff --git a/Multiplayer/Multiplayer.csproj b/Multiplayer/Multiplayer.csproj index 70448c4..537fb85 100644 --- a/Multiplayer/Multiplayer.csproj +++ b/Multiplayer/Multiplayer.csproj @@ -1,19 +1,24 @@ - + net48 latest Multiplayer + 0.1.10.0 - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + - - + @@ -39,21 +44,18 @@ + + - - + - - + - - ../build/LiteNetLib.dll - ../build/MultiplayerEditor.dll @@ -64,30 +66,40 @@ ../build/UnityChan.dll + - - + + + + + + - - - - - + + + + + + + + + + + - - - - - + + + + - + \ No newline at end of file diff --git a/Multiplayer/Networking/Data/BogieData.cs b/Multiplayer/Networking/Data/BogieData.cs deleted file mode 100644 index 33f6851..0000000 --- a/Multiplayer/Networking/Data/BogieData.cs +++ /dev/null @@ -1,54 +0,0 @@ -using LiteNetLib.Utils; -using Multiplayer.Utils; - -namespace Multiplayer.Networking.Data; - -public readonly struct BogieData -{ - private readonly byte PackedBools; - public readonly double PositionAlongTrack; - public readonly ushort TrackNetId; - public readonly int TrackDirection; - - public bool IncludesTrackData => (PackedBools & 1) != 0; - public bool HasDerailed => (PackedBools & 2) != 0; - - private BogieData(byte packedBools, double positionAlongTrack, ushort trackNetId, int trackDirection) - { - PackedBools = packedBools; - PositionAlongTrack = positionAlongTrack; - TrackNetId = trackNetId; - TrackDirection = trackDirection; - } - - public static BogieData FromBogie(Bogie bogie, bool includeTrack, int trackDirection) - { - bool includesTrackData = includeTrack && !bogie.HasDerailed && bogie.track; - return new BogieData( - (byte)((includesTrackData ? 1 : 0) | (bogie.HasDerailed ? 2 : 0)), - bogie.traveller?.Span ?? -1.0, - includesTrackData ? bogie.track.Networked().NetId : (ushort)0, - trackDirection - ); - } - - public static void Serialize(NetDataWriter writer, BogieData data) - { - writer.Put(data.PackedBools); - if (!data.HasDerailed) writer.Put(data.PositionAlongTrack); - if (!data.IncludesTrackData) return; - writer.Put(data.TrackNetId); - writer.Put(data.TrackDirection); - } - - public static BogieData Deserialize(NetDataReader reader) - { - byte packedBools = reader.GetByte(); - bool includesTrackData = (packedBools & 1) != 0; - bool hasDerailed = (packedBools & 2) != 0; - double positionAlongTrack = !hasDerailed ? reader.GetDouble() : -1.0; - ushort trackNetId = includesTrackData ? reader.GetUShort() : (ushort)0; - int trackDirection = includesTrackData ? reader.GetInt() : 0; - return new BogieData(packedBools, positionAlongTrack, trackNetId, trackDirection); - } -} diff --git a/Multiplayer/Networking/Data/ItemPositionData.cs b/Multiplayer/Networking/Data/ItemPositionData.cs new file mode 100644 index 0000000..e8d34e7 --- /dev/null +++ b/Multiplayer/Networking/Data/ItemPositionData.cs @@ -0,0 +1,37 @@ +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Serialization; +using UnityEngine; + +namespace Multiplayer.Networking.Data; + +public struct ItemPositionData +{ + public Vector3 Position; + public Quaternion Rotation; + + public static ItemPositionData FromItem(NetworkedItem item) + { + //Multiplayer.Log($"ItemPositionData.FromItem() Position: {item.Item.transform.position}, Less currentMove: {item.Item.transform.position - WorldMover.currentMove } "); + return new ItemPositionData + { + Position = item.Item.transform.position - WorldMover.currentMove, + Rotation = item.Item.transform.rotation, + }; + } + + public static void Serialize(NetDataWriter writer, ItemPositionData data) + { + Vector3Serializer.Serialize(writer, data.Position); + QuaternionSerializer.Serialize(writer, data.Rotation); + } + + public static ItemPositionData Deserialize(NetDataReader reader) + { + return new ItemPositionData + { + Position = Vector3Serializer.Deserialize(reader), + Rotation = QuaternionSerializer.Deserialize(reader), + }; + } +} diff --git a/Multiplayer/Networking/Data/ItemUpdateData.cs b/Multiplayer/Networking/Data/ItemUpdateData.cs new file mode 100644 index 0000000..3c8e8c4 --- /dev/null +++ b/Multiplayer/Networking/Data/ItemUpdateData.cs @@ -0,0 +1,188 @@ +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Serialization; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Networking.Data; + +public class ItemUpdateData +{ + [Flags] + public enum ItemUpdateType : byte + { + None = 0, + Create = 1, + Destroy = 2, + ItemState = 4, + ItemPosition = 8, + ObjectState = 16, + FullSync = ItemState | ItemPosition | ObjectState, + } + + public ItemUpdateType UpdateType { get; set; } + public ushort ItemNetId { get; set; } + public string PrefabName { get; set; } + public ItemState ItemState { get; set; } + public Vector3 ItemPosition { get; set; } + public Quaternion ItemRotation { get; set; } + public Vector3 ThrowDirection { get; set; } + public byte Player { get; set; } + public ushort CarNetId { get; set; } + public bool AttachedFront { get; set; } + public Dictionary States { get; set; } + + public void Serialize(NetDataWriter writer) + { + writer.Put((byte)UpdateType); + writer.Put(ItemNetId); + + if (UpdateType == ItemUpdateType.Destroy) + return; + + writer.Put((byte)ItemState); + + if (UpdateType.HasFlag(ItemUpdateType.Create)) + writer.Put(PrefabName); + + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.ItemState)) + { + if (ItemState == ItemState.Dropped || ItemState == ItemState.Thrown) // || UpdateType.HasFlag(ItemUpdateType.ItemPosition) + { + Vector3Serializer.Serialize(writer, ItemPosition); + QuaternionSerializer.Serialize(writer, ItemRotation); + + if (ItemState == ItemState.Thrown) + Vector3Serializer.Serialize(writer, ThrowDirection); + } + else if (ItemState == ItemState.InInventory || ItemState == ItemState.InHand) + { + writer.Put(Player); + } + else if (ItemState == ItemState.Attached) + { + writer.Put(CarNetId); + writer.Put(AttachedFront); + } + } + + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.ObjectState)) + { + if (States == null) + writer.Put(0); + else + { + writer.Put(States.Count); + foreach (var state in States) + { + writer.Put(state.Key); + SerializeTrackedValue(writer, state.Value); + } + } + } + } + + public void Deserialize(NetDataReader reader) + { + UpdateType = (ItemUpdateType)reader.GetByte(); + ItemNetId = reader.GetUShort(); + + if (UpdateType == ItemUpdateType.Destroy) + return; + + ItemState = (ItemState)reader.GetByte(); + + if (UpdateType.HasFlag(ItemUpdateType.Create)) + PrefabName = reader.GetString(); + + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.ItemState)) + { + if (ItemState == ItemState.Dropped || ItemState == ItemState.Thrown) // || UpdateType.HasFlag(ItemUpdateType.ItemPosition) + { + ItemPosition = Vector3Serializer.Deserialize(reader); + ItemRotation = QuaternionSerializer.Deserialize(reader); + + if (ItemState == ItemState.Thrown) + { + Multiplayer.LogDebug(() => $"ItemUpdateData.Deserialize() Item Thrown before: {ThrowDirection}"); + ThrowDirection = Vector3Serializer.Deserialize(reader); + Multiplayer.LogDebug(() => $"ItemUpdateData.Deserialize() Item Thrown after: {ThrowDirection}"); + } + } + else if (ItemState == ItemState.InInventory || ItemState == ItemState.InHand) + { + Player = reader.GetByte(); + } + else if (ItemState == ItemState.Attached) + { + CarNetId = reader.GetUShort(); + AttachedFront = reader.GetBool(); + } + } + + if (UpdateType.HasFlag(ItemUpdateType.Create) || UpdateType.HasFlag(ItemUpdateType.ObjectState)) + { + int stateCount = reader.GetInt(); + if (stateCount > 0) + { + States = new Dictionary(); + for (int i = 0; i < stateCount; i++) + { + string key = reader.GetString(); + object value = DeserializeTrackedValue(reader); + States[key] = value; + } + } + } + } + + private void SerializeTrackedValue(NetDataWriter writer, object value) + { + if (value is bool boolValue) + { + writer.Put((byte)0); + writer.Put(boolValue); + } + else if (value is int intValue) + { + writer.Put((byte)1); + writer.Put(intValue); + } + else if (value is uint uintValue) + { + writer.Put((byte)2); + writer.Put(uintValue); + } + else if (value is float floatValue) + { + writer.Put((byte)3); + writer.Put(floatValue); + } + else if (value is string stringValue) + { + writer.Put((byte)4); + writer.Put(stringValue); + } + else + { + throw new NotSupportedException($"ItemUpdateData.SerializeTrackedValue({ItemNetId}, {PrefabName??""}) Unsupported type for serialization: {value.GetType()}"); + } + } + + private object DeserializeTrackedValue(NetDataReader reader) + { + byte typeCode = reader.GetByte(); + switch (typeCode) + { + case 0: return reader.GetBool(); + case 1: return reader.GetInt(); + case 2: return reader.GetUInt(); + case 3: return reader.GetFloat(); + case 4: return reader.GetString(); + + default: + throw new NotSupportedException($"ItemUpdateData.DeserializeTrackedValue({ItemNetId}, {PrefabName ?? ""}) Unsupported type code for deserialization: {typeCode}"); + } + } +} diff --git a/Multiplayer/Networking/Data/JobData.cs b/Multiplayer/Networking/Data/JobData.cs new file mode 100644 index 0000000..1a5b676 --- /dev/null +++ b/Multiplayer/Networking/Data/JobData.cs @@ -0,0 +1,237 @@ +using DV.Logic.Job; +using DV.ThingTypes; +using LiteNetLib.Utils; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Multiplayer.Networking.Data; + +public class JobData +{ + public ushort NetID { get; set; } + public JobType JobType { get; set; } //serialise as byte + public string ID { get; set; } + public TaskNetworkData[] Tasks { get; set; } + public StationsChainNetworkData ChainData { get; set; } + public JobLicenses RequiredLicenses { get; set; } //serialise as int + public float StartTime { get; set; } + public float FinishTime { get; set; } + public float InitialWage { get; set; } + public JobState State { get; set; } //serialise as byte + public float TimeLimit { get; set; } + public ushort ItemNetID { get; set; } + public ItemPositionData ItemPosition { get; set; } + + public static JobData FromJob(NetworkedStationController netStation, NetworkedJob networkedJob) + { + Job job = networkedJob.Job; + + ushort itemNetId = 0; + ItemPositionData itemPos = new(); + + //Multiplayer.Log($"JobData.FromJob({netStation.name}, {job.ID}, {networkedJob.Job.State})"); + + if (networkedJob.Job.State == JobState.Available) + { + if (networkedJob.JobOverview != null) + { + itemNetId = networkedJob.JobOverview.NetId; + itemPos = ItemPositionData.FromItem(networkedJob.JobOverview); + } + } + else if (job.State == JobState.InProgress) + { + if (networkedJob.JobBooklet != null) + { + itemNetId = networkedJob.JobBooklet.NetId; + itemPos = ItemPositionData.FromItem(networkedJob.JobBooklet); + } + } + else if(job.State == JobState.Completed) + { + if (networkedJob.JobReport != null) + { + itemNetId = networkedJob.JobReport.NetId; + itemPos = ItemPositionData.FromItem(networkedJob.JobReport); + } + } + + return new JobData + { + NetID = networkedJob.NetId, + JobType = job.jobType, + ID = job.ID, + Tasks = TaskNetworkDataFactory.ConvertTasks(job.tasks), + ChainData = StationsChainNetworkData.FromStationData(job.chainData), + RequiredLicenses = job.requiredLicenses, + StartTime = job.startTime, + FinishTime = job.finishTime, + InitialWage = job.initialWage, + State = job.State, + TimeLimit = job.TimeLimit, + ItemNetID = itemNetId, + ItemPosition = itemPos, + }; + } + + public static void Serialize(NetDataWriter writer, JobData data) + { + //NetworkLifecycle.Instance.Server.Log($"JobData.Serialize({data.ID}) NetID {data.NetID}"); + + writer.Put(data.NetID); + writer.Put((byte)data.JobType); + writer.Put(data.ID); + + //task data - add compression + using (MemoryStream ms = new()) + using (BinaryWriter bw = new(ms)) + { + bw.Write((byte)data.Tasks.Length); + foreach (var task in data.Tasks) + { + NetDataWriter taskSerialiser = new NetDataWriter(); + + bw.Write((byte)task.TaskType); + task.Serialize(taskSerialiser); + + bw.Write(taskSerialiser.Data.Length); + bw.Write(taskSerialiser.Data); + } + + byte[] compressedData = PacketCompression.Compress(ms.ToArray()); + + // Multiplayer.Log($"JobData.Serialize() Uncompressed: {ms.Length} Compressed: {compressedData.Length}"); + writer.PutBytesWithLength(compressedData); + } + + StationsChainNetworkData.Serialize(writer, data.ChainData); + + writer.Put((int)data.RequiredLicenses); + writer.Put(data.StartTime); + writer.Put(data.FinishTime); + writer.Put(data.InitialWage); + writer.Put((byte)data.State); + writer.Put(data.TimeLimit); + writer.Put(data.ItemNetID); + ItemPositionData.Serialize(writer, data.ItemPosition); + } + + public static JobData Deserialize(NetDataReader reader) + { + try + { + + ushort netID = reader.GetUShort(); + JobType jobType = (JobType)reader.GetByte(); + string id = reader.GetString(); + + //Decompress task data + byte[] compressedData = reader.GetBytesWithLength(); + byte[] decompressedData = PacketCompression.Decompress(compressedData); + + //Multiplayer.Log($"JobData.Deserialize() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); + + TaskNetworkData[] tasks; + + using (MemoryStream ms = new MemoryStream(decompressedData)) + using (BinaryReader br = new BinaryReader(ms)) + { + byte tasksLength = br.ReadByte(); + tasks = new TaskNetworkData[tasksLength]; + + for (int i = 0; i < tasksLength; i++) + { + TaskType taskType = (TaskType)br.ReadByte(); + + int taskLength = br.ReadInt32(); + NetDataReader taskReader = new NetDataReader(br.ReadBytes(taskLength)); + + tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); + tasks[i].Deserialize(taskReader); + } + } + + StationsChainNetworkData chainData = StationsChainNetworkData.Deserialize(reader); + + JobLicenses requiredLicenses = (JobLicenses)reader.GetInt(); + float startTime = reader.GetFloat(); + float finishTime = reader.GetFloat(); + float initialWage = reader.GetFloat(); + JobState state = (JobState)reader.GetByte(); + float timeLimit = reader.GetFloat(); + ushort itemNetId = reader.GetUShort(); + ItemPositionData itemPositionData = ItemPositionData.Deserialize(reader); + + return new JobData + { + NetID = netID, + JobType = jobType, + ID = id, + Tasks = tasks, + ChainData = chainData, + RequiredLicenses = requiredLicenses, + StartTime = startTime, + FinishTime = finishTime, + InitialWage = initialWage, + State = state, + TimeLimit = timeLimit, + ItemNetID = itemNetId, + ItemPosition = itemPositionData + }; + } + catch (Exception ex) + { + Multiplayer.Log($"JobData.Deserialize() Failed! {ex.Message}\r\n{ex.StackTrace}"); + return null; + } + } + + public List GetCars() + { + List result = []; + + foreach (var task in Tasks) + { + var cars = task.GetCars(); + result.AddRange(cars); + } + + return result; + } + +} + +public struct StationsChainNetworkData +{ + public string ChainOriginYardId { get; set; } + public string ChainDestinationYardId { get; set; } + + public static StationsChainNetworkData FromStationData(StationsChainData data) + { + return new StationsChainNetworkData + { + ChainOriginYardId = data.chainOriginYardId, + ChainDestinationYardId = data.chainDestinationYardId + }; + } + + public static void Serialize(NetDataWriter writer, StationsChainNetworkData data) + { + writer.Put(data.ChainOriginYardId); + writer.Put(data.ChainDestinationYardId); + } + + public static StationsChainNetworkData Deserialize(NetDataReader reader) + { + return new StationsChainNetworkData + { + ChainOriginYardId = reader.GetString(), + ChainDestinationYardId = reader.GetString() + }; + } +} diff --git a/Multiplayer/Networking/Data/JobUpdateData.cs b/Multiplayer/Networking/Data/JobUpdateData.cs new file mode 100644 index 0000000..c71c56d --- /dev/null +++ b/Multiplayer/Networking/Data/JobUpdateData.cs @@ -0,0 +1,48 @@ +using DV.ThingTypes; +using LiteNetLib.Utils; +namespace Multiplayer.Networking.Data; + +public struct JobUpdateStruct : INetSerializable +{ + public ushort JobNetID; + public bool Invalid; + public JobState JobState; + public float StartTime; + public float FinishTime; + public ushort ItemNetID; + public ushort ValidationStationId; + public ItemPositionData ItemPositionData; + + public readonly void Serialize(NetDataWriter writer) + { + writer.Put(JobNetID); + writer.Put(Invalid); + + //Invalid jobs will be deleted / deregistered + if (Invalid) + return; + + writer.Put((byte)JobState); + writer.Put(StartTime); + writer.Put(FinishTime); + writer.Put(ItemNetID); + writer.Put(ValidationStationId); + ItemPositionData.Serialize(writer,ItemPositionData); + } + + public void Deserialize(NetDataReader reader) + { + JobNetID = reader.GetUShort(); + Invalid = reader.GetBool(); + + if (Invalid) + return; + + JobState = (JobState)reader.GetByte(); + StartTime = reader.GetFloat(); + FinishTime = reader.GetFloat(); + ItemNetID = reader.GetUShort(); + ValidationStationId = reader.GetUShort(); + ItemPositionData = ItemPositionData.Deserialize(reader); + } +} diff --git a/Multiplayer/Networking/Data/JobValidationData.cs b/Multiplayer/Networking/Data/JobValidationData.cs new file mode 100644 index 0000000..f2b59ec --- /dev/null +++ b/Multiplayer/Networking/Data/JobValidationData.cs @@ -0,0 +1,8 @@ + +namespace Multiplayer.Networking.Data; + +public enum ValidationType : byte +{ + JobOverview, + JobBooklet +} diff --git a/Multiplayer/Networking/Data/LobbyServerData.cs b/Multiplayer/Networking/Data/LobbyServerData.cs new file mode 100644 index 0000000..3c8317f --- /dev/null +++ b/Multiplayer/Networking/Data/LobbyServerData.cs @@ -0,0 +1,177 @@ +using LiteNetLib.Utils; +using Multiplayer.Components.MainMenu; +using Newtonsoft.Json; + +namespace Multiplayer.Networking.Data +{ + public class LobbyServerData : IServerBrowserGameDetails + { + [JsonProperty("game_server_id")] + public string id { get; set; } + + public string ipv4 { get; set; } + public string ipv6 { get; set; } + public int port { get; set; } + + [JsonIgnore] + public string LocalIPv4 { get; set; } + [JsonIgnore] + public string LocalIPv6 { get; set; } + + [JsonProperty("server_name")] + public string Name { get; set; } + + + [JsonProperty("password_protected")] + public bool HasPassword { get; set; } + + + [JsonProperty("game_mode")] + public int GameMode { get; set; } + + + [JsonProperty("difficulty")] + public int Difficulty { get; set; } + + + [JsonProperty("time_passed")] + public string TimePassed { get; set; } + + + [JsonProperty("current_players")] + public int CurrentPlayers { get; set; } + + + [JsonProperty("max_players")] + public int MaxPlayers { get; set; } + + + [JsonProperty("required_mods")] + public string RequiredMods { get; set; } + + + [JsonProperty("game_version")] + public string GameVersion { get; set; } + + + [JsonProperty("multiplayer_version")] + public string MultiplayerVersion { get; set; } + + + [JsonProperty("server_info")] + public string ServerDetails { get; set; } + + [JsonIgnore] + public int Ping { get; set; } = -1; + [JsonIgnore] + public bool isPublic { get; set; } + [JsonIgnore] + public int LastSeen { get; set; } = int.MaxValue; + + + public void Dispose() { } + public static int GetDifficultyFromString(string difficulty) + { + int diff = 0; + + switch (difficulty) + { + case "Standard": + diff = 0; + break; + case "Comfort": + diff = 1; + break; + case "Realistic": + diff = 2; + break; + default: + diff = 3; + break; + } + return diff; + } + + public static string GetDifficultyFromInt(int difficulty) + { + string diff = "Standard"; + + switch (difficulty) + { + case 0: + diff = "Standard"; + break; + case 1: + diff = "Comfort"; + break; + case 2: + diff = "Realistic"; + break; + default: + diff = "Custom"; + break; + } + return diff; + } + + public static int GetGameModeFromString(string difficulty) + { + int diff = 0; + + switch (difficulty) + { + case "Career": + diff = 0; + break; + case "Sandbox": + diff = 1; + break; + case "Scenario": + diff = 2; + break; + } + return diff; + } + + public static string GetGameModeFromInt(int difficulty) + { + string diff = "Career"; + + switch (difficulty) + { + case 0: + diff = "Career"; + break; + case 1: + diff = "Sandbox"; + break; + case 2: + diff = "Scenario"; + break; + } + return diff; + } + + public static void Serialize(NetDataWriter writer, LobbyServerData data) + { + //Multiplayer.Log($"LobbyServerData.Serialize() {writer != null }, {data != null} "); + + //have we got data? + writer.Put(data != null); + + if (data != null) + writer.Put(new NetSerializer().Serialize(data)); + + //Multiplayer.Log($"LobbyServerData.Serialize() {writer != null}, {data != null} POST"); + + } + + public static LobbyServerData Deserialize(NetDataReader reader) + { + if(reader.GetBool()) + return new NetSerializer().Deserialize(reader); + else + return null; + } + } +} diff --git a/Multiplayer/Networking/Data/LobbyServerResponseData.cs b/Multiplayer/Networking/Data/LobbyServerResponseData.cs new file mode 100644 index 0000000..4990298 --- /dev/null +++ b/Multiplayer/Networking/Data/LobbyServerResponseData.cs @@ -0,0 +1,15 @@ +namespace Multiplayer.Networking.Data +{ + public class LobbyServerResponseData + { + + public string game_server_id { get; set; } + public string private_key { get; set; } + + public LobbyServerResponseData(string game_server_id, string private_key, bool? ipv4_request = null) + { + this.game_server_id = game_server_id; + this.private_key = private_key; + } + } +} diff --git a/Multiplayer/Networking/Data/LobbyServerUpdateData.cs b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs new file mode 100644 index 0000000..3e632ac --- /dev/null +++ b/Multiplayer/Networking/Data/LobbyServerUpdateData.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; + +namespace Multiplayer.Networking.Data +{ + public class LobbyServerUpdateData + { + public string game_server_id { get; set; } + + public string private_key { get; set; } + + [JsonProperty("time_passed")] + public string TimePassed { get; set; } + + + [JsonProperty("current_players")] + public int CurrentPlayers { get; set; } + + public LobbyServerUpdateData(string game_server_id, string private_key, string timePassed,int currentPlayers, string ipv4 = null) + { + this.game_server_id = game_server_id; + this.private_key = private_key; + this.TimePassed = timePassed; + this.CurrentPlayers = currentPlayers; + } + + + + } +} diff --git a/Multiplayer/Networking/Data/ModInfo.cs b/Multiplayer/Networking/Data/ModInfo.cs index 323884e..31ccf1d 100644 --- a/Multiplayer/Networking/Data/ModInfo.cs +++ b/Multiplayer/Networking/Data/ModInfo.cs @@ -11,8 +11,8 @@ public readonly struct ModInfo { public readonly string Id; public readonly string Version; - - private ModInfo(string id, string version) + + public ModInfo(string id, string version) { Id = id; Version = version; @@ -37,6 +37,7 @@ public static ModInfo Deserialize(NetDataReader reader) public static ModInfo[] FromModEntries(IEnumerable modEntries) { return modEntries + .Where(entry => entry.Enabled) //We only care if it's enabled .OrderBy(entry => entry.Info.Id) .Select(entry => new ModInfo(entry.Info.Id, entry.Info.Version)) .ToArray(); diff --git a/Multiplayer/Networking/Data/ServerPlayer.cs b/Multiplayer/Networking/Data/ServerPlayer.cs index 4e367e5..72c4d7e 100644 --- a/Multiplayer/Networking/Data/ServerPlayer.cs +++ b/Multiplayer/Networking/Data/ServerPlayer.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.World; using UnityEngine; namespace Multiplayer.Networking.Data; @@ -9,22 +12,134 @@ public class ServerPlayer public byte Id { get; set; } public bool IsLoaded { get; set; } public string Username { get; set; } + public string OriginalUsername { get; set; } public Guid Guid { get; set; } public Vector3 RawPosition { get; set; } public float RawRotationY { get; set; } public ushort CarId { get; set; } - public Vector3 AbsoluteWorldPosition => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) - ? RawPosition - : car.transform.TransformPoint(RawPosition) - WorldMover.currentMove; + public Dictionary KnownItems { get; private set; } = new Dictionary(); //NetworkedItem, last updated tick + public Dictionary NearbyItems { get; private set; } = new Dictionary(); //NetworkedItem, time since near the item + public HashSet OwnedItems { get; private set; } = new HashSet(); + public StorageBase Storage { get; set; } = new StorageBase(); - public Vector3 WorldPosition => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) - ? RawPosition + WorldMover.currentMove - : car.transform.TransformPoint(RawPosition); + private Vector3 _lastWorldPos = Vector3.zero; + private Vector3 _lastAbsoluteWorldPosition = Vector3.zero; + #region Positioning + public Vector3 AbsoluteWorldPosition + { + get + { + + Vector3 pos; + try + { + if (CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car)) + { + if (CarId != 0) + Multiplayer.LogDebug(() => $"AbsoluteWorldPosition() noID {Username}: CarId: {CarId}"); + + pos = RawPosition; + } + else + { + //Multiplayer.LogDebug(() => $"AbsoluteWorldPosition() hasID {Username}: CarId: {CarId}"); + pos = car.transform.TransformPoint(RawPosition) - WorldMover.currentMove; ; + } + + _lastAbsoluteWorldPosition = pos; + } + catch (Exception e) + { + Multiplayer.LogWarning($"AbsoluteWorldPosition() Exception {Username}"); + Multiplayer.LogWarning(e.Message); + Multiplayer.LogWarning(e.StackTrace); + pos = _lastAbsoluteWorldPosition; + } + + return pos; + + } + } + + public Vector3 WorldPosition { + get + { + Vector3 pos; + try + { + if (CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car)) + { + if(CarId != 0) + Multiplayer.LogDebug(() =>$"WorldPosition() noID {Username}: CarId: {CarId}"); + + pos = RawPosition + WorldMover.currentMove; + } + else + { + //Multiplayer.LogDebug(() => $"WorldPosition() hasID {Username}: CarId: {CarId}"); + pos = car.transform.TransformPoint(RawPosition); + } + + _lastWorldPos = pos; + } + catch (Exception e) + { + Multiplayer.LogWarning($"WorldPosition() Exception {Username}"); + Multiplayer.LogWarning(e.Message); + Multiplayer.LogWarning(e.StackTrace); + + pos = _lastWorldPos; + } + + return pos; + } + } public float WorldRotationY => CarId == 0 || !NetworkedTrainCar.Get(CarId, out NetworkedTrainCar car) ? RawRotationY : (Quaternion.Euler(0, RawRotationY, 0) * car.transform.rotation).eulerAngles.y; + #endregion + + #region Item Ownership + public bool OwnsItem(ushort itemNetId) => OwnedItems.Contains(itemNetId); + + public void AddOwnedItem(ushort itemNetId) + { + OwnedItems.Add(itemNetId); + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player {Username} now owns item {itemNetId}"); + } + + public void AddOwnedItems(IEnumerable itemNetIds) + { + OwnedItems.UnionWith(itemNetIds); + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player {Username} batch added items: {string.Join(", ", itemNetIds)}"); + } + + public void RemoveOwnedItem(ushort itemNetId) + { + if (OwnedItems.Remove(itemNetId)) + { + NetworkLifecycle.Instance.Server.LogDebug(() => $"Player {Username} no longer owns item {itemNetId}"); + } + } + + public void ClearOwnedItems() + { + OwnedItems.Clear(); + NetworkLifecycle.Instance.Server.LogDebug(() => $"Cleared all owned items for player {Username}"); + } + + public bool TryGetOwnedItem(ushort itemNetId, out NetworkedItem item) + { + if (OwnedItems.Contains(itemNetId) && NetworkedItem.TryGet(itemNetId, out item)) + { + return true; + } + item = null; + return false; + } + #endregion public override string ToString() { diff --git a/Multiplayer/Networking/Data/TaskNetworkData.cs b/Multiplayer/Networking/Data/TaskNetworkData.cs new file mode 100644 index 0000000..37caf8c --- /dev/null +++ b/Multiplayer/Networking/Data/TaskNetworkData.cs @@ -0,0 +1,474 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using DV.Logic.Job; +using DV.ThingTypes; +using LiteNetLib.Utils; +using Multiplayer.Components.Networking.Train; + +namespace Multiplayer.Networking.Data; + +#region TaskData Base Class +public abstract class TaskNetworkData +{ + public TaskState State { get; set; } + public float TaskStartTime { get; set; } + public float TaskFinishTime { get; set; } + public bool IsLastTask { get; set; } + public float TimeLimit { get; set; } + public TaskType TaskType { get; set; } + + public abstract void Serialize(NetDataWriter writer); + public abstract void Deserialize(NetDataReader reader); + public abstract Task ToTask(); + public abstract List GetCars(); +} +public abstract class TaskNetworkData : TaskNetworkData where T : TaskNetworkData +{ + public abstract T FromTask(Task task); + + protected void SerializeCommon(NetDataWriter writer) + { + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() State {(byte)State}, {State}"); + writer.Put((byte)State); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskStartTime {TaskStartTime}"); + writer.Put(TaskStartTime); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskFinishTime {TaskFinishTime}"); + writer.Put(TaskFinishTime); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() IsLastTask {IsLastTask}"); + writer.Put(IsLastTask); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TimeLimit {TimeLimit}"); + writer.Put(TimeLimit); + //Multiplayer.Log($"TaskNetworkData.SerializeCommon() TaskType {(byte)TaskType}, {TaskType}"); + writer.Put((byte)TaskType); + } + + protected void DeserializeCommon(NetDataReader reader) + { + State = (TaskState)reader.GetByte(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() State {State}"); + TaskStartTime = reader.GetFloat(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskStartTime {TaskStartTime}"); + TaskFinishTime = reader.GetFloat(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskFinishTime {TaskFinishTime}"); + IsLastTask = reader.GetBool(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() IsLastTask {IsLastTask}"); + TimeLimit = reader.GetFloat(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TimeLimit {TimeLimit}"); + TaskType = (TaskType)reader.GetByte(); + //Multiplayer.Log($"TaskNetworkData.DeserializeCommon() TaskType {TaskType}"); + } +} + +#endregion + +#region Extension of TaskTypes +public static class TaskNetworkDataFactory +{ + private static readonly Dictionary> TypeToTaskNetworkData = []; + private static readonly Dictionary> EnumToEmptyTaskNetworkData = []; + + public static void RegisterTaskType(TaskType taskType, Func converter, Func emptyCreator) + where TGameTask : Task + { + TypeToTaskNetworkData[typeof(TGameTask)] = task => converter((TGameTask)task); + EnumToEmptyTaskNetworkData[taskType] = emptyCreator; + } + + public static TaskNetworkData ConvertTask(Task task) + { + //Multiplayer.LogDebug(()=>$"TaskNetworkDataFactory.ConvertTask: Processing task of type {task.GetType()}"); + if (TypeToTaskNetworkData.TryGetValue(task.GetType(), out var converter)) + { + return converter(task); + } + throw new ArgumentException($"Unknown task type: {task.GetType()}"); + } + + public static TaskNetworkData[] ConvertTasks(IEnumerable tasks) + { + return tasks.Select(ConvertTask).ToArray(); + } + + public static TaskNetworkData ConvertTask(TaskType type) + { + if (EnumToEmptyTaskNetworkData.TryGetValue(type, out var creator)) + { + return creator(type); + } + throw new ArgumentException($"Unknown task type: {type}"); + } + + // Register base task types + static TaskNetworkDataFactory() + { + RegisterTaskType( + TaskType.Warehouse, + task => new WarehouseTaskData { TaskType = TaskType.Warehouse }.FromTask(task), + type => new WarehouseTaskData { TaskType = type } + ); + RegisterTaskType( + TaskType.Transport, + task => new TransportTaskData { TaskType = TaskType.Transport }.FromTask(task), + type => new TransportTaskData { TaskType = type } + ); + RegisterTaskType( + TaskType.Sequential, + task => new SequentialTasksData { TaskType = TaskType.Sequential }.FromTask(task), + type => new SequentialTasksData { TaskType = type } + ); + RegisterTaskType( + TaskType.Parallel, + task => new ParallelTasksData { TaskType = TaskType.Parallel }.FromTask(task), + type => new ParallelTasksData { TaskType = type } + ); + } +} +#endregion + +public class WarehouseTaskData : TaskNetworkData +{ + public ushort[] CarNetIDs { get; set; } + public WarehouseTaskType WarehouseTaskType { get; set; } + public string WarehouseMachine { get; set; } + public CargoType CargoType { get; set; } + public float CargoAmount { get; set; } + public bool ReadyForMachine { get; set; } + + public override void Serialize(NetDataWriter writer) + { + SerializeCommon(writer); + writer.PutArray(CarNetIDs); + writer.Put((byte)WarehouseTaskType); + writer.Put(WarehouseMachine); + writer.Put((int)CargoType); + writer.Put(CargoAmount); + writer.Put(ReadyForMachine); + } + + public override void Deserialize(NetDataReader reader) + { + DeserializeCommon(reader); + CarNetIDs = reader.GetUShortArray(); + WarehouseTaskType = (WarehouseTaskType)reader.GetByte(); + WarehouseMachine = reader.GetString(); + CargoType = (CargoType)reader.GetInt(); + CargoAmount = reader.GetFloat(); + ReadyForMachine = reader.GetBool(); + } + + public override WarehouseTaskData FromTask(Task task) + { + if (task is not WarehouseTask warehouseTask) + throw new ArgumentException("Task is not a WarehouseTask"); + + CarNetIDs = warehouseTask.cars + .Select(car => NetworkedTrainCar.GetFromTrainId(car.ID, out var networkedTrainCar) + ? networkedTrainCar.NetId + : (ushort)0) + .ToArray(); + WarehouseTaskType = warehouseTask.warehouseTaskType; + WarehouseMachine = warehouseTask.warehouseMachine.ID; + CargoType = warehouseTask.cargoType; + CargoAmount = warehouseTask.cargoAmount; + ReadyForMachine = warehouseTask.readyForMachine; + + return this; + } + + public override Task ToTask() + { + + List cars = CarNetIDs + .Select(netId => NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar) ? trainCar : null) + .Where(car => car != null) + .Select(car =>car.logicCar) + .ToList(); + + WarehouseTask newWareTask = new WarehouseTask( + cars, + WarehouseTaskType, + JobSaveManager.Instance.GetWarehouseMachineWithId(WarehouseMachine), + CargoType, + CargoAmount + ); + + newWareTask.readyForMachine = ReadyForMachine; + + return newWareTask; + } + + public override List GetCars() + { + return CarNetIDs.ToList(); + } +} + +public class TransportTaskData : TaskNetworkData +{ + public ushort[] CarNetIDs { get; set; } + public string StartingTrack { get; set; } + public string DestinationTrack { get; set; } + public CargoType[] TransportedCargoPerCar { get; set; } + public bool CouplingRequiredAndNotDone { get; set; } + public bool AnyHandbrakeRequiredAndNotDone { get; set; } + + public override void Serialize(NetDataWriter writer) + { + SerializeCommon(writer); + //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); + writer.PutArray(CarNetIDs); + + //Multiplayer.LogDebug(() => $"TransportTaskData.Serialize() raw after: [{string.Join(", ", writer.Data?.Select(id => id.ToString()))}]"); + + //Multiplayer.Log($"TaskNetworkData.Serialize() StartingTrack {StartingTrack}"); + writer.Put(StartingTrack); + //Multiplayer.Log($"TaskNetworkData.Serialize() DestinationTrack {DestinationTrack}"); + writer.Put(DestinationTrack); + + //Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar != null {TransportedCargoPerCar != null}"); + writer.Put(TransportedCargoPerCar != null); + + if (TransportedCargoPerCar != null) + { + //Multiplayer.Log($"TaskNetworkData.Serialize() TransportedCargoPerCar.PutArray() length: {TransportedCargoPerCar.Length}"); + writer.PutArray(TransportedCargoPerCar.Select(x => (int)x).ToArray()); + } + + //Multiplayer.Log($"TaskNetworkData.Serialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); + writer.Put(CouplingRequiredAndNotDone); + //Multiplayer.Log($"TaskNetworkData.Serialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); + writer.Put(AnyHandbrakeRequiredAndNotDone); + } + + public override void Deserialize(NetDataReader reader) + { + DeserializeCommon(reader); + + CarNetIDs = reader.GetUShortArray(); + + //Multiplayer.LogDebug(() => $"TransportTaskData.Deserialize() CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs?.Select(id => id.ToString()))}]"); + + StartingTrack = reader.GetString(); + //Multiplayer.Log($"TaskNetworkData.Deserialize() StartingTrack {StartingTrack}"); + DestinationTrack = reader.GetString(); + //Multiplayer.Log($"TaskNetworkData.Deserialize() DestinationTrack {DestinationTrack}"); + + if (reader.GetBool()) + { + //Multiplayer.Log($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null True"); + TransportedCargoPerCar = reader.GetIntArray().Select(x => (CargoType)x).ToArray(); + } + //else + //{ + // Multiplayer.LogWarning($"TaskNetworkData.Deserialize() TransportedCargoPerCar != null False"); + //} + CouplingRequiredAndNotDone = reader.GetBool(); + //Multiplayer.Log($"TaskNetworkData.Deserialize() CouplingRequiredAndNotDone {CouplingRequiredAndNotDone}"); + AnyHandbrakeRequiredAndNotDone = reader.GetBool(); + //Multiplayer.Log($"TaskNetworkData.Deserialize() AnyHandbrakeRequiredAndNotDone {AnyHandbrakeRequiredAndNotDone}"); + } + + public override TransportTaskData FromTask(Task task) + { + if (task is not TransportTask transportTask) + throw new ArgumentException("Task is not a TransportTask"); + + //Multiplayer.LogDebug(() => $"TransportTaskData.FromTask() CarNetIDs count: {transportTask.cars.Count()}, Values: [{string.Join(", ", transportTask.cars.Select(car => car.ID))}]"); + CarNetIDs = transportTask.cars + .Select(car => NetworkedTrainCar.GetFromTrainId(car.ID, out var networkedTrainCar) + ? networkedTrainCar.NetId + : (ushort)0) + .ToArray(); + + //Multiplayer.LogDebug(() => $"TransportTaskData.FromTask() after CarNetIDs count: {CarNetIDs.Length}, Values: [{string.Join(", ", CarNetIDs.Select(id => id.ToString()))}]"); + + StartingTrack = transportTask.startingTrack.ID.RailTrackGameObjectID; + DestinationTrack = transportTask.destinationTrack.ID.RailTrackGameObjectID; + TransportedCargoPerCar = transportTask.transportedCargoPerCar?.ToArray(); + CouplingRequiredAndNotDone = transportTask.couplingRequiredAndNotDone; + AnyHandbrakeRequiredAndNotDone = transportTask.anyHandbrakeRequiredAndNotDone; + + return this; + } + + public override Task ToTask() + { + //Multiplayer.LogDebug(() => $"TransportTaskData.ToTask() CarNetIDs !null {CarNetIDs != null}, count: {CarNetIDs?.Length}"); + + List cars = CarNetIDs + .Select(netId => NetworkedTrainCar.GetTrainCar(netId, out TrainCar trainCar) ? trainCar.logicCar : null) + .Where(car => car != null) + .ToList(); + + return new TransportTask( + cars, + RailTrackRegistry.Instance.GetTrackWithName(DestinationTrack).logicTrack, + RailTrackRegistry.Instance.GetTrackWithName(StartingTrack).logicTrack, + TransportedCargoPerCar?.ToList() + ); + } + + public override List GetCars() + { + return CarNetIDs.ToList(); + } +} + +public class SequentialTasksData : TaskNetworkData +{ + public TaskNetworkData[] Tasks { get; set; } + public byte CurrentTaskIndex { get; set; } + + public override void Serialize(NetDataWriter writer) + { + //Multiplayer.Log($"SequentialTasksData.Serialize({writer != null})"); + + SerializeCommon(writer); + + //Multiplayer.Log($"SequentialTasksData.Serialize() {Tasks.Length}"); + + writer.Put((byte)Tasks.Length); + foreach (var task in Tasks) + { + //Multiplayer.Log($"SequentialTasksData.Serialize() {task.TaskType} {task.GetType()}"); + writer.Put((byte)task.TaskType); + task.Serialize(writer); + } + + writer.Put(CurrentTaskIndex); + } + + public override void Deserialize(NetDataReader reader) + { + DeserializeCommon(reader); + var tasksLength = reader.GetByte(); + Tasks = new TaskNetworkData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + { + var taskType = (TaskType)reader.GetByte(); + Tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); + Tasks[i].Deserialize(reader); + } + + CurrentTaskIndex = reader.GetByte(); + + } + + public override SequentialTasksData FromTask(Task task) + { + if (task is not SequentialTasks sequentialTasks) + throw new ArgumentException("Task is not a SequentialTasks"); + + //Multiplayer.Log($"SequentialTasksData.FromTask() {sequentialTasks.tasks.Count}"); + + Tasks = TaskNetworkDataFactory.ConvertTasks(sequentialTasks.tasks); + + bool found=false; + + CurrentTaskIndex = 0; + foreach(Task subTask in sequentialTasks.tasks) + { + if(subTask == sequentialTasks.currentTask.Value) + { + found = true; + break; + } + CurrentTaskIndex++; + } + + if (!found) + CurrentTaskIndex = byte.MaxValue; + + return this; + } + + public override Task ToTask() + { + List tasks = []; + + foreach (var task in Tasks) + { + //Multiplayer.LogDebug(() => $"SequentialTask.ToTask() task not null: {task != null}"); + + tasks.Add(task.ToTask()); + } + + SequentialTasks newSeqTask = new SequentialTasks(Tasks.Select(t => t.ToTask()).ToList()); + + if(CurrentTaskIndex <= newSeqTask.tasks.Count()) + newSeqTask.currentTask = new LinkedListNode(newSeqTask.tasks.ToArray()[CurrentTaskIndex]); + + return newSeqTask; + } + + public override List GetCars() + { + List result = []; + + foreach (var task in Tasks) + { + var cars = task.GetCars(); + result.AddRange(cars); + } + + return result; + } +} + +public class ParallelTasksData : TaskNetworkData +{ + public TaskNetworkData[] Tasks { get; set; } + + public override void Serialize(NetDataWriter writer) + { + SerializeCommon(writer); + writer.Put((byte)Tasks.Length); + foreach (var task in Tasks) + { + writer.Put((byte)task.TaskType); + task.Serialize(writer); + } + } + + public override void Deserialize(NetDataReader reader) + { + DeserializeCommon(reader); + var tasksLength = reader.GetByte(); + Tasks = new TaskNetworkData[tasksLength]; + for (int i = 0; i < tasksLength; i++) + { + var taskType = (TaskType)reader.GetByte(); + Tasks[i] = TaskNetworkDataFactory.ConvertTask(taskType); + Tasks[i].Deserialize(reader); + } + } + + public override ParallelTasksData FromTask(Task task) + { + if (task is not ParallelTasks parallelTasks) + throw new ArgumentException("Task is not a ParallelTasks"); + + Tasks = TaskNetworkDataFactory.ConvertTasks(parallelTasks.tasks); + + return this; + } + + public override Task ToTask() + { + return new ParallelTasks(Tasks.Select(t => t.ToTask()).ToList()); + } + + public override List GetCars() + { + List result = []; + + foreach(var task in Tasks) + { + var cars = task.GetCars(); + result.AddRange(cars); + } + + return result; + } +} diff --git a/Multiplayer/Networking/Data/TrackedValue.cs b/Multiplayer/Networking/Data/TrackedValue.cs new file mode 100644 index 0000000..02db9d3 --- /dev/null +++ b/Multiplayer/Networking/Data/TrackedValue.cs @@ -0,0 +1,71 @@ +using Multiplayer.Components.Networking; +using System; +using System.Collections.Generic; + +namespace Multiplayer.Networking.Data; + +public class TrackedValue +{ + private T lastSentValue; + private Func valueGetter; + private Action valueSetter; + private Func thresholdComparer; + private bool serverAuthoritative; + public string Key { get; } + + public TrackedValue(string key, Func valueGetter, Action valueSetter, Func thresholdComparer = null, bool serverAuthoritative = false) + { + Key = key; + this.valueGetter = valueGetter; + this.valueSetter = valueSetter; + + this.thresholdComparer = thresholdComparer ?? DefaultComparer; + this.serverAuthoritative = serverAuthoritative; + + lastSentValue = valueGetter(); + } + + public bool IsDirty => thresholdComparer(CurrentValue, lastSentValue); + + public bool ServerAuthoritative => serverAuthoritative; + + public T CurrentValue + { + get => valueGetter(); + set + { + valueSetter(value); + lastSentValue = value; + } + } + + public void MarkClean() + { + lastSentValue = CurrentValue; + } + + public object GetValueAsObject() => CurrentValue; + + public void SetValueFromObject(object value) + { + if (value is T typedValue) + { + CurrentValue = typedValue; + } + else + { + throw new ArgumentException($"Value type mismatch. Expected {typeof(T)}, got {value.GetType()}"); + } + } + + private bool DefaultComparer(T current, T last) + { + return !current.Equals(last); + } + + public string GetDebugString() + { + return $"{Key}: {lastSentValue} -> {CurrentValue}"; + } + +} diff --git a/Multiplayer/Networking/Data/Train/BogieData.cs b/Multiplayer/Networking/Data/Train/BogieData.cs new file mode 100644 index 0000000..a4670ae --- /dev/null +++ b/Multiplayer/Networking/Data/Train/BogieData.cs @@ -0,0 +1,81 @@ +using LiteNetLib.Utils; +using Multiplayer.Utils; +using System; + +namespace Multiplayer.Networking.Data.Train; + +[Flags] +public enum BogieFlags : byte +{ + None = 0, + IncludesTrackData = 1, + HasDerailed = 2 +} +public readonly struct BogieData +{ + private readonly BogieFlags DataFlags; + public readonly double PositionAlongTrack; + public readonly ushort TrackNetId; + public readonly int TrackDirection; + + public bool IncludesTrackData => DataFlags.HasFlag(BogieFlags.IncludesTrackData); + public bool HasDerailed => DataFlags.HasFlag(BogieFlags.HasDerailed); + + private BogieData(BogieFlags flags, double positionAlongTrack, ushort trackNetId, int trackDirection) + { + // Prevent invalid state combinations + if (flags.HasFlag(BogieFlags.HasDerailed)) + flags &= ~BogieFlags.IncludesTrackData; // Clear track data flag if derailed + + DataFlags = flags; + PositionAlongTrack = positionAlongTrack; + TrackNetId = trackNetId; + TrackDirection = trackDirection; + } + + public static BogieData FromBogie(Bogie bogie, bool includeTrack) + { + bool includesTrackData = includeTrack && !bogie.HasDerailed && bogie.track; + + BogieFlags flags = BogieFlags.None; + if (includesTrackData) flags |= BogieFlags.IncludesTrackData; + if (bogie.HasDerailed) flags |= BogieFlags.HasDerailed; + + return new BogieData( + flags, + bogie.traveller?.Span ?? -1.0, + includesTrackData ? bogie.track.Networked().NetId : (ushort)0, + bogie.trackDirection + ); + } + + public static void Serialize(NetDataWriter writer, BogieData data) + { + writer.Put((byte)data.DataFlags); + if (!data.HasDerailed) writer.Put(data.PositionAlongTrack); + if (!data.IncludesTrackData) return; + writer.Put(data.TrackNetId); + writer.Put(data.TrackDirection); + } + + public static BogieData Deserialize(NetDataReader reader) + { + BogieFlags flags = (BogieFlags)reader.GetByte(); + + // Read position if not derailed + double positionAlongTrack = !flags.HasFlag(BogieFlags.HasDerailed) + ? reader.GetDouble() + : -1.0; + + // Read track data if included + ushort trackNetId = 0; + int trackDirection = 0; + if (flags.HasFlag(BogieFlags.IncludesTrackData)) + { + trackNetId = reader.GetUShort(); + trackDirection = reader.GetInt(); + } + + return new BogieData(flags, positionAlongTrack, trackNetId, trackDirection); + } +} diff --git a/Multiplayer/Networking/Data/Train/BrakeSystemData.cs b/Multiplayer/Networking/Data/Train/BrakeSystemData.cs new file mode 100644 index 0000000..2fef7bb --- /dev/null +++ b/Multiplayer/Networking/Data/Train/BrakeSystemData.cs @@ -0,0 +1,89 @@ +using DV.Simulation.Brake; +using LiteNetLib.Utils; + +namespace Multiplayer.Networking.Data.Train; + +public readonly struct BrakeSystemData +{ + public readonly bool HasHandbrake; + public readonly bool HasTrainbrake; + public readonly float HandBrakePosition; + public readonly float TrainBrakePosition; + public readonly float BrakePipePressure; + public readonly float AuxResPressure; + public readonly float MainResPressure; + public readonly float ControlResPressure; + public readonly float BrakeCylPressure; + + public BrakeSystemData( + bool hasHandbrake, bool hasTrainbrake, + float handBrakePosition, float trainBrakePosition, + float brakePipePressure, float auxResPressure, + float mainResPressure, float controlResPressure, + float brakeCylPressure) + { + HasHandbrake = hasHandbrake; + HasTrainbrake = hasTrainbrake; + HandBrakePosition = handBrakePosition; + TrainBrakePosition = trainBrakePosition; + BrakePipePressure = brakePipePressure; + AuxResPressure = auxResPressure; + MainResPressure = mainResPressure; + ControlResPressure = controlResPressure; + BrakeCylPressure = brakeCylPressure; + } + + public static void Serialize(NetDataWriter writer, BrakeSystemData data) + { + writer.Put(data.HasHandbrake); + if (data.HasHandbrake) + writer.Put(data.HandBrakePosition); + + writer.Put(data.HasTrainbrake); + if (data.HasTrainbrake) + writer.Put(data.TrainBrakePosition); + + writer.Put(data.BrakePipePressure); + writer.Put(data.AuxResPressure); + writer.Put(data.MainResPressure); + writer.Put(data.ControlResPressure); + writer.Put(data.BrakeCylPressure); + } + + public static BrakeSystemData Deserialize(NetDataReader reader) + { + bool hasHandbrake = reader.GetBool(); + float handBrakePosition = hasHandbrake ? reader.GetFloat() : 0f; + + bool hasTrainbrake = reader.GetBool(); + float trainBrakePosition = hasTrainbrake ? reader.GetFloat() : 0f; + + return new BrakeSystemData( + hasHandbrake, + hasTrainbrake, + handBrakePosition, + trainBrakePosition, + reader.GetFloat(), // BrakePipePressure + reader.GetFloat(), // AuxResPressure + reader.GetFloat(), // MainResPressure + reader.GetFloat(), // ControlResPressure + reader.GetFloat() // BrakeCylPressure + ); + } + + public static BrakeSystemData From(BrakeSystem brakeSystem) + { + return new BrakeSystemData( + hasHandbrake: brakeSystem.hasHandbrake, + hasTrainbrake: brakeSystem.hasTrainBrake, + handBrakePosition: brakeSystem.handbrakePosition, + trainBrakePosition: brakeSystem.trainBrakePosition, + brakePipePressure: brakeSystem.brakePipePressure, + auxResPressure: brakeSystem.auxReservoirPressure, + mainResPressure: brakeSystem.mainReservoirPressure, + controlResPressure: brakeSystem.controlReservoirPressure, + brakeCylPressure: brakeSystem.brakeCylinderPressure + ); + } + +} diff --git a/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs b/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs new file mode 100644 index 0000000..36845ad --- /dev/null +++ b/Multiplayer/Networking/Data/Train/CouplerInteractionType.cs @@ -0,0 +1,28 @@ +using System; + +namespace Multiplayer.Networking.Data.Train; + +[Flags] +public enum CouplerInteractionType : ushort +{ + NoAction = 0, + Start = 1, + + CouplerCouple = 2, + CouplerPark = 4, + CouplerDrop = 8, + CouplerTighten = 16, + CouplerLoosen = 32, + + HoseConnect = 64, + HoseDisconnect = 128, + + CockOpen = 256, + CockClose = 512, + + CoupleViaUI = 1024, + UncoupleViaUI = 2048, + + CoupleViaRemote = 4096, + UncoupleViaRemote = 8192, +} diff --git a/Multiplayer/Networking/Data/Train/CouplingData.cs b/Multiplayer/Networking/Data/Train/CouplingData.cs new file mode 100644 index 0000000..3424cb3 --- /dev/null +++ b/Multiplayer/Networking/Data/Train/CouplingData.cs @@ -0,0 +1,77 @@ +using LiteNetLib.Utils; +using Multiplayer.Utils; + +namespace Multiplayer.Networking.Data.Train; + +public readonly struct CouplingData +{ + public readonly bool IsCoupled; + public readonly ChainCouplerInteraction.State State; + public readonly ushort ConnectionNetId; + public readonly bool ConnectionToFront; + public readonly bool HoseConnected; + public readonly bool PreventAutoCouple; + public readonly bool CockOpen; + + public CouplingData(bool isCoupled, bool hoseConnected, ChainCouplerInteraction.State state, + ushort connectionNetId, bool connectionToFront, bool preventAutoCouple, bool cockOpen) + { + IsCoupled = isCoupled; + State = state; + ConnectionNetId = connectionNetId; + ConnectionToFront = connectionToFront; + HoseConnected = hoseConnected; + PreventAutoCouple = preventAutoCouple; + CockOpen = cockOpen; + } + + public static void Serialize(NetDataWriter writer, CouplingData data) + { + writer.Put(data.IsCoupled); + writer.Put(data.HoseConnected); + writer.Put((byte)data.State); + + if (data.IsCoupled || data.HoseConnected) + { + writer.Put(data.ConnectionNetId); + writer.Put(data.ConnectionToFront); + } + + writer.Put(data.PreventAutoCouple); + writer.Put(data.CockOpen); + } + + public static CouplingData Deserialize(NetDataReader reader) + { + bool isCoupled = reader.GetBool(); + bool hoseConnected = reader.GetBool(); + var state = (ChainCouplerInteraction.State)reader.GetByte(); + + ushort connectionNetId = 0; + bool connectionToFront = false; + + if (isCoupled || hoseConnected) + { + connectionNetId = reader.GetUShort(); + connectionToFront = reader.GetBool(); + } + + bool preventAutoCouple = reader.GetBool(); + bool cockOpen = reader.GetBool(); + + return new CouplingData(isCoupled, hoseConnected, state, connectionNetId, + connectionToFront, preventAutoCouple, cockOpen); + } + public static CouplingData From(Coupler coupler) + { + return new CouplingData( + isCoupled: coupler.IsCoupled(), + hoseConnected: coupler.hoseAndCock.IsHoseConnected, + state: coupler.state, + connectionNetId: coupler.IsCoupled() ? coupler.coupledTo.train.GetNetId() : (ushort)0, + connectionToFront: coupler.IsCoupled() && coupler.coupledTo.isFrontCoupler, + preventAutoCouple: coupler.preventAutoCouple, + cockOpen: coupler.IsCockOpen + ); + } +} diff --git a/Multiplayer/Networking/Data/RigidbodySnapshot.cs b/Multiplayer/Networking/Data/Train/RigidbodySnapshot.cs similarity index 80% rename from Multiplayer/Networking/Data/RigidbodySnapshot.cs rename to Multiplayer/Networking/Data/Train/RigidbodySnapshot.cs index d4161d8..00af0e7 100644 --- a/Multiplayer/Networking/Data/RigidbodySnapshot.cs +++ b/Multiplayer/Networking/Data/Train/RigidbodySnapshot.cs @@ -1,15 +1,15 @@ -using System; +using System; using LiteNetLib.Utils; using Multiplayer.Networking.Serialization; using UnityEngine; -namespace Multiplayer.Networking.Data; +namespace Multiplayer.Networking.Data.Train; public class RigidbodySnapshot { public byte IncludedDataFlags { get; set; } public Vector3 Position { get; set; } - public Vector3 Rotation { get; set; } + public Quaternion Rotation { get; set; } public Vector3 Velocity { get; set; } public Vector3 AngularVelocity { get; set; } @@ -17,12 +17,16 @@ public static void Serialize(NetDataWriter writer, RigidbodySnapshot data) { writer.Put(data.IncludedDataFlags); IncludedData flags = (IncludedData)data.IncludedDataFlags; + if (flags.HasFlag(IncludedData.Position)) Vector3Serializer.Serialize(writer, data.Position); + if (flags.HasFlag(IncludedData.Rotation)) - Vector3Serializer.Serialize(writer, data.Rotation); + QuaternionSerializer.Serialize(writer, data.Rotation); + if (flags.HasFlag(IncludedData.Velocity)) Vector3Serializer.Serialize(writer, data.Velocity); + if (flags.HasFlag(IncludedData.AngularVelocity)) Vector3Serializer.Serialize(writer, data.AngularVelocity); } @@ -30,46 +34,66 @@ public static void Serialize(NetDataWriter writer, RigidbodySnapshot data) public static RigidbodySnapshot Deserialize(NetDataReader reader) { IncludedData IncludedDataFlags = (IncludedData)reader.GetByte(); - RigidbodySnapshot snapshot = new() { + + RigidbodySnapshot snapshot = new() + { IncludedDataFlags = (byte)IncludedDataFlags }; + if (IncludedDataFlags.HasFlag(IncludedData.Position)) snapshot.Position = Vector3Serializer.Deserialize(reader); + if (IncludedDataFlags.HasFlag(IncludedData.Rotation)) - snapshot.Rotation = Vector3Serializer.Deserialize(reader); + snapshot.Rotation = QuaternionSerializer.Deserialize(reader); + if (IncludedDataFlags.HasFlag(IncludedData.Velocity)) snapshot.Velocity = Vector3Serializer.Deserialize(reader); + if (IncludedDataFlags.HasFlag(IncludedData.AngularVelocity)) snapshot.AngularVelocity = Vector3Serializer.Deserialize(reader); + return snapshot; } public static RigidbodySnapshot From(Rigidbody rb, IncludedData includedDataFlags = IncludedData.All) { - RigidbodySnapshot snapshot = new() { + RigidbodySnapshot snapshot = new() + { IncludedDataFlags = (byte)includedDataFlags }; + if (includedDataFlags.HasFlag(IncludedData.Position)) snapshot.Position = rb.position - WorldMover.currentMove; + if (includedDataFlags.HasFlag(IncludedData.Rotation)) - snapshot.Rotation = rb.rotation.eulerAngles; + snapshot.Rotation = rb.rotation;//.eulerAngles; + if (includedDataFlags.HasFlag(IncludedData.Velocity)) snapshot.Velocity = rb.velocity; + if (includedDataFlags.HasFlag(IncludedData.AngularVelocity)) snapshot.AngularVelocity = rb.angularVelocity; + return snapshot; } public void Apply(Rigidbody rb) { + if (rb == null) + return; + IncludedData flags = (IncludedData)IncludedDataFlags; + if (flags.HasFlag(IncludedData.Position)) rb.MovePosition(Position + WorldMover.currentMove); - if (flags.HasFlag(IncludedData.Position)) - rb.MoveRotation(Quaternion.Euler(Rotation)); - if (flags.HasFlag(IncludedData.Position)) + + if (flags.HasFlag(IncludedData.Rotation)) + rb.MoveRotation(Rotation); + + if (flags.HasFlag(IncludedData.Velocity)) rb.velocity = Velocity; - if (flags.HasFlag(IncludedData.Position)) + + if (flags.HasFlag(IncludedData.AngularVelocity)) rb.angularVelocity = AngularVelocity; } diff --git a/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs new file mode 100644 index 0000000..e3d12d3 --- /dev/null +++ b/Multiplayer/Networking/Data/Train/TrainsetMovementPart.cs @@ -0,0 +1,125 @@ +using LiteNetLib.Utils; +using Multiplayer.Networking.Serialization; +using System; +using UnityEngine; +namespace Multiplayer.Networking.Data.Train; + +public readonly struct TrainsetMovementPart +{ + public readonly ushort NetId; + public readonly MovementType typeFlag; + public readonly float Speed; + public readonly float SlowBuildUpStress; + public readonly Vector3 Position; //Used in sync only + public readonly Quaternion Rotation; //Used in sync only + public readonly BogieData Bogie1; + public readonly BogieData Bogie2; + public readonly RigidbodySnapshot RigidbodySnapshot; + + [Flags] + public enum MovementType : byte + { + Physics = 1, + RigidBody = 2, + Position = 4 + } + + public TrainsetMovementPart(ushort netId, float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2, Vector3? position = null, Quaternion? rotation = null) + { + NetId = netId; + + typeFlag = MovementType.Physics; //no rigid body data + + Speed = speed; + SlowBuildUpStress = slowBuildUpStress; + Bogie1 = bogie1; + Bogie2 = bogie2; + + if (position != null && rotation != null) + { + //Multiplayer.LogDebug(()=>$"new TrainsetMovementPart() Sync"); + + typeFlag |= MovementType.Position; //includes positional data + + Position = (Vector3)position; + Rotation = (Quaternion)rotation; + } + } + + public TrainsetMovementPart(ushort netId, RigidbodySnapshot rigidbodySnapshot) + { + NetId = netId; + typeFlag = MovementType.RigidBody; //rigid body data + + //Multiplayer.LogDebug(() => $"new TrainsetMovementPart() RigidBody"); + + RigidbodySnapshot = rigidbodySnapshot; + } + +#pragma warning disable EPS05 // Use in-modifier for a readonly struct + public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) +#pragma warning restore EPS05 // Use in-modifier for a readonly struct + { + writer.Put(data.NetId); + + writer.Put((byte)data.typeFlag); + + //Multiplayer.LogDebug(() => $"TrainsetMovementPart.Serialize() {data.typeFlag}"); + + if (data.typeFlag.HasFlag(MovementType.RigidBody)) + { + RigidbodySnapshot.Serialize(writer, data.RigidbodySnapshot); + return; + } + + if (data.typeFlag.HasFlag(MovementType.Physics)) + { + writer.Put(data.Speed); + writer.Put(data.SlowBuildUpStress); + BogieData.Serialize(writer, data.Bogie1); + BogieData.Serialize(writer, data.Bogie2); + } + + if (data.typeFlag.HasFlag(MovementType.Position)) + { + Vector3Serializer.Serialize(writer, data.Position); + QuaternionSerializer.Serialize(writer, data.Rotation); + } + } + + public static TrainsetMovementPart Deserialize(NetDataReader reader) + { + ushort netId = 0; + float speed = 0; + float slowBuildUpStress = 0; + Vector3? position = null; + Quaternion? rotation = null; + BogieData bd1 = default; + BogieData bd2 = default; + + netId = reader.GetUShort(); + + MovementType dataType = (MovementType)reader.GetByte(); + + if (dataType.HasFlag(MovementType.RigidBody)) + { + return new TrainsetMovementPart(0, RigidbodySnapshot.Deserialize(reader)); + } + + if (dataType.HasFlag(MovementType.Physics)) + { + speed = reader.GetFloat(); + slowBuildUpStress = reader.GetFloat(); + bd1 = BogieData.Deserialize(reader); + bd2 = BogieData.Deserialize(reader); + } + + if (dataType.HasFlag(MovementType.Position)) + { + position = Vector3Serializer.Deserialize(reader); + rotation = QuaternionSerializer.Deserialize(reader); + } + + return new TrainsetMovementPart(0, speed, slowBuildUpStress, bd1, bd2, position, rotation); + } +} diff --git a/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs new file mode 100644 index 0000000..afd580d --- /dev/null +++ b/Multiplayer/Networking/Data/Train/TrainsetSpawnPart.cs @@ -0,0 +1,209 @@ +using DV.Customization.Paint; +using DV.LocoRestoration; +using LiteNetLib.Utils; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Serialization; +using Multiplayer.Utils; +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Networking.Data.Train; + +public readonly struct TrainsetSpawnPart +{ + private static readonly byte[] EMPTY_GUID = new Guid().ToByteArray(); // Empty GUID as bytes + + public readonly ushort NetId; + + // Car details + public readonly string LiveryId; + public readonly string CarId; + public readonly string CarGuid; + + // Customisation details + public readonly bool PlayerSpawnedCar; + public readonly bool IsRestorationLoco; + public readonly LocoRestorationController.RestorationState RestorationState; + public readonly PaintTheme PaintExterior; + public readonly PaintTheme PaintInterior; + + // Coupling data + public readonly CouplingData FrontCoupling; + public readonly CouplingData RearCoupling; + + // Positional details + public readonly float Speed; + public readonly Vector3 Position; + public readonly Quaternion Rotation; + + // Bogie data + public readonly BogieData Bogie1; + public readonly BogieData Bogie2; + + // Brake initial states + public readonly BrakeSystemData BrakeData; + + public TrainsetSpawnPart( + ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, bool isRestoration, LocoRestorationController.RestorationState restorationState, PaintTheme paintExterior, PaintTheme paintInterior, + CouplingData frontCoupling, CouplingData rearCoupling, + float speed, Vector3 position, Quaternion rotation, + BogieData bogie1, BogieData bogie2, BrakeSystemData brakeData) + { + NetId = netId; + LiveryId = liveryId; + CarId = carId; + CarGuid = carGuid; + + PlayerSpawnedCar = playerSpawnedCar; + IsRestorationLoco = isRestoration; + RestorationState = restorationState; + + PaintExterior = paintExterior; + PaintInterior = paintInterior; + + FrontCoupling = frontCoupling; + RearCoupling = rearCoupling; + + Speed = speed; + Position = position; + Rotation = rotation; + Bogie1 = bogie1; + Bogie2 = bogie2; + BrakeData = brakeData; + } + + public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) + { + writer.Put(data.NetId); + writer.Put(data.LiveryId); + writer.Put(data.CarId); + + if (Guid.TryParse(data.CarGuid, out Guid guid)) + writer.PutBytesWithLength(guid.ToByteArray()); + else + { + Multiplayer.LogError($"TrainsetSpawnPart.Serialize() failed to parse carGuid: {data.CarGuid}"); + writer.PutBytesWithLength(EMPTY_GUID); + } + + writer.Put(data.PlayerSpawnedCar); + writer.Put(data.IsRestorationLoco); + + if(data.IsRestorationLoco) + writer.Put((byte) data.RestorationState); + + writer.Put(PaintThemeLookup.Instance.GetThemeIndex(data.PaintExterior)); + writer.Put(PaintThemeLookup.Instance.GetThemeIndex(data.PaintInterior)); + + + CouplingData.Serialize(writer, data.FrontCoupling); + CouplingData.Serialize(writer, data.RearCoupling); + + writer.Put(data.Speed); + Vector3Serializer.Serialize(writer, data.Position); + QuaternionSerializer.Serialize(writer, data.Rotation); + + BogieData.Serialize(writer, data.Bogie1); + BogieData.Serialize(writer, data.Bogie2); + BrakeSystemData.Serialize(writer, data.BrakeData); + } + + public static TrainsetSpawnPart Deserialize(NetDataReader reader) + { + ushort netId = reader.GetUShort(); + string liveryId = reader.GetString(); + string carId = reader.GetString(); + string carGuid = new Guid(reader.GetBytesWithLength()).ToString(); + bool playerSpawnedCar = reader.GetBool(); + + bool isRestoration = reader.GetBool(); + LocoRestorationController.RestorationState restorationState = default; + if (isRestoration) + restorationState = (LocoRestorationController.RestorationState)reader.GetByte(); + + sbyte extThemeIndex = reader.GetSByte(); + sbyte intThemeIndex = reader.GetSByte(); + + + PaintTheme exteriorPaint = PaintThemeLookup.Instance.GetPaintTheme(extThemeIndex); + PaintTheme interiorPaint = PaintThemeLookup.Instance.GetPaintTheme(intThemeIndex); + + var frontCoupling = CouplingData.Deserialize(reader); + var rearCoupling = CouplingData.Deserialize(reader); + + float speed = reader.GetFloat(); + Vector3 position = Vector3Serializer.Deserialize(reader); + Quaternion rotation = QuaternionSerializer.Deserialize(reader); + + var bogie1 = BogieData.Deserialize(reader); + var bogie2 = BogieData.Deserialize(reader); + var brakeSet = BrakeSystemData.Deserialize(reader); + + return new TrainsetSpawnPart( + netId, liveryId, carId, carGuid, playerSpawnedCar, isRestoration, restorationState, exteriorPaint, interiorPaint, + frontCoupling, rearCoupling, + speed, position, rotation, + bogie1, bogie2, brakeSet); + } + + public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar) + { + TrainCar trainCar = networkedTrainCar.TrainCar; + Transform transform = networkedTrainCar.transform; + + + LocoRestorationController restorationController = LocoRestorationController.GetForTrainCar(trainCar); + var restorationState = restorationController?.State ?? default; + + return new TrainsetSpawnPart( + networkedTrainCar.NetId, + trainCar.carLivery.id, + trainCar.ID, + trainCar.CarGUID, + + trainCar.playerSpawnedCar, + restorationController != null, + restorationState, + + trainCar?.PaintExterior?.currentTheme, + trainCar?.PaintInterior?.currentTheme, + + frontCoupling: CouplingData.From(trainCar.frontCoupler), + rearCoupling: CouplingData.From(trainCar.rearCoupler), + trainCar.GetForwardSpeed(), + transform.position - WorldMover.currentMove, + transform.rotation, + BogieData.FromBogie(trainCar.Bogies[0], true), + BogieData.FromBogie(trainCar.Bogies[1], true), + BrakeSystemData.From(trainCar.brakeSystem) + ); + } + + public static TrainsetSpawnPart[] FromTrainSet(List trainset/*, bool resolveCoupling = false*/) + { + if (trainset == null) + { + NetworkLifecycle.Instance.Server.LogWarning("TrainsetSpawnPart.FromTrainSet() trainset list is null!"); + return null; + } + + TrainsetSpawnPart[] parts = new TrainsetSpawnPart[trainset.Count]; + for (int i = 0; i < trainset.Count; i++) + { + NetworkedTrainCar networkedTrainCar; + + if (!trainset[i].TryNetworked(out networkedTrainCar)) + { + NetworkLifecycle.Instance.Server.LogWarning($"TrainsetSpawnPart.FromTrainSet() Failed to find NetworkedTrainCar for: {trainset[i]?.ID}"); + networkedTrainCar = trainset[i].GetOrAddComponent(); + } + + parts[i] = FromTrainCar(networkedTrainCar); + } + + return parts; + } + +} diff --git a/Multiplayer/Networking/Data/TrainsetMovementPart.cs b/Multiplayer/Networking/Data/TrainsetMovementPart.cs deleted file mode 100644 index 62fade0..0000000 --- a/Multiplayer/Networking/Data/TrainsetMovementPart.cs +++ /dev/null @@ -1,59 +0,0 @@ -using LiteNetLib.Utils; - -namespace Multiplayer.Networking.Data; - -public readonly struct TrainsetMovementPart -{ - public readonly bool IsRigidbodySnapshot; - public readonly float Speed; - public readonly float SlowBuildUpStress; - public readonly BogieData Bogie1; - public readonly BogieData Bogie2; - public readonly RigidbodySnapshot RigidbodySnapshot; - - public TrainsetMovementPart(float speed, float slowBuildUpStress, BogieData bogie1, BogieData bogie2) - { - IsRigidbodySnapshot = false; - Speed = speed; - SlowBuildUpStress = slowBuildUpStress; - Bogie1 = bogie1; - Bogie2 = bogie2; - } - - public TrainsetMovementPart(RigidbodySnapshot rigidbodySnapshot) - { - IsRigidbodySnapshot = true; - RigidbodySnapshot = rigidbodySnapshot; - } - -#pragma warning disable EPS05 - public static void Serialize(NetDataWriter writer, TrainsetMovementPart data) -#pragma warning restore EPS05 - { - writer.Put(data.IsRigidbodySnapshot); - - if (data.IsRigidbodySnapshot) - { - RigidbodySnapshot.Serialize(writer, data.RigidbodySnapshot); - return; - } - - writer.Put(data.Speed); - writer.Put(data.SlowBuildUpStress); - BogieData.Serialize(writer, data.Bogie1); - BogieData.Serialize(writer, data.Bogie2); - } - - public static TrainsetMovementPart Deserialize(NetDataReader reader) - { - bool isRigidbodySnapshot = reader.GetBool(); - return isRigidbodySnapshot - ? new TrainsetMovementPart(RigidbodySnapshot.Deserialize(reader)) - : new TrainsetMovementPart( - reader.GetFloat(), - reader.GetFloat(), - BogieData.Deserialize(reader), - BogieData.Deserialize(reader) - ); - } -} diff --git a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs b/Multiplayer/Networking/Data/TrainsetSpawnPart.cs deleted file mode 100644 index 5d6b6cd..0000000 --- a/Multiplayer/Networking/Data/TrainsetSpawnPart.cs +++ /dev/null @@ -1,102 +0,0 @@ -using LiteNetLib.Utils; -using Multiplayer.Components.Networking.Train; -using Multiplayer.Networking.Serialization; -using Multiplayer.Utils; -using UnityEngine; - -namespace Multiplayer.Networking.Data; - -public readonly struct TrainsetSpawnPart -{ - public readonly ushort NetId; - public readonly string LiveryId; - public readonly string CarId; - public readonly string CarGuid; - public readonly bool PlayerSpawnedCar; - public readonly bool IsFrontCoupled; - public readonly bool IsRearCoupled; - public readonly float Speed; - public readonly Vector3 Position; - public readonly Vector3 Rotation; - public readonly BogieData Bogie1; - public readonly BogieData Bogie2; - - private TrainsetSpawnPart(ushort netId, string liveryId, string carId, string carGuid, bool playerSpawnedCar, bool isFrontCoupled, bool isRearCoupled, float speed, Vector3 position, Vector3 rotation, - BogieData bogie1, BogieData bogie2) - { - NetId = netId; - LiveryId = liveryId; - CarId = carId; - CarGuid = carGuid; - PlayerSpawnedCar = playerSpawnedCar; - IsFrontCoupled = isFrontCoupled; - IsRearCoupled = isRearCoupled; - Speed = speed; - Position = position; - Rotation = rotation; - Bogie1 = bogie1; - Bogie2 = bogie2; - } - - public static void Serialize(NetDataWriter writer, TrainsetSpawnPart data) - { - writer.Put(data.NetId); - writer.Put(data.LiveryId); - writer.Put(data.CarId); - writer.Put(data.CarGuid); - writer.Put(data.PlayerSpawnedCar); - writer.Put(data.IsFrontCoupled); - writer.Put(data.IsRearCoupled); - writer.Put(data.Speed); - Vector3Serializer.Serialize(writer, data.Position); - Vector3Serializer.Serialize(writer, data.Rotation); - BogieData.Serialize(writer, data.Bogie1); - BogieData.Serialize(writer, data.Bogie2); - } - - public static TrainsetSpawnPart Deserialize(NetDataReader reader) - { - return new TrainsetSpawnPart( - reader.GetUShort(), - reader.GetString(), - reader.GetString(), - reader.GetString(), - reader.GetBool(), - reader.GetBool(), - reader.GetBool(), - reader.GetFloat(), - Vector3Serializer.Deserialize(reader), - Vector3Serializer.Deserialize(reader), - BogieData.Deserialize(reader), - BogieData.Deserialize(reader) - ); - } - - public static TrainsetSpawnPart FromTrainCar(NetworkedTrainCar networkedTrainCar) - { - TrainCar trainCar = networkedTrainCar.TrainCar; - Transform transform = networkedTrainCar.transform; - return new TrainsetSpawnPart( - networkedTrainCar.NetId, - trainCar.carLivery.id, - trainCar.ID, - trainCar.CarGUID, - trainCar.playerSpawnedCar, - trainCar.frontCoupler.IsCoupled(), - trainCar.rearCoupler.IsCoupled(), - trainCar.GetForwardSpeed(), - transform.position - WorldMover.currentMove, - transform.eulerAngles, - BogieData.FromBogie(trainCar.Bogies[0], true, networkedTrainCar.Bogie1TrackDirection), - BogieData.FromBogie(trainCar.Bogies[1], true, networkedTrainCar.Bogie2TrackDirection) - ); - } - - public static TrainsetSpawnPart[] FromTrainSet(Trainset trainset) - { - TrainsetSpawnPart[] parts = new TrainsetSpawnPart[trainset.cars.Count]; - for (int i = 0; i < trainset.cars.Count; i++) - parts[i] = FromTrainCar(trainset.cars[i].Networked()); - return parts; - } -} diff --git a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs index 1911f9c..bcd7445 100644 --- a/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs +++ b/Multiplayer/Networking/Managers/Client/ClientPlayerManager.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using DV; +using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Player; using UnityEngine; using Object = UnityEngine.Object; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers.Client; public class ClientPlayerManager { @@ -27,14 +28,14 @@ public bool TryGetPlayer(byte id, out NetworkedPlayer player) return playerMap.TryGetValue(id, out player); } - public void AddPlayer(byte id, string username, Guid guid) + public void AddPlayer(byte id, string username) { GameObject go = Object.Instantiate(playerPrefab, WorldMover.Instance.originShiftParent); go.layer = LayerMask.NameToLayer(Layers.Player); NetworkedPlayer networkedPlayer = go.AddComponent(); networkedPlayer.Id = id; networkedPlayer.Username = username; - networkedPlayer.Guid = guid; + //networkedPlayer.Guid = guid; playerMap.Add(id, networkedPlayer); OnPlayerConnected?.Invoke(id, networkedPlayer); } @@ -43,6 +44,7 @@ public void RemovePlayer(byte id) { if (!playerMap.TryGetValue(id, out NetworkedPlayer networkedPlayer)) return; + OnPlayerDisconnected?.Invoke(id, networkedPlayer); Object.Destroy(networkedPlayer.gameObject); playerMap.Remove(id); @@ -55,17 +57,18 @@ public void UpdatePing(byte id, int ping) player.SetPing(ping); } - public void UpdatePosition(byte id, Vector3 position, Vector3 moveDir, float rotation, bool isJumping, bool isOnCar) + public void UpdatePosition(byte id, Vector3 position, Vector3 moveDir, float rotation, bool isJumping, bool isOnCar, ushort carId) { if (!playerMap.TryGetValue(id, out NetworkedPlayer player)) return; + player.UpdateCar(carId); player.UpdatePosition(position, moveDir, rotation, isJumping, isOnCar); } - public void UpdateCar(byte playerId, ushort carId) - { - if (!playerMap.TryGetValue(playerId, out NetworkedPlayer player)) - return; - player.UpdateCar(carId); - } + //public void UpdateCar(byte playerId, ushort carId) + //{ + // if (!playerMap.TryGetValue(playerId, out NetworkedPlayer player)) + // return; + // player.UpdateCar(carId); + //} } diff --git a/Multiplayer/Networking/Managers/Client/NetworkClient.cs b/Multiplayer/Networking/Managers/Client/NetworkClient.cs index b44d387..9078f65 100644 --- a/Multiplayer/Networking/Managers/Client/NetworkClient.cs +++ b/Multiplayer/Networking/Managers/Client/NetworkClient.cs @@ -1,5 +1,7 @@ using System; +using System.Net; using System.Text; +using System.Collections.Generic; using DV; using DV.Damage; using DV.InventorySystem; @@ -8,69 +10,116 @@ using DV.ServicePenalty.UI; using DV.ThingTypes; using DV.UI; -using DV.UIFramework; using DV.WeatherSystem; using LiteNetLib; using Multiplayer.Components.MainMenu; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.Player; using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.UI; using Multiplayer.Components.Networking.World; using Multiplayer.Components.SaveGame; using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound; +using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Clientbound.World; using Multiplayer.Networking.Packets.Common; using Multiplayer.Networking.Packets.Common.Train; using Multiplayer.Networking.Packets.Serverbound; +using Multiplayer.Networking.Data.Train; using Multiplayer.Patches.SaveGame; using Multiplayer.Utils; using Newtonsoft.Json.Linq; using UnityEngine; using UnityModManagerNet; using Object = UnityEngine.Object; +using Multiplayer.Networking.Packets.Serverbound.Train; +using System.Linq; +using LiteNetLib.Utils; +using DV.UserManagement; +using DV.Common; +using DV.Customization.Paint; +using Multiplayer.Networking.TransportLayers; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers.Client; public class NetworkClient : NetworkManager { protected override string LogPrefix => "[Client]"; - public NetPeer selfPeer { get; private set; } - public readonly ClientPlayerManager PlayerManager; + private Action onDisconnect; + private string disconnectMessage; + + public ITransportPeer SelfPeer { get; private set; } + public readonly ClientPlayerManager ClientPlayerManager; // One way ping in milliseconds public int Ping { get; private set; } - private NetPeer serverPeer; + private ITransportPeer serverPeer; + + private ChatGUI chatGUI; + public bool isSinglePlayer; + + private bool isAlsoHost; + IGameSession originalSession; public NetworkClient(Settings settings) : base(settings) { - PlayerManager = new ClientPlayerManager(); + ClientPlayerManager = new ClientPlayerManager(); } - public void Start(string address, int port, string password) + public void Start(string address, int port, string password, bool isSinglePlayer, Action onDisconnect) { - netManager.Start(); - ServerboundClientLoginPacket serverboundClientLoginPacket = new() { - Username = Multiplayer.Settings.Username, + LogDebug(() => $"NetworkClient Constructor"); + + this.onDisconnect = onDisconnect; + //netManager.Start(); + base.Start(); + + ServerboundClientLoginPacket serverboundClientLoginPacket = new() + { + Username = Multiplayer.Settings.GetUserName(), Guid = Multiplayer.Settings.GetGuid().ToByteArray(), Password = password, BuildMajorVersion = (ushort)BuildInfo.BUILD_VERSION_MAJOR, Mods = ModInfo.FromModEntries(UnityModManager.modEntries) }; netPacketProcessor.Write(cachedWriter, serverboundClientLoginPacket); - selfPeer = netManager.Connect(address, port, cachedWriter); + SelfPeer = Connect(address, port, cachedWriter); + + isAlsoHost = NetworkLifecycle.Instance.IsServerRunning; + originalSession = UserManager.Instance.CurrentUser.CurrentSession; + + LogDebug(() => $"NetworkClient.Start() isAlsoHost: {isAlsoHost}, Original session is Null: {originalSession == null}"); + } + + public override void Stop() + { + if (!isAlsoHost && originalSession != null) + { + LogDebug(() => $"NetworkClient.Stop() destroying session... Original session is Null: {originalSession == null}"); + //IGameSession session = UserManager.Instance.CurrentUser.CurrentSession; + Client_GameSession.SetCurrent(originalSession); + //session?.Dispose(); + } + + base.Stop(); } protected override void Subscribe() { - netPacketProcessor.SubscribeReusable(OnClientboundServerDenyPacket); + netPacketProcessor.SubscribeReusable(OnClientboundLoginResponsePacket); + netPacketProcessor.SubscribeReusable(OnClientboundDisconnectPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPlayerJoinedPacket); netPacketProcessor.SubscribeReusable(OnClientboundPlayerDisconnectPacket); + netPacketProcessor.SubscribeReusable(OnClientboundPlayerPositionPacket); - netPacketProcessor.SubscribeReusable(OnClientboundPlayerCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundPingUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundTickSyncPacket); netPacketProcessor.SubscribeReusable(OnClientboundServerLoadingPacket); netPacketProcessor.SubscribeReusable(OnClientboundBeginWorldSyncPacket); @@ -80,12 +129,14 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundRemoveLoadingScreen); netPacketProcessor.SubscribeReusable(OnClientboundTimeAdvancePacket); netPacketProcessor.SubscribeReusable(OnClientboundRailwayStatePacket); + netPacketProcessor.SubscribeReusable(OnClientBoundStationControllerLookupPacket); netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); netPacketProcessor.SubscribeReusable(OnClientboundSpawnTrainCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundSpawnTrainSetPacket); netPacketProcessor.SubscribeReusable(OnClientboundDestroyTrainCarPacket); netPacketProcessor.SubscribeReusable(OnClientboundTrainPhysicsPacket); + netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); @@ -95,8 +146,11 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); + netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); netPacketProcessor.SubscribeReusable(OnCommonSimFlowPacket); netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + netPacketProcessor.SubscribeReusable(OnClientboundBrakeStateUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundFireboxStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCargoStatePacket); netPacketProcessor.SubscribeReusable(OnClientboundCarHealthUpdatePacket); netPacketProcessor.SubscribeReusable(OnClientboundRerailTrainPacket); @@ -106,28 +160,32 @@ protected override void Subscribe() netPacketProcessor.SubscribeReusable(OnClientboundLicenseAcquiredPacket); netPacketProcessor.SubscribeReusable(OnClientboundGarageUnlockPacket); netPacketProcessor.SubscribeReusable(OnClientboundDebtStatusPacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobsUpdatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobsCreatePacket); + netPacketProcessor.SubscribeReusable(OnClientboundJobValidateResponsePacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } #region Net Events - public override void OnPeerConnected(NetPeer peer) + public override void OnPeerConnected(ITransportPeer peer) { serverPeer = peer; - if (NetworkLifecycle.Instance.IsHost(peer)) - SendReadyPacket(); - else - SendSaveGameDataRequest(); } - public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason disconnectReason) { + + LogDebug(()=>$"OnPeerDisconnected({peer.Id}, {disconnectReason}) disconnect message: {disconnectMessage}"); + NetworkLifecycle.Instance.Stop(); TrainStress.globalIgnoreStressCalculation = false; if (MainMenuThingsAndStuff.Instance != null) { - MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); + //MainMenuThingsAndStuff.Instance.SwitchToDefaultMenu(); NetworkLifecycle.Instance.TriggerMainMenuEventLater(); } else @@ -135,96 +193,98 @@ public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectI MainMenu.GoBackToMainMenu(); } - string text = $"{disconnectInfo.Reason}"; - - switch (disconnectInfo.Reason) - { - case DisconnectReason.DisconnectPeerCalled: - case DisconnectReason.ConnectionRejected: - netPacketProcessor.ReadAllPackets(disconnectInfo.AdditionalData); - return; - case DisconnectReason.RemoteConnectionClose: - text = "The server shut down"; - break; - } - - NetworkLifecycle.Instance.QueueMainMenuEvent(() => - { - Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - if (popup == null) - return; - popup.labelTMPro.text = text; - }); + onDisconnect(disconnectReason, disconnectMessage); } - public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) + public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) { Ping = latency; } - public override void OnConnectionRequest(ConnectionRequest request) + public override void OnConnectionRequest(NetDataReader dataReader, IConnectionRequest request) { - // todo + // Clients don't receive incomming requests. + request.Reject(); } #endregion - #region Listeners - private void OnClientboundServerDenyPacket(ClientboundServerDenyPacket packet) + #region Listeners + + private void OnClientboundLoginResponsePacket(ClientboundLoginResponsePacket packet) { - NetworkLifecycle.Instance.QueueMainMenuEvent(() => + + if (packet.Accepted) { - Popup popup = MainMenuThingsAndStuff.Instance.ShowOkPopup(); - if (popup == null) - return; - string text = Locale.Get(packet.ReasonKey, packet.ReasonArgs); + Log($"Received player accepted packet"); + + if (NetworkLifecycle.Instance.IsHost(SelfPeer)) + SendReadyPacket(); + else + SendSaveGameDataRequest(); - if (packet.Missing.Length != 0 || packet.Extra.Length != 0) - { - text += "\n\n"; - if (packet.Missing.Length != 0) - { - text += Locale.Get(Locale.DISCONN_REASON__MODS_MISSING_KEY, placeholders: string.Join("\n - ", packet.Missing)); - if (packet.Extra.Length != 0) - text += "\n"; - } + return; + } + + + string text = Locale.Get(packet.ReasonKey, packet.ReasonArgs); + if (packet.Missing.Length != 0 || packet.Extra.Length != 0) + { + text += "\n\n"; + if (packet.Missing.Length != 0) + { + text += Locale.Get(Locale.DISCONN_REASON__MODS_MISSING_KEY, placeholders: string.Join("\n - ", packet.Missing)); if (packet.Extra.Length != 0) - text += Locale.Get(Locale.DISCONN_REASON__MODS_EXTRA_KEY, placeholders: string.Join("\n - ", packet.Extra)); + text += "\n"; } - popup.labelTMPro.text = text; - }); + if (packet.Extra.Length != 0) + text += Locale.Get(Locale.DISCONN_REASON__MODS_EXTRA_KEY, placeholders: string.Join("\n - ", packet.Extra)); + } + + Log($"Received player deny packet: {text}"); + onDisconnect(DisconnectReason.ConnectionRejected, text); } private void OnClientboundPlayerJoinedPacket(ClientboundPlayerJoinedPacket packet) { - Guid guid = new(packet.Guid); - PlayerManager.AddPlayer(packet.Id, packet.Username, guid); - PlayerManager.UpdateCar(packet.Id, packet.TrainCar); - PlayerManager.UpdatePosition(packet.Id, packet.Position, Vector3.zero, packet.Rotation, false, packet.TrainCar != 0); + //Guid guid = new(packet.Guid); + ClientPlayerManager.AddPlayer(packet.Id, packet.Username); + + ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, Vector3.zero, packet.Rotation, false, packet.CarID != 0, packet.CarID); } + //For other player left the game private void OnClientboundPlayerDisconnectPacket(ClientboundPlayerDisconnectPacket packet) { - Log($"Received player disconnect packet (Id: {packet.Id})"); - PlayerManager.RemovePlayer(packet.Id); + Log($"Received player disconnect packet for player id: {packet.Id}"); + ClientPlayerManager.RemovePlayer(packet.Id); } - private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket packet) + + //For server shutting down / player kicked + private void OnClientboundDisconnectPacket(ClientboundDisconnectPacket packet) { - PlayerManager.UpdatePosition(packet.Id, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar); + if (packet.Kicked) + { + Log($"Player was kicked!"); + disconnectMessage = "You were kicked!"; + } + else + { + disconnectMessage = "Server Shutting Down"; + } } - - private void OnClientboundPlayerCarPacket(ClientboundPlayerCarPacket packet) + private void OnClientboundPlayerPositionPacket(ClientboundPlayerPositionPacket packet) { - PlayerManager.UpdateCar(packet.Id, packet.CarId); + ClientPlayerManager.UpdatePosition(packet.Id, packet.Position, packet.MoveDir, packet.RotationY, packet.IsJumping, packet.IsOnCar, packet.CarID); } private void OnClientboundPingUpdatePacket(ClientboundPingUpdatePacket packet) { - PlayerManager.UpdatePing(packet.Id, packet.Ping); + ClientPlayerManager.UpdatePing(packet.Id, packet.Ping); } private void OnClientboundTickSyncPacket(ClientboundTickSyncPacket packet) @@ -268,13 +328,26 @@ private void OnClientboundSaveGameDataPacket(ClientboundSaveGameDataPacket packe AStartGameData.DestroyAllInstances(); GameObject go = new("Server Start Game Data"); + + //create a new save and load it go.AddComponent().SetFromPacket(packet); + + //ensure save is not destroyed on scene switch Object.DontDestroyOnLoad(go); SceneSwitcher.SwitchToScene(DVScenes.Game); - WorldStreamingInit.LoadingFinished += SendReadyPacket; + WorldStreamingInit.LoadingFinished += () => + { + Log($"WorldStreamingInit.LoadingFinished()"); + NetworkedItemManager.Instance.CheckInstance(); + Log($"WorldStreamingInit.LoadingFinished() CacheWorldItems()"); + NetworkedItemManager.Instance.CacheWorldItems(); + Log($"WorldStreamingInit.LoadingFinished() SendReadyPacket()"); + SendReadyPacket(); + }; TrainStress.globalIgnoreStressCalculation = true; + } private void OnClientboundBeginWorldSyncPacket(ClientboundBeginWorldSyncPacket packet) @@ -308,6 +381,19 @@ private void OnClientboundRemoveLoadingScreen(ClientboundRemoveLoadingScreenPack } displayLoadingInfo.OnLoadingFinished(); + + //if not single player, add in chat + if (!isSinglePlayer) + { + GameObject common = GameObject.Find("[MAIN]/[GameUI]/[NewCanvasController]/Auxiliary Canvas, EventSystem, Input Module"); + if (common != null) + { + // + GameObject chat = new("Chat GUI", typeof(ChatGUI)); + chat.transform.SetParent(common.transform, false); + chatGUI = chat.GetComponent(); + } + } } private void OnClientboundTimeAdvancePacket(ClientboundTimeAdvancePacket packet) @@ -315,6 +401,41 @@ private void OnClientboundTimeAdvancePacket(ClientboundTimeAdvancePacket packet) TimeAdvance.AdvanceTime(packet.amountOfTimeToSkipInSeconds); } + //Force stations to be mapped to same netId across all clients and server - probably should implement for junctions, etc. + private void OnClientBoundStationControllerLookupPacket(ClientBoundStationControllerLookupPacket packet) + { + + if (packet == null) + { + LogError("OnClientBoundStationControllerLookupPacket received null packet"); + return; + } + + if (packet.NetID == null || packet.StationID == null) + { + LogError($"OnClientBoundStationControllerLookupPacket received packet with null arrays: NetID is null: {packet.NetID == null}, StationID is null: {packet.StationID == null}"); + return; + } + + + for (int i = 0; i < packet.NetID.Length; i++) + { + if (!NetworkedStationController.GetFromStationId(packet.StationID[i], out NetworkedStationController netStationCont)) + { + LogError($"OnClientBoundStationControllerLookupPacket() could not find station: {packet.StationID[i]}"); + } + else if (packet.NetID[i] > 0) + { + netStationCont.NetId = packet.NetID[i]; + } + else + { + LogError($"OnClientBoundStationControllerLookupPacket() station: {packet.StationID[i]} mapped to NetID 0"); + } + } + } + + private void OnClientboundRailwayStatePacket(ClientboundRailwayStatePacket packet) { for (int i = 0; i < packet.SelectedJunctionBranches.Length; i++) @@ -359,18 +480,18 @@ private void OnClientboundSpawnTrainCarPacket(ClientboundSpawnTrainCarPacket pac private void OnClientboundSpawnTrainSetPacket(ClientboundSpawnTrainSetPacket packet) { - LogDebug(() => - { - StringBuilder sb = new("Spawning trainset consisting of "); - foreach (TrainsetSpawnPart spawnPart in packet.SpawnParts) - sb.Append($"{spawnPart.CarId} ({spawnPart.LiveryId}) with net ID {spawnPart.NetId}, "); - return sb.ToString(); - }); + LogDebug(() => $"Spawning trainset consisting of {string.Join(", ", packet.SpawnParts.Select(p => $"{p.CarId} ({p.LiveryId}) with netId: {p.NetId}"))}"); - NetworkedCarSpawner.SpawnCars(packet.SpawnParts); + foreach (var part in packet.SpawnParts) + { + if (NetworkedTrainCar.GetTrainCarFromTrainId(part.CarId, out TrainCar car)) + { + LogError($"ClientboundSpawnTrainSetPacket() Tried to spawn trainset with carId: {part.CarId}, but car already exists!"); + return; + } + } - foreach (TrainsetSpawnPart spawnPart in packet.SpawnParts) - SendTrainSyncRequest(spawnPart.NetId); + NetworkedCarSpawner.SpawnCars(packet.SpawnParts, packet.AutoCouple); } private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket packet) @@ -378,7 +499,30 @@ private void OnClientboundDestroyTrainCarPacket(ClientboundDestroyTrainCarPacket if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - CarSpawner.Instance.DeleteCar(networkedTrainCar.TrainCar); + //Protect myself from getting deleted in race conditions + if (PlayerManager.Car == networkedTrainCar.TrainCar) + { + LogWarning($"Server attempted to delete car I'm on: {PlayerManager.Car?.ID}, net ID: {packet?.NetId}"); + PlayerManager.SetCar(null); + } + + //Protect other players from getting deleted in race conditions - this should be a temporary fix, if another playe's game object is deleted we should just recreate it + if(networkedTrainCar == null || networkedTrainCar.gameObject == null || networkedTrainCar.TrainCar == null) + { + LogDebug(() => $"OnClientboundDestroyTrainCarPacket({packet?.NetId}) networkedTrainCar: {networkedTrainCar != null}, go: {(networkedTrainCar?.gameObject) != null}, trainCar: {networkedTrainCar?.TrainCar != null}"); + } + else + { + NetworkedPlayer[] componentsInChildren = (networkedTrainCar?.gameObject != null) ? networkedTrainCar.GetComponentsInChildren() : []; + + foreach (NetworkedPlayer networkedPlayer in componentsInChildren) + { + networkedPlayer.UpdateCar(0); + } + + networkedTrainCar.TrainCar.UpdateJobIdOnCarPlates(string.Empty); + CarSpawner.Instance.DeleteCar(networkedTrainCar.TrainCar); + } } public void OnClientboundTrainPhysicsPacket(ClientboundTrainsetPhysicsPacket packet) @@ -386,43 +530,105 @@ public void OnClientboundTrainPhysicsPacket(ClientboundTrainsetPhysicsPacket pac NetworkTrainsetWatcher.Instance.Client_HandleTrainsetPhysicsUpdate(packet); } - private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) + private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out TrainCar otherTrainCar)) + if (!NetworkedTrainCar.Get(packet.NetId, out var netTrainCar)) + { + LogError($"OnCommonCouplerInteractionPacket netId: {packet.NetId}, TrainCar not found!"); return; + } + + netTrainCar.Common_ReceiveCouplerInteraction(packet); + } + private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet) + { + // TrainCar trainCar = null; + // TrainCar otherTrainCar = null; + + // if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out otherTrainCar)) + // { + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}, otherNetId: {packet.OtherNetId}, otherTrainCar found?: {otherTrainCar != null}"); + // return; + // } + + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID}"); - Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; - Coupler otherCoupler = packet.OtherCarIsFrontCoupler ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; + // Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; + // Coupler otherCoupler = packet.OtherCarIsFrontCoupler ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; - coupler.CoupleTo(otherCoupler, packet.PlayAudio, packet.ViaChainInteraction); + // if (coupler.CoupleTo(otherCoupler, packet.PlayAudio, false/*B99 packet.ViaChainInteraction*/) == null) + // LogDebug(() => $"OnCommonTrainCouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, otherNetId: {packet.OtherNetId}, otherTrainCar: {otherTrainCar.ID} Failed to couple!"); } private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) - return; + //if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + //{ + // LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar found?: {trainCar != null}"); + // return; + //} - Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; + //LogDebug(() => $"OnCommonTrainUncouplePacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFrontCoupler}, playAudio: {packet.PlayAudio}, DueToBrokenCouple: {packet.DueToBrokenCouple}, viaChainInteraction: {packet.ViaChainInteraction}"); - coupler.Uncouple(packet.PlayAudio, false, packet.DueToBrokenCouple, packet.ViaChainInteraction); + //Coupler coupler = packet.IsFrontCoupler ? trainCar.frontCoupler : trainCar.rearCoupler; + //coupler.Uncouple(packet.PlayAudio, false, packet.DueToBrokenCouple, false/*B99 packet.ViaChainInteraction*/); } private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar) || !NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out TrainCar otherTrainCar)) + bool foundTrainCar = NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar); + bool foundOtherTrainCar = NetworkedTrainCar.GetTrainCar(packet.OtherNetId, out TrainCar otherTrainCar); + + if (!foundTrainCar || trainCar == null || + !foundOtherTrainCar || otherTrainCar == null) + { + LogError($"OnCommonHoseConnectedPacket() netId: {packet.NetId}, trainCar found: {foundTrainCar}, trainCar is null: {trainCar == null}, otherNetId: {packet.OtherNetId}, otherTrainCar found: {foundOtherTrainCar}, other trainCar is null: {otherTrainCar == null}"); return; + } + + string carId = $"[{ trainCar?.ID}, { packet.NetId}]"; + string otherCarId = $"[{ otherTrainCar?.ID}, { packet.OtherNetId}]"; + + LogDebug(() => $"OnCommonHoseConnectedPacket() trainCar: {carId}, isFront: {packet.IsFront}, otherTrainCar: {otherCarId}, isFront: {packet.OtherIsFront}, playAudio: {packet.PlayAudio}"); Coupler coupler = packet.IsFront ? trainCar.frontCoupler : trainCar.rearCoupler; Coupler otherCoupler = packet.OtherIsFront ? otherTrainCar.frontCoupler : otherTrainCar.rearCoupler; - coupler.ConnectAirHose(otherCoupler, packet.PlayAudio); + if (coupler == null || coupler.hoseAndCock == null || + otherCoupler == null || otherCoupler.hoseAndCock == null) + { + LogError($"OnCommonHoseConnectedPacket() trainCar: {carId}, coupler found: {coupler != null}, otherCoupler found: {otherCoupler != null}, hoseAndCock found: {coupler.hoseAndCock != null}, otherHoseAndCock found: {otherCoupler.hoseAndCock != null}"); + return; + } + + if (coupler.hoseAndCock.IsHoseConnected || otherCoupler.hoseAndCock.IsHoseConnected) + { + Coupler connectedTo = null; + Coupler otherConnectedTo = null; + + if(coupler?.hoseAndCock?.connectedTo != null) + NetworkedTrainCar.TryGetCoupler(coupler.hoseAndCock.connectedTo, out connectedTo); + if(otherCoupler?.hoseAndCock?.connectedTo != null) + NetworkedTrainCar.TryGetCoupler(otherCoupler.hoseAndCock.connectedTo, out otherConnectedTo); + + LogWarning($"OnCommonHoseConnectedPacket() trainCar: {carId}, isFront: {packet.IsFront}, IsHoseConnected: {coupler?.hoseAndCock?.IsHoseConnected}, connectedTo: {connectedTo?.train?.ID}," + + $" otherTrainCar: {otherCarId}, other isFront: {otherCoupler?.isFrontCoupler}, other IsHoseConnected: {otherCoupler?.hoseAndCock?.IsHoseConnected}, other connectedTo: {otherConnectedTo?.train?.ID}"); + } + else + { + coupler.ConnectAirHose(otherCoupler, packet.PlayAudio); + } } private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet) { - if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar netTrainCar) || netTrainCar.IsDestroying) return; + TrainCar trainCar = netTrainCar.TrainCar; + + LogDebug(() => $"OnCommonHoseDisconnectedPacket() netId: {packet.NetId}, trainCar: {trainCar.ID}, isFront: {packet.IsFront}, playAudio: {packet.PlayAudio}"); + Coupler coupler = packet.IsFront ? trainCar.frontCoupler : trainCar.rearCoupler; coupler.DisconnectAirHose(packet.PlayAudio); @@ -434,7 +640,7 @@ private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet) return; MultipleUnitCable cable = packet.IsFront ? trainCar.muModule.frontCable : trainCar.muModule.rearCable; - MultipleUnitCable otherCable = packet.IsFront ? otherTrainCar.muModule.frontCable : otherTrainCar.muModule.rearCable; + MultipleUnitCable otherCable = packet.OtherIsFront ? otherTrainCar.muModule.frontCable : otherTrainCar.muModule.rearCable; cable.Connect(otherCable, packet.PlayAudio); } @@ -491,6 +697,26 @@ private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet) networkedTrainCar.Common_UpdateFuses(packet); } + private void OnClientboundBrakeStateUpdatePacket(ClientboundBrakeStateUpdatePacket packet) + { + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + + networkedTrainCar.Client_ReceiveBrakeStateUpdate(packet); + + //LogDebug(() => $"Received Brake Pressures netId {packet.NetId}: {packet.MainReservoirPressure}, {packet.IndependentPipePressure}, {packet.BrakePipePressure}, {packet.BrakeCylinderPressure}"); + } + + private void OnClientboundFireboxStatePacket(ClientboundFireboxStatePacket packet) + { + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + + networkedTrainCar.Client_ReceiveFireboxStateUpdate(packet.Contents, packet.IsOn); + } + private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) { if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) @@ -499,17 +725,59 @@ private void OnClientboundCargoStatePacket(ClientboundCargoStatePacket packet) networkedTrainCar.CargoModelIndex = packet.CargoModelIndex; Car logicCar = networkedTrainCar.TrainCar.logicCar; + if (logicCar == null) + { + Multiplayer.LogWarning($"OnClientboundCargoStatePacket() Failed to find logic car for [{networkedTrainCar.TrainCar.ID}, {packet.NetId}] is initialised: {networkedTrainCar.Client_Initialized}"); + return; + } + if (packet.CargoType == (ushort)CargoType.None && logicCar.CurrentCargoTypeInCar == CargoType.None) return; + //packet.CargoAmount is the total amount, not the amount to load/unload float cargoAmount = Mathf.Clamp(packet.CargoAmount, 0, logicCar.capacity); // todo: cache warehouse machine WarehouseMachine warehouse = string.IsNullOrEmpty(packet.WarehouseMachineId) ? null : JobSaveManager.Instance.GetWarehouseMachineWithId(packet.WarehouseMachineId); if (packet.IsLoading) - logicCar.LoadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + { + //Check correct cargo is loaded and the amount is correct + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + return; + + //We need either no cargo or the same cargo - if it's different, we need to remove it first + if (logicCar.CurrentCargoTypeInCar != CargoType.None && logicCar.CurrentCargoTypeInCar != (CargoType)packet.CargoType) + logicCar.DumpCargo(); + + //We have the correct cargo, but not the right amount, calculate the delta + if (logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + cargoAmount -= logicCar.LoadedCargoAmount; + + if (cargoAmount > 0) + logicCar.LoadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + } else - logicCar.UnloadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + { + //Check correct cargo is loaded and the amount is correct + if (logicCar.LoadedCargoAmount == cargoAmount && logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + return; + + //If there is different cargo we need to remove it, then load the appropriate amount + if (logicCar.CurrentCargoTypeInCar == CargoType.None || logicCar.CurrentCargoTypeInCar != (CargoType)packet.CargoType) + { + //avoid triggering the load event by backdooring it + logicCar.LastUnloadedCargoType = logicCar.CurrentCargoTypeInCar; + logicCar.CurrentCargoTypeInCar = (CargoType)packet.CargoType; + logicCar.LoadedCargoAmount = cargoAmount; + } + + //We have the correct cargo, calculate the delta + if (logicCar.CurrentCargoTypeInCar == (CargoType)packet.CargoType) + cargoAmount = logicCar.LoadedCargoAmount - cargoAmount; + + if (cargoAmount > 0) + logicCar.UnloadCargo(cargoAmount, (CargoType)packet.CargoType, warehouse); + } } private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket packet) @@ -530,10 +798,14 @@ private void OnClientboundCarHealthUpdatePacket(ClientboundCarHealthUpdatePacket private void OnClientboundRerailTrainPacket(ClientboundRerailTrainPacket packet) { + if (!NetworkedTrainCar.GetTrainCar(packet.NetId, out TrainCar trainCar)) return; if (!NetworkedRailTrack.Get(packet.TrackId, out NetworkedRailTrack networkedRailTrack)) return; + + Log($"Rerailing [{trainCar?.ID}, {packet.NetId}] to track {networkedRailTrack?.RailTrack?.logicTrack?.ID}"); + LogDebug(() => $"Rerailing [{trainCar?.ID}, {packet.NetId}] track: [{networkedRailTrack?.RailTrack?.logicTrack?.ID}, {packet.TrackId}], raw position: {packet.Position}, adjusted position: {packet.Position + WorldMover.currentMove}, forward: {packet.Forward}"); trainCar.Rerail(networkedRailTrack.RailTrack, packet.Position + WorldMover.currentMove, packet.Forward); } @@ -579,7 +851,7 @@ private void OnClientboundLicenseAcquiredPacket(ClientboundLicenseAcquiredPacket LicenseManager.Instance.AcquireGeneralLicense(Globals.G.Types.generalLicenses.Find(l => l.id == packet.Id)); foreach (CareerManagerLicensesScreen screen in Object.FindObjectsOfType()) - screen.PopulateLicensesTextsFromIndex(screen.indexOfFirstDisplayedLicense); + screen.PopulateTextsFromIndex(screen.IndexOfFirstDisplayedEntry); //B99 } private void OnClientboundGarageUnlockPacket(ClientboundGarageUnlockPacket packet) @@ -592,6 +864,116 @@ private void OnClientboundDebtStatusPacket(ClientboundDebtStatusPacket packet) { CareerManagerDebtControllerPatch.HasDebt = packet.HasDebt; } + private void OnCommonChatPacket(CommonChatPacket packet) + { + chatGUI?.ReceiveMessage(packet.message); + } + + + private void OnClientboundJobsCreatePacket(ClientboundJobsCreatePacket packet) + { + if (NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController)) + { + LogError($"OnClientboundJobsCreatePacket() {packet.StationNetId} does not exist!"); + return; + } + + Log($"Received {packet.Jobs.Length} jobs for station {networkedStationController.StationController.logicStation.ID}"); + + networkedStationController.AddJobs(packet.Jobs); + } + + private void OnClientboundJobsUpdatePacket(ClientboundJobsUpdatePacket packet) + { + if (NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController)) + { + LogError($"OnClientboundJobsUpdatePacket() {packet.StationNetId} does not exist!"); + return; + } + + Log($"Received {packet.JobUpdates.Length} job updates for station {networkedStationController.StationController.logicStation.ID}"); + + networkedStationController.UpdateJobs(packet.JobUpdates); + } + + + private void OnClientboundJobValidateResponsePacket(ClientboundJobValidateResponsePacket packet) + { + Log($"Job validation response received JobNetId: {packet.JobNetId}, Status: {packet.Invalid}"); + + if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) + return; + + Object.Destroy(networkedJob.gameObject); + } + + private void OnCommonItemChangePacket(CommonItemChangePacket packet) + { + //LogDebug(() => $"OnCommonItemChangePacket({packet?.Items?.Count})"); + + + //Multiplayer.LogDebug(() => + //{ + // string debug = ""; + + // foreach (var item in packet?.Items) + // { + // debug += "UpdateType: " + item?.UpdateType + "\r\n"; + // debug += "itemNetId: " + item?.ItemNetId + "\r\n"; + // debug += "PrefabName: " + item?.PrefabName + "\r\n"; + // debug += "Equipped: " + item?.ItemState + "\r\n"; + // debug += "Position: " + item?.ItemPosition + "\r\n"; + // debug += "Rotation: " + item?.ItemRotation + "\r\n"; + // debug += "ThrowDirection: " + item?.ThrowDirection + "\r\n"; + // debug += "Player: " + item?.Player + "\r\n"; + // debug += "CarNetId: " + item?.CarNetId + "\r\n"; + // debug += "AttachedFront: " + item?.AttachedFront + "\r\n"; + + // debug += $"States: {item?.States?.Count}\r\n"; + + // if (item.States != null) + // foreach (var state in item?.States) + // debug += "\t" + state.Key + ": " + state.Value + "\r\n"; + // else + // debug += "\r\n"; + // } + + // return debug; + //}); + + //NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, null); + } + + private void OnCommonPaintThemePacket(CommonPaintThemePacket packet) + { + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar netTrainCar)) + return; + + Log($"Received paint theme change for {netTrainCar?.CurrentID}"); + + PaintTheme paint = PaintThemeLookup.Instance.GetPaintTheme(packet.PaintThemeId); + + if (paint == null) + { + LogWarning($"Paint theme index {packet.PaintThemeId} does not exist!"); + return; + } + + if (!Enum.IsDefined(typeof(TrainCarPaint.Target), packet.TargetArea)) + { + LogWarning($"TrainCarPaint Target {packet.TargetArea} is not defined!"); + return; + } + + LogDebug(() => $"OnCommonPaintThemePacket() [{netTrainCar?.CurrentID}, {packet.NetId}], area: {(TrainCarPaint.Target)packet.TargetArea}, paint: [{paint?.assetName}, {packet.PaintThemeId}]"); + netTrainCar?.Common_ReceivePaintThemeUpdate((TrainCarPaint.Target)packet.TargetArea, paint); + } #endregion @@ -602,6 +984,11 @@ private void OnClientboundDebtStatusPacket(ClientboundDebtStatusPacket packet) SendPacket(serverPeer, packet, deliveryMethod); } + private void SendNetSerializablePacketToServer(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() + { + SendNetSerializablePacket(serverPeer, packet, deliveryMethod); + } + public void SendSaveGameDataRequest() { SendPacketToServer(new ServerboundSaveGameDataRequestPacket(), DeliveryMethod.ReliableOrdered); @@ -613,33 +1000,32 @@ private void SendReadyPacket() SendPacketToServer(new ServerboundClientReadyPacket(), DeliveryMethod.ReliableOrdered); } - public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotationY, bool isJumping, bool isOnCar, bool reliable) + public void SendPlayerPosition(Vector3 position, Vector3 moveDir, float rotationY, ushort carId, bool isJumping, bool isOnCar, bool reliable) { - SendPacketToServer(new ServerboundPlayerPositionPacket { + //LogDebug(() => $"SendPlayerPosition({position}, {moveDir}, {rotationY}, {carId}, {isJumping}, {isOnCar})"); + + SendPacketToServer(new ServerboundPlayerPositionPacket + { Position = position, MoveDir = new Vector2(moveDir.x, moveDir.z), RotationY = rotationY, - IsJumpingIsOnCar = (byte)((isJumping ? 1 : 0) | (isOnCar ? 2 : 0)) + IsJumpingIsOnCar = (byte)((isJumping ? 1 : 0) | (isOnCar ? 2 : 0)), + CarID = carId }, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Sequenced); } - public void SendPlayerCar(ushort carId) - { - SendPacketToServer(new ServerboundPlayerCarPacket { - CarId = carId - }, DeliveryMethod.ReliableOrdered); - } - public void SendTimeAdvance(float amountOfTimeToSkipInSeconds) { - SendPacketToServer(new ServerboundTimeAdvancePacket { + SendPacketToServer(new ServerboundTimeAdvancePacket + { amountOfTimeToSkipInSeconds = amountOfTimeToSkipInSeconds }, DeliveryMethod.ReliableUnordered); } public void SendJunctionSwitched(ushort netId, byte selectedBranch, Junction.SwitchMode mode) { - SendPacketToServer(new CommonChangeJunctionPacket { + SendPacketToServer(new CommonChangeJunctionPacket + { NetId = netId, SelectedBranch = selectedBranch, Mode = (byte)mode @@ -648,18 +1034,62 @@ public void SendJunctionSwitched(ushort netId, byte selectedBranch, Junction.Swi public void SendTurntableRotation(byte netId, float rotation) { - SendPacketToServer(new CommonRotateTurntablePacket { + SendPacketToServer(new CommonRotateTurntablePacket + { NetId = netId, rotation = rotation }, DeliveryMethod.ReliableOrdered); } + public void SendCouplerInteraction(CouplerInteractionType flags, Coupler coupler, Coupler otherCoupler = null) + { + ushort couplerNetId = coupler?.train?.GetNetId() ?? 0; + ushort otherCouplerNetId = otherCoupler?.train?.GetNetId() ?? 0; + bool couplerIsFront = coupler?.isFrontCoupler ?? false; + bool otherCouplerIsFront = otherCoupler?.isFrontCoupler ?? false; + + if (couplerNetId == 0) + { + LogWarning($"SendCouplerInteraction failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + + LogDebug(() => $"SendCouplerInteraction([{flags}], {coupler?.train?.ID}, {otherCoupler?.train?.ID}) coupler isFront: {couplerIsFront}, otherCoupler isFront: {otherCouplerIsFront}"); + + if (coupler == null) + return; + + Log($"Sending coupler interaction [{flags}] for {coupler?.train?.ID}, {(couplerIsFront ? "Front" : "Rear")}"); + + SendPacketToServer(new CommonCouplerInteractionPacket + { + NetId = couplerNetId, + IsFrontCoupler = couplerIsFront, + OtherNetId = otherCouplerNetId, + IsFrontOtherCoupler = otherCouplerIsFront, + Flags = (ushort)flags, + }, DeliveryMethod.ReliableOrdered); + } + + public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudio, bool viaChainInteraction) { - SendPacketToServer(new CommonTrainCouplePacket { - NetId = coupler.train.GetNetId(), + ushort couplerNetId = coupler.train.GetNetId(); + ushort otherCouplerNetId = otherCoupler.train.GetNetId(); + + if (couplerNetId == 0 || otherCouplerNetId == 0) + { + LogWarning($"SendTrainCouple failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); + return; + } + + SendPacketToServer(new CommonTrainCouplePacket + { + NetId = couplerNetId, //coupler.train.GetNetId(), IsFrontCoupler = coupler.isFrontCoupler, - OtherNetId = otherCoupler.train.GetNetId(), + State = (byte)coupler.state, + OtherNetId = otherCouplerNetId, //otherCoupler.train.GetNetId(), + OtherState = (byte)otherCoupler.state, OtherCarIsFrontCoupler = otherCoupler.isFrontCoupler, PlayAudio = playAudio, ViaChainInteraction = viaChainInteraction @@ -668,8 +1098,17 @@ public void SendTrainCouple(Coupler coupler, Coupler otherCoupler, bool playAudi public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenCouple, bool viaChainInteraction) { - SendPacketToServer(new CommonTrainUncouplePacket { - NetId = coupler.train.GetNetId(), + ushort couplerNetId = coupler.train.GetNetId(); + + if (couplerNetId == 0) + { + LogWarning($"SendTrainUncouple failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + + SendPacketToServer(new CommonTrainUncouplePacket + { + NetId = couplerNetId, IsFrontCoupler = coupler.isFrontCoupler, PlayAudio = playAudio, ViaChainInteraction = viaChainInteraction, @@ -679,10 +1118,20 @@ public void SendTrainUncouple(Coupler coupler, bool playAudio, bool dueToBrokenC public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAudio) { - SendPacketToServer(new CommonHoseConnectedPacket { - NetId = coupler.train.GetNetId(), + ushort couplerNetId = coupler.train.GetNetId(); + ushort otherCouplerNetId = otherCoupler.train.GetNetId(); + + if (couplerNetId == 0 || otherCouplerNetId == 0) + { + LogWarning($"SendHoseConnected failed. Coupler: {coupler.name} {couplerNetId}, OtherCoupler: {otherCoupler.name} {otherCouplerNetId}"); + return; + } + + SendPacketToServer(new CommonHoseConnectedPacket + { + NetId = couplerNetId, IsFront = coupler.isFrontCoupler, - OtherNetId = otherCoupler.train.GetNetId(), + OtherNetId = otherCouplerNetId, OtherIsFront = otherCoupler.isFrontCoupler, PlayAudio = playAudio }, DeliveryMethod.ReliableUnordered); @@ -690,8 +1139,17 @@ public void SendHoseConnected(Coupler coupler, Coupler otherCoupler, bool playAu public void SendHoseDisconnected(Coupler coupler, bool playAudio) { - SendPacketToServer(new CommonHoseDisconnectedPacket { - NetId = coupler.train.GetNetId(), + ushort couplerNetId = coupler.train.GetNetId(); + + if (couplerNetId == 0) + { + LogWarning($"SendHoseDisconnected failed. Coupler: {coupler.name} {couplerNetId}"); + return; + } + + SendPacketToServer(new CommonHoseDisconnectedPacket + { + NetId = couplerNetId, IsFront = coupler.isFrontCoupler, PlayAudio = playAudio }, DeliveryMethod.ReliableUnordered); @@ -699,10 +1157,20 @@ public void SendHoseDisconnected(Coupler coupler, bool playAudio) public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCable, bool playAudio) { - SendPacketToServer(new CommonMuConnectedPacket { - NetId = cable.muModule.train.GetNetId(), + ushort cableNetId = cable.muModule.train.GetNetId(); + ushort otherCableNetId = otherCable.muModule.train.GetNetId(); + + if (cableNetId == 0 || otherCableNetId == 0) + { + LogWarning($"SendMuConnected failed. Cable: {cable.muModule.train.name} {cableNetId}, OtherCable: {otherCable.muModule.train.name} {otherCableNetId}"); + return; + } + + SendPacketToServer(new CommonMuConnectedPacket + { + NetId = cableNetId, IsFront = cable.isFront, - OtherNetId = otherCable.muModule.train.GetNetId(), + OtherNetId = otherCableNetId, OtherIsFront = otherCable.isFront, PlayAudio = playAudio }, DeliveryMethod.ReliableUnordered); @@ -710,7 +1178,9 @@ public void SendMuConnected(MultipleUnitCable cable, MultipleUnitCable otherCabl public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playAudio) { - SendPacketToServer(new CommonMuDisconnectedPacket { + + SendPacketToServer(new CommonMuDisconnectedPacket + { NetId = netId, IsFront = cable.isFront, PlayAudio = playAudio @@ -719,7 +1189,8 @@ public void SendMuDisconnected(ushort netId, MultipleUnitCable cable, bool playA public void SendCockState(ushort netId, Coupler coupler, bool isOpen) { - SendPacketToServer(new CommonCockFiddlePacket { + SendPacketToServer(new CommonCockFiddlePacket + { NetId = netId, IsFront = coupler.isFrontCoupler, IsOpen = isOpen @@ -728,31 +1199,60 @@ public void SendCockState(ushort netId, Coupler coupler, bool isOpen) public void SendBrakeCylinderReleased(ushort netId) { - SendPacketToServer(new CommonBrakeCylinderReleasePacket { + SendPacketToServer(new CommonBrakeCylinderReleasePacket + { NetId = netId }, DeliveryMethod.ReliableUnordered); } public void SendHandbrakePositionChanged(ushort netId, float position) { - SendPacketToServer(new CommonHandbrakePositionPacket { + SendPacketToServer(new CommonHandbrakePositionPacket + { NetId = netId, Position = position }, DeliveryMethod.ReliableOrdered); } + public void SendAddCoal(ushort netId, float coalMassDelta) + { + SendPacketToServer(new ServerboundAddCoalPacket + { + NetId = netId, + CoalMassDelta = coalMassDelta + }, DeliveryMethod.ReliableOrdered); + } + + public void SendFireboxIgnition(ushort netId) + { + SendPacketToServer(new ServerboundFireboxIgnitePacket + { + NetId = netId, + }, DeliveryMethod.ReliableOrdered); + } public void SendPorts(ushort netId, string[] portIds, float[] portValues) { - SendPacketToServer(new CommonTrainPortsPacket { + SendPacketToServer(new CommonTrainPortsPacket + { NetId = netId, PortIds = portIds, PortValues = portValues }, DeliveryMethod.ReliableOrdered); + + /* + string log=$"Sending ports netId: {netId}"; + for (int i = 0; i < portIds.Length; i++) { + log += $"\r\n\t{portIds[i]}: {portValues[i]}"; + } + + LogDebug(() => log); + */ } public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) { - SendPacketToServer(new CommonTrainFusesPacket { + SendPacketToServer(new CommonTrainFusesPacket + { NetId = netId, FuseIds = fuseIds, FuseValues = fuseValues @@ -761,21 +1261,24 @@ public void SendFuses(ushort netId, string[] fuseIds, bool[] fuseValues) public void SendTrainSyncRequest(ushort netId) { - SendPacketToServer(new ServerboundTrainSyncRequestPacket { + SendPacketToServer(new ServerboundTrainSyncRequestPacket + { NetId = netId }, DeliveryMethod.ReliableUnordered); } public void SendTrainDeleteRequest(ushort netId) { - SendPacketToServer(new ServerboundTrainDeleteRequestPacket { + SendPacketToServer(new ServerboundTrainDeleteRequestPacket + { NetId = netId }, DeliveryMethod.ReliableUnordered); } public void SendTrainRerailRequest(ushort netId, ushort trackId, Vector3 position, Vector3 forward) { - SendPacketToServer(new ServerboundTrainRerailRequestPacket { + SendPacketToServer(new ServerboundTrainRerailRequestPacket + { NetId = netId, TrackId = trackId, Position = position, @@ -785,11 +1288,45 @@ public void SendTrainRerailRequest(ushort netId, ushort trackId, Vector3 positio public void SendLicensePurchaseRequest(string id, bool isJobLicense) { - SendPacketToServer(new ServerboundLicensePurchaseRequestPacket { + SendPacketToServer(new ServerboundLicensePurchaseRequestPacket + { Id = id, IsJobLicense = isJobLicense }, DeliveryMethod.ReliableUnordered); } + public void SendJobValidateRequest(NetworkedJob job, NetworkedStationController station) + { + SendPacketToServer(new ServerboundJobValidateRequestPacket + { + JobNetId = job.NetId, + StationNetId = station.NetId, + validationType = job.ValidationType + }, DeliveryMethod.ReliableUnordered); + } + + public void SendChat(string message) + { + SendPacketToServer(new CommonChatPacket + { + message = message + }, DeliveryMethod.ReliableUnordered); + } + + public void SendItemsChangePacket(List items) + { + Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items"); + //SendPacketToServer(new CommonItemChangePacket { Items = items }, + // DeliveryMethod.ReliableUnordered); + + SendNetSerializablePacketToServer(new CommonItemChangePacket { Items = items }, + DeliveryMethod.ReliableOrdered); + } + + public void SendPaintThemeChangePacket(ushort netId, byte targetArea, sbyte themeIndex) + { + SendPacketToServer(new CommonPaintThemePacket { NetId = netId, TargetArea = targetArea, PaintThemeId = themeIndex }, DeliveryMethod.ReliableUnordered); + } + #endregion } diff --git a/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs new file mode 100644 index 0000000..532475f --- /dev/null +++ b/Multiplayer/Networking/Managers/Client/ServerBrowserClient.cs @@ -0,0 +1,251 @@ +//using System; +//using System.Net; +//using System.Collections.Generic; +//using LiteNetLib; +//using Multiplayer.Networking.Packets.Unconnected; +//using System.Threading.Tasks; +//using System.Diagnostics; +//using System.Linq; +//using Multiplayer.Networking.Data; +//using Steamworks; +//using System.Text; +//using Steamworks.Data; +//using UnityEngine; + + +//namespace Multiplayer.Networking.Managers.Client; + +//public class ServerBrowserClient : NetworkManager, IDisposable +//{ +// protected override string LogPrefix => "[SBClient]"; +// private class PingInfo +// { +// public Stopwatch Stopwatch { get; } = new Stopwatch(); +// public DateTime StartTime { get; private set; } +// public bool IPv4Received { get; set; } +// public bool IPv6Received { get; set; } +// public bool IPv4Sent { get; set; } +// public bool IPv6Sent { get; set; } + +// public void Start() +// { +// StartTime = DateTime.Now; +// Stopwatch.Start(); +// } +// } + +// private readonly Dictionary pingInfos = []; +// public Action OnPing; // serverId, pingTime, isIPv4 +// public Action OnDiscovery; // endPoint, serverId, serverData + +// private readonly int[] discoveryPorts = [8888, 8889, 8890]; + +// private const int PingTimeoutMs = 5000; // 5 seconds timeout + +// public ServerBrowserClient(Settings settings) : base(settings) +// { +// } + +// public void Start() +// { +// netManager.UseNativeSockets = true; +// netManager.IPv6Enabled = true; +// netManager.Start(); + +// netManager.UpdateTime = 0; +// } +// public override void Stop() +// { +// base.Stop(); +// Dispose(); +// } + +// public void Dispose() +// { +// foreach (var pingInfo in pingInfos.Values) +// { +// pingInfo.Stopwatch.Stop(); +// } +// pingInfos.Clear(); +// } +// private async Task CleanupTimedOutPings() +// { +// while (true) +// { +// await Task.Delay(PingTimeoutMs * 2); +// var now = DateTime.Now; +// var timedOutServers = pingInfos +// .Where(kvp => (now - kvp.Value.StartTime).TotalMilliseconds > PingTimeoutMs) +// .Select(kvp => kvp.Key) +// .ToList(); + +// foreach (var serverId in timedOutServers) +// { +// pingInfos.Remove(serverId); +// LogDebug(() => $"Cleaned up timed out ping for {serverId}"); +// } +// } +// } + +// private async Task StartTimeoutTask(string serverId) +// { +// await Task.Delay(PingTimeoutMs); +// if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) +// { +// pingInfo.Stopwatch.Stop(); +// //LogDebug(() => $"Ping timeout for {serverId}, elapsed: {pingInfo.Stopwatch.ElapsedMilliseconds}, IPv4: ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6: ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received}) "); + +// if (!pingInfo.IPv4Received && pingInfo.IPv4Sent) +// OnPing?.Invoke(serverId, -1, true); + +// if (!pingInfo.IPv6Received && pingInfo.IPv6Sent) +// OnPing?.Invoke(serverId, -1, false); + + +// pingInfos.Remove(serverId); +// } +// } + +// protected override void Subscribe() +// { +// netPacketProcessor.RegisterNestedType(LobbyServerData.Serialize, LobbyServerData.Deserialize); +// netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); +// netPacketProcessor.SubscribeReusable(OnUnconnectedDiscoveryPacket); +// } + +// #region Net Events + +// public override void OnPeerConnected(NetPeer peer) +// { +// } + +// public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) +// { +// } + +// public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) +// { +// } + +// public override void OnConnectionRequest(ConnectionRequest request) +// { +// } + +// #endregion + +// #region Listeners + +// private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) +// { +// string serverId = new Guid(packet.ServerID).ToString(); +// //Log($"OnUnconnectedPingPacket({serverId ?? ""}, {endPoint?.Address})"); + +// if (pingInfos.TryGetValue(serverId, out PingInfo pingInfo)) +// { +// int pingTime = (int)pingInfo.Stopwatch.ElapsedMilliseconds / 2; //game reports one-way ping, so we should do the same in the server browser + +// bool isIPv4 = endPoint.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork; + +// if (isIPv4) +// pingInfo.IPv4Received = true; +// else +// pingInfo.IPv6Received = true; + +// OnPing?.Invoke(serverId, pingTime, isIPv4); + +// //LogDebug(()=>$"OnUnconnectedPingPacket() serverId {serverId}, IPv4 ({pingInfo.IPv4Sent}, {pingInfo.IPv4Received}), IPv6 ({pingInfo.IPv6Sent}, {pingInfo.IPv6Received})"); +// if ((!pingInfo.IPv4Sent || pingInfo.IPv4Received) && (!pingInfo.IPv6Sent || pingInfo.IPv6Received)) +// { +// pingInfo.Stopwatch.Stop(); +// pingInfos.Remove(serverId); +// //LogDebug(()=>$"OnUnconnectedPingPacket() removed {serverId}"); +// } +// } +// } + +// private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPEndPoint endPoint) +// { +// //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address})"); + +// if (packet.IsResponse) +// { + +// //Log($"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint?.Address}) id: {packet.data.id}"); +// OnDiscovery?.Invoke(endPoint, packet.Data); +// } +// } + +// public override void OnConnecting(Connection connection, ConnectionInfo info) +// { +// throw new NotImplementedException(); +// } + +// public override void OnConnected(Connection connection, ConnectionInfo info) +// { +// throw new NotImplementedException(); +// } + +// public override void OnDisconnected(Connection connection, ConnectionInfo info) +// { +// throw new NotImplementedException(); +// } + +// public override void OnMessage(Connection connection, NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel) +// { +// throw new NotImplementedException(); +// } + +// #endregion + +// #region Senders +// public void SendUnconnectedPingPacket(string serverId, string ipv4, string ipv6, int port) +// { +// if (!Guid.TryParse(serverId, out Guid server)) +// { +// //LogError($"SendUnconnectedPingPacket({serverId}) failed to parse GUID"); +// return; +// } + +// PingInfo pingInfo = new(); +// pingInfos[serverId] = pingInfo; + +// //LogDebug(()=>$"Sending ping to {serverId} at IPv4: {ipv4}, IPv6: {ipv6}, Port: {port}"); +// var packet = new UnconnectedPingPacket { ServerID = server.ToByteArray() }; + +// pingInfo.Start(); + +// // Send to IPv4 if provided +// if (!string.IsNullOrEmpty(ipv4)) +// { +// SendUnconnectedPacket(packet, ipv4, port); +// pingInfo.IPv4Sent = true; +// } + +// // Send to IPv6 if provided +// if (!string.IsNullOrEmpty(ipv6)) +// { +// SendUnconnectedPacket(packet, ipv6, port); +// pingInfo.IPv6Sent = true; +// } + +// // Start a timeout task +// _ = StartTimeoutTask(serverId); +// } + +// public void SendDiscoveryRequest() +// { +// foreach (int port in discoveryPorts) +// { +// try +// { +// netManager.SendBroadcast(WritePacket(new UnconnectedDiscoveryPacket()), port); +// } +// catch (Exception ex) +// { +// Multiplayer.Log($"SendDiscoveryRequest() Broadcast error: {ex.Message}\r\n{ex.StackTrace}"); +// } +// } +// } +// #endregion + +//} diff --git a/Multiplayer/Networking/Managers/NetworkManager.cs b/Multiplayer/Networking/Managers/NetworkManager.cs index 93b5cd8..970384e 100644 --- a/Multiplayer/Networking/Managers/NetworkManager.cs +++ b/Multiplayer/Networking/Managers/NetworkManager.cs @@ -4,40 +4,59 @@ using LiteNetLib; using LiteNetLib.Utils; using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; using Multiplayer.Networking.Serialization; +using Multiplayer.Networking.TransportLayers; -namespace Multiplayer.Networking.Listeners; +namespace Multiplayer.Networking.Managers; -public abstract class NetworkManager : INetEventListener +public abstract class NetworkManager { protected readonly NetPacketProcessor netPacketProcessor; - protected readonly NetManager netManager; protected readonly NetDataWriter cachedWriter = new(); + private readonly ITransport transport; + protected readonly NetManager netManager; + protected abstract string LogPrefix { get; } - public NetStatistics Statistics => netManager.Statistics; - public bool IsRunning => netManager.IsRunning; + public NetStatistics Statistics => transport.Statistics; + public bool IsRunning => transport.IsRunning; public bool IsProcessingPacket { get; private set; } protected NetworkManager(Settings settings) { - netManager = new NetManager(this) { - DisconnectTimeout = 10000 - }; - netPacketProcessor = new NetPacketProcessor(netManager); + Multiplayer.LogDebug(() => $"NetworkManager Constructor"); + + netPacketProcessor = new NetPacketProcessor(); + //transport = new LiteNetLibTransport(); + transport = new SteamWorksTransport(); + + transport.OnConnectionRequest += OnConnectionRequest; + transport.OnPeerConnected += OnPeerConnected; + transport.OnPeerDisconnected += OnPeerDisconnected; + transport.OnNetworkReceive += OnNetworkReceive; + transport.OnNetworkError += OnNetworkError; + transport.OnNetworkLatencyUpdate += OnNetworkLatencyUpdate; + + RegisterNestedTypes(); + OnSettingsUpdated(settings); Settings.OnSettingsUpdated += OnSettingsUpdated; - // ReSharper disable once VirtualMemberCallInConstructor + Subscribe(); + } private void RegisterNestedTypes() { netPacketProcessor.RegisterNestedType(BogieData.Serialize, BogieData.Deserialize); + netPacketProcessor.RegisterNestedType(); + netPacketProcessor.RegisterNestedType(JobData.Serialize, JobData.Deserialize); netPacketProcessor.RegisterNestedType(ModInfo.Serialize, ModInfo.Deserialize); netPacketProcessor.RegisterNestedType(RigidbodySnapshot.Serialize, RigidbodySnapshot.Deserialize); + netPacketProcessor.RegisterNestedType(StationsChainNetworkData.Serialize, StationsChainNetworkData.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetMovementPart.Serialize, TrainsetMovementPart.Deserialize); netPacketProcessor.RegisterNestedType(TrainsetSpawnPart.Serialize, TrainsetSpawnPart.Deserialize); netPacketProcessor.RegisterNestedType(Vector2Serializer.Serialize, Vector2Serializer.Deserialize); @@ -46,27 +65,45 @@ private void RegisterNestedTypes() private void OnSettingsUpdated(Settings settings) { - if (netManager == null) - return; - netManager.NatPunchEnabled = settings.EnableNatPunch; - netManager.AutoRecycle = settings.ReuseNetPacketReaders; - netManager.UseNativeSockets = settings.UseNativeSockets; - netManager.EnableStatistics = settings.ShowStats; - netManager.SimulatePacketLoss = settings.SimulatePacketLoss; - netManager.SimulateLatency = settings.SimulateLatency; - netManager.SimulationPacketLossChance = settings.SimulationPacketLossChance; - netManager.SimulationMinLatency = settings.SimulationMinLatency; - netManager.SimulationMaxLatency = settings.SimulationMaxLatency; + transport?.UpdateSettings(settings); } public void PollEvents() { - netManager.PollEvents(); + //netManager.PollEvents(); + transport?.PollEvents(); } - public void Stop() + public virtual bool Start() + { + return transport.Start(); + } + public virtual bool Start(IPAddress ipv4, IPAddress ipv6, int port) { - netManager.Stop(true); + return transport.Start(ipv4, ipv6, port); + } + public virtual bool Start(int port) + { + return transport.Start(port); + } + + protected virtual ITransportPeer Connect(string address, int port, NetDataWriter netDataWriter) + { + return transport.Connect(address, port, netDataWriter); + } + + + public virtual void Stop() + { + transport.Stop(true); + + transport.OnConnectionRequest -= OnConnectionRequest; + transport.OnPeerConnected -= OnPeerConnected; + transport.OnPeerDisconnected -= OnPeerDisconnected; + transport.OnNetworkReceive -= OnNetworkReceive; + transport.OnNetworkError -= OnNetworkError; + transport.OnNetworkLatencyUpdate -= OnNetworkLatencyUpdate; + Settings.OnSettingsUpdated -= OnSettingsUpdated; } @@ -77,17 +114,34 @@ public void Stop() return cachedWriter; } - protected void SendPacket(NetPeer peer, T packet, DeliveryMethod deliveryMethod) where T : class, new() + protected NetDataWriter WriteNetSerializablePacket(T packet) where T : INetSerializable, new() + { + cachedWriter.Reset(); + netPacketProcessor.WriteNetSerializable(cachedWriter, ref packet); + return cachedWriter; + } + + protected void SendPacket(ITransportPeer peer, T packet, DeliveryMethod deliveryMethod) where T : class, new() { peer?.Send(WritePacket(packet), deliveryMethod); } + protected void SendNetSerializablePacket(ITransportPeer peer, T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() + { + peer?.Send(WriteNetSerializablePacket(packet), deliveryMethod); + } + + //protected void SendUnconnectedPacket(T packet, string ipAddress, int port) where T : class, new() + //{ + // transport.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); + //} + protected abstract void Subscribe(); #region Net Events - - public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) + public void OnNetworkReceive(ITransportPeer peer, NetDataReader reader, byte channel, DeliveryMethod deliveryMethod) { + //LogDebug(() => $"NetworkManager.OnNetworkReceive()"); try { IsProcessingPacket = true; @@ -95,7 +149,7 @@ public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelN } catch (ParseException e) { - Multiplayer.LogWarning($"Failed to parse packet: {e.Message}"); + Multiplayer.LogWarning($"[{GetType()}] Failed to parse packet: {e.Message}\r\n{e.StackTrace}"); } finally { @@ -110,13 +164,27 @@ public void OnNetworkError(IPEndPoint endPoint, SocketError socketError) public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) { - // todo + //Multiplayer.Log($"OnNetworkReceiveUnconnected({remoteEndPoint}, {messageType})"); + try + { + IsProcessingPacket = true; + netPacketProcessor.ReadAllPackets(reader, remoteEndPoint); + } + catch (ParseException e) + { + Multiplayer.LogWarning($"Failed to parse packet: {e.Message}"); + } + finally + { + IsProcessingPacket = false; + } } - public abstract void OnPeerConnected(NetPeer peer); - public abstract void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo); - public abstract void OnNetworkLatencyUpdate(NetPeer peer, int latency); - public abstract void OnConnectionRequest(ConnectionRequest request); + //Standard networking callbacks + public abstract void OnPeerConnected(ITransportPeer peer); + public abstract void OnPeerDisconnected(ITransportPeer peer, DisconnectReason disconnectInfo); + public abstract void OnConnectionRequest(NetDataReader requestData, IConnectionRequest request); + public abstract void OnNetworkLatencyUpdate(ITransportPeer peer, int latency); #endregion diff --git a/Multiplayer/Networking/Managers/Server/ChatManager.cs b/Multiplayer/Networking/Managers/Server/ChatManager.cs new file mode 100644 index 0000000..27769f9 --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/ChatManager.cs @@ -0,0 +1,263 @@ +using Multiplayer.Components.Networking; +using System.Linq; +using Multiplayer.Networking.Data; +using System.Text.RegularExpressions; +using Multiplayer.Networking.TransportLayers; + +namespace Multiplayer.Networking.Managers.Server; + +public static class ChatManager +{ + public const string COMMAND_SERVER = "server"; + public const string COMMAND_SERVER_SHORT = "s"; + public const string COMMAND_WHISPER = "whisper"; + public const string COMMAND_WHISPER_SHORT = "w"; + public const string COMMAND_HELP_SHORT = "?"; + public const string COMMAND_HELP = "help"; + public const string COMMAND_LOG = "log"; + public const string COMMAND_LOG_SHORT = "l"; + public const string COMMAND_KICK = "kick"; + //public const string COMMAND_KICK_SHORT = "kick"; + + public const string MESSAGE_COLOUR_SERVER = "9CDCFE"; + public const string MESSAGE_COLOUR_HELP = "00FF00"; + + public static void ProcessMessage(string message, ITransportPeer sender) + { + + if (message == null || message == string.Empty) + return; + + //Check we could find the sender player data + if (!NetworkLifecycle.Instance.Server.TryGetServerPlayer(sender, out var player)) + return; + + + //Check if we have a command + if (message.StartsWith("/")) + { + string command = message.Substring(1).Split(' ')[0]; + + switch (command) + { + case COMMAND_SERVER_SHORT: + ServerMessage(message, sender, null, COMMAND_SERVER_SHORT.Length); + break; + case COMMAND_SERVER: + ServerMessage(message, sender, null, COMMAND_SERVER.Length); + break; + + case COMMAND_WHISPER_SHORT: + WhisperMessage(message, COMMAND_WHISPER_SHORT.Length, player.Username, sender); + break; + case COMMAND_WHISPER: + WhisperMessage(message, COMMAND_WHISPER.Length, player.Username, sender); + break; + + case COMMAND_HELP_SHORT: + HelpMessage(sender); + break; + case COMMAND_HELP: + HelpMessage(sender); + break; + + case COMMAND_KICK: + KickMessage(message, COMMAND_KICK.Length, player.Username, sender); + break; + +#if DEBUG + case COMMAND_LOG_SHORT: + Multiplayer.specLog = !Multiplayer.specLog; + break; + case COMMAND_LOG: + Multiplayer.specLog = !Multiplayer.specLog; + break; +#endif + + //allow messages that are not commands to go through + default: + ChatMessage(message,player.Username, sender); + break; + } + + return; + + } + + //not a server command, process as normal message + ChatMessage(message, player.Username, sender); + } + + private static void ChatMessage(string message, string sender, ITransportPeer peer) + { + //clean up the message to stop format injection + message = Regex.Replace(message, "", string.Empty, RegexOptions.IgnoreCase); + + message = $"{sender}: {message}"; + NetworkLifecycle.Instance.Server.SendChat(message, peer); + } + + public static void ServerMessage(string message, ITransportPeer sender, ITransportPeer exclude = null, int commandLength =-1) + { + //If user is not the host, we should ignore - will require changes for dedicated server + if (sender !=null && !NetworkLifecycle.Instance.IsHost(sender)) + return; + + //Remove the command "/server" or "/s" + if (commandLength > 0) + { + message = message.Substring(commandLength + 2); + } + + message = $"{message}"; + NetworkLifecycle.Instance.Server.SendChat(message, exclude); + } + + private static void WhisperMessage(string message, int commandLength, string senderName, ITransportPeer sender) + { + ITransportPeer recipient; + string recipientName; + + Multiplayer.Log($"Whispering: \"{message}\", sender: {senderName}, senderID: {sender?.Id}"); + + //Remove the command "/whisper" or "/w" + message = message.Substring(commandLength + 2); + + if (message == null || message == string.Empty) + return; + + /* + //Check if name is in Quotes e.g. '/w "Mr Noname" my message' + if (message.StartsWith("\"")) + { + int endQuote = message.Substring(1).IndexOf('"'); + if (endQuote == -1 || endQuote == 0) + return; + + recipientName = message.Substring(1, endQuote); + + //Remove the peer name + message = message.Substring(recipientName.Length + 3); + } + else + {*/ + recipientName = message.Split(' ')[0]; + + //Remove the peer name + message = message.Substring(recipientName.Length + 1); + //} + + Multiplayer.Log($"Whispering parse 1: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}"); + + //look up the peer ID + recipient = NetPeerFromName(recipientName); + if(recipient == null) + { + Multiplayer.Log($"Whispering failed: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}"); + + message = $"{recipientName} not found - you're whispering into the void!"; + NetworkLifecycle.Instance.Server.SendWhisper(message, sender); + return; + } + + Multiplayer.Log($"Whispering parse 2: \"{message}\", sender: {senderName}, senderID: {sender?.Id}, peerName: {recipientName}, peerID: {recipient?.Id}"); + + //clean up the message to stop format injection + message = Regex.Replace(message, "", string.Empty, RegexOptions.IgnoreCase); + + message = "" + senderName + ": " + message + ""; + + NetworkLifecycle.Instance.Server.SendWhisper(message, recipient); + } + + public static void KickMessage(string message, int commandLength, string senderName, ITransportPeer sender) + { + ITransportPeer player; + string playerName; + + //If user is not the host, we should ignore - will require changes for dedicated server + if (sender != null && !NetworkLifecycle.Instance.IsHost(sender)) + return; + + //Remove the command "/server" or "/s" + if (commandLength > 0) + { + message = message.Substring(commandLength + 2); + } + + playerName = message.Split(' ')[0]; + + player = NetPeerFromName(playerName); + + if (player == null || NetworkLifecycle.Instance.IsHost(player)) + { + message = $"Unable to kick {playerName}"; + } + else + { + message = $"{playerName} was kicked"; + + NetworkLifecycle.Instance.Server.KickPlayer(player); + } + + NetworkLifecycle.Instance.Server.SendWhisper(message, sender); + } + + private static void HelpMessage(ITransportPeer peer) + { + string message = $"{Locale.CHAT_HELP_AVAILABLE}" + + + $"\r\n\r\n\t{Locale.CHAT_HELP_SERVER_MSG}" + + $"\r\n\t\t/server <{Locale.CHAT_HELP_MSG}>" + + $"\r\n\t\t/s <{Locale.CHAT_HELP_MSG}>" + + + $"\r\n\r\n\t{Locale.CHAT_HELP_WHISPER_MSG}" + + $"\r\n\t\t/whisper <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>" + + $"\r\n\t\t/w <{Locale.CHAT_HELP_PLAYER_NAME}> <{Locale.CHAT_HELP_MSG}>" + + + $"\r\n\r\n\t{Locale.CHAT_HELP_HELP}" + + "\r\n\t\t/help" + + "\r\n\t\t/?" + + + ""; + + /* + * $"Available commands:" + + + "\r\n\r\n\tSend a message as the server (host only)" + + "\r\n\t\t/server " + + "\r\n\t\t/s " + + + "\r\n\r\n\tWhisper to a player" + + "\r\n\t\t/whisper " + + "\r\n\t\t/w " + + + "\r\n\r\n\tDisplay this help message" + + "\r\n\t\t/help" + + "\r\n\t\t/?" + + + ""; + */ + NetworkLifecycle.Instance.Server.SendWhisper(message, peer); + } + + + private static ITransportPeer NetPeerFromName(string peerName) + { + + if(peerName == null || peerName == string.Empty) + return null; + + ServerPlayer player = NetworkLifecycle.Instance.Server.ServerPlayers.Where(p => p.Username == peerName).FirstOrDefault(); + if (player == null) + return null; + + if(NetworkLifecycle.Instance.Server.TryGetPeer(player.Id, out ITransportPeer peer)) + { + return peer; + } + + return null; + + } +} diff --git a/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs new file mode 100644 index 0000000..051fde7 --- /dev/null +++ b/Multiplayer/Networking/Managers/Server/LobbyServerManager.cs @@ -0,0 +1,500 @@ +using System; +using Multiplayer.Networking.Data; +using Newtonsoft.Json; +using System.Collections; +using UnityEngine; +using UnityEngine.Networking; +using Multiplayer.Components.Networking; +using DV.WeatherSystem; +using System.Text.RegularExpressions; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using LiteNetLib; +using LiteNetLib.Utils; +using Multiplayer.Networking.Packets.Unconnected; +using System.Net; +using System.Linq; +using Steamworks; +using Steamworks.Data; +using Multiplayer.Utils; +using Multiplayer.Networking.TransportLayers; + +namespace Multiplayer.Networking.Managers.Server; +public class LobbyServerManager : MonoBehaviour +{ + //API endpoints + private const string ENDPOINT_ADD_SERVER = "add_game_server"; + private const string ENDPOINT_UPDATE_SERVER = "update_game_server"; + private const string ENDPOINT_REMOVE_SERVER = "remove_game_server"; + + //RegEx + private readonly Regex IPv4Match = new(@"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}"); + + private const int REDIRECT_MAX = 5; + + private const int UPDATE_TIME_BUFFER = 10; //We don't want to miss our update, let's phone in just a little early + private const int UPDATE_TIME = 120 - UPDATE_TIME_BUFFER; //How often to update the lobby server - this should match the lobby server's time-out period + private const int PLAYER_CHANGE_TIME = 5; //Update server early if the number of players has changed in this time frame + + private NetworkServer server; + private string server_id; + private string private_key; + + //Steam Lobby + public static readonly string[] EXCLUDE_PARAMS = {"id", "ipv4", "ipv6", "port", "LocalIPv4", "LocalIPv6", "Ping", "isPublic", "LastSeen", "CurrentPlayers", "MaxPlayers"}; + private Lobby? lobby; + + private bool initialised = false; + private bool sendUpdates = false; + private float timePassed = 0f; + + //LAN discovery + private NetManager discoveryManager; + private NetPacketProcessor packetProcessor; + private EventBasedNetListener discoveryListener; + private readonly NetDataWriter cachedWriter = new(); + public static int[] discoveryPorts = [8888, 8889, 8890]; + + #region Unity + public void Awake() + { + server = NetworkLifecycle.Instance.Server; + + Multiplayer.Log($"LobbyServerManager New({server != null})"); + } + + public IEnumerator Start() + { + //Create a steam lobby + if (DVSteamworks.Success) + { + CreateSteamLobby(); + } + + //Register with old php lobby server (provides stats and makes the lobby visible, but not joinable to users on old versions) + server.serverData.ipv6 = GetStaticIPv6Address(); + server.serverData.LocalIPv4 = GetLocalIPv4Address(); + + StartCoroutine(GetIPv4(Multiplayer.Settings.Ipv4AddressCheck)); + + while(!initialised) + yield return null; + + server.Log("Public IPv4: " + server.serverData.ipv4); + server.Log("Public IPv6: " + server.serverData.ipv6); + server.Log("Private IPv4: " + server.serverData.LocalIPv4); + + if (server.serverData.isPublic) + { + Multiplayer.Log($"Registering server at: {Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}"); + StartCoroutine(RegisterWithLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}")); + + //allow the server some time to register (should take less than a second) + float timeout = 5f; + while (server_id == null || server_id == string.Empty || (timeout -= Time.deltaTime) <= 0) + yield return null; + + + } + + if(server_id == null || server_id == string.Empty) + { + server_id = Guid.NewGuid().ToString(); + } + + server.serverData.id = server_id; + + StartDiscoveryServer(); + } + + public void OnDestroy() + { + Multiplayer.Log($"LobbyServerManager OnDestroy()"); + sendUpdates = false; + StopAllCoroutines(); + StartCoroutine(RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_REMOVE_SERVER}")); + + + lobby?.SetJoinable(false); + lobby?.Leave(); + + discoveryManager?.Stop(); + } + + public void Update() + { + if (sendUpdates) + { + timePassed += Time.deltaTime; + + if (timePassed > UPDATE_TIME || (server.serverData.CurrentPlayers != server.PlayerCount && timePassed > PLAYER_CHANGE_TIME)) + { + timePassed = 0f; + server.serverData.CurrentPlayers = server.PlayerCount; + StartCoroutine(UpdateLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_UPDATE_SERVER}")); + + if(lobby != null) + { + SteamworksUtils.SetLobbyData((Lobby)lobby, server.serverData, EXCLUDE_PARAMS); + } + } + }else if (!server.serverData.isPublic || !sendUpdates) + { + server.serverData.CurrentPlayers = server.PlayerCount; + } + + //Keep LAN discovery running + discoveryManager?.PollEvents(); + } + + #endregion + + #region Steam Lobby + public async void CreateSteamLobby() + { + + // Specify the lobby type (public, private, etc.) + var result = await SteamMatchmaking.CreateLobbyAsync(server.serverData.MaxPlayers); + + if (result.HasValue) + { + // Lobby was created successfully + lobby = result.Value; + + server.Log("Steam Lobby created successfully!"); + server.LogDebug(() => $"Steam lobby ID: {lobby?.Id}"); + + lobby?.SetData(SteamworksUtils.LOBBY_MP_MOD_KEY, string.Empty); //We'll add this in for filtering + lobby?.SetData(SteamworksUtils.LOBBY_NET_LOCATION_KEY, SteamNetworkingUtils.LocalPingLocation.ToString()); //for ping estimation + + SteamworksUtils.SetLobbyData((Lobby)lobby, server.serverData, EXCLUDE_PARAMS); + + //todo implement public/private/friends + if (server.serverData.isPublic) + lobby?.SetPublic(); + else + lobby?.SetPrivate(); + + lobby?.SetJoinable(true); + } + else + { + // Handle failure + server.LogError("Failed to create lobby."); + } + } + #endregion + + #region Lobby Server + public void RemoveFromLobbyServer() + { + Multiplayer.Log($"RemoveFromLobbyServer OnDestroy()"); + sendUpdates = false; + StopAllCoroutines(); + StartCoroutine(RemoveFromLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_REMOVE_SERVER}")); + } + + private IEnumerator RegisterWithLobbyServer(string uri) + { + JsonSerializerSettings jsonSettings = new() { NullValueHandling = NullValueHandling.Ignore }; + string json = JsonConvert.SerializeObject(server.serverData, jsonSettings); + Multiplayer.LogDebug(()=>$"JsonRequest: {json}"); + + yield return SendWebRequest( + uri, + json, + webRequest => + { + LobbyServerResponseData response = JsonConvert.DeserializeObject(webRequest.downloadHandler.text); + if (response != null) + { + private_key = response.private_key; + server_id = response.game_server_id; + + sendUpdates = true; + } + }, + webRequest => Multiplayer.LogError("Failed to register with lobby server") + ); + } + + private IEnumerator RemoveFromLobbyServer(string uri) + { + JsonSerializerSettings jsonSettings = new() { NullValueHandling = NullValueHandling.Ignore }; + string json = JsonConvert.SerializeObject(new LobbyServerResponseData(server_id, private_key), jsonSettings); + Multiplayer.LogDebug(() => $"JsonRequest: {json}"); + + yield return SendWebRequest( + uri, + json, + webRequest => Multiplayer.Log("Successfully removed from lobby server"), + webRequest => Multiplayer.LogError("Failed to remove from lobby server") + ); + } + + private IEnumerator UpdateLobbyServer(string uri) + { + JsonSerializerSettings jsonSettings = new() { NullValueHandling = NullValueHandling.Ignore }; + + DateTime start = AStartGameData.BaseTimeAndDate; + DateTime current = WeatherDriver.Instance.manager.DateTime; + TimeSpan inGame = current - start; + + LobbyServerUpdateData reqData = new( + server_id, + private_key, + inGame.ToString("d\\d\\ hh\\h\\ mm\\m\\ ss\\s"), + server.serverData.CurrentPlayers + ); + + string json = JsonConvert.SerializeObject(reqData, jsonSettings); + Multiplayer.LogDebug(() => $"UpdateLobbyServer JsonRequest: {json}"); + + yield return SendWebRequest( + uri, + json, + webRequest => Multiplayer.Log("Successfully updated lobby server"), + webRequest => + { + Multiplayer.LogError("Failed to update lobby server, attempting to re-register"); + + //cleanup + sendUpdates = false; + private_key = null; + server_id = null; + + //Attempt to re-register + StartCoroutine(RegisterWithLobbyServer($"{Multiplayer.Settings.LobbyServerAddress}/{ENDPOINT_ADD_SERVER}")); + } + ); + } + + private IEnumerator GetIPv4(string uri) + { + + Multiplayer.Log("Preparing to get IPv4: " + uri); + + yield return SendWebRequestGET( + uri, + webRequest => + { + Match match = IPv4Match.Match(webRequest.downloadHandler.text); + if (match != null) + { + Multiplayer.Log($"IPv4 address extracted: {match.Value}"); + server.serverData.ipv4 = match.Value; + } + else + { + Multiplayer.LogError($"Failed to find IPv4 address. Server will only be available via IPv6"); + } + + initialised = true; + + }, + webRequest => + { + Multiplayer.LogError("Failed to find IPv4 address. Server will only be available via IPv6"); + initialised = true; + } + ); + } + + private IEnumerator SendWebRequest(string uri, string json, Action onSuccess, Action onError, int depth=0) + { + if (depth > REDIRECT_MAX) + { + Multiplayer.LogError($"Reached maximum redirects: {uri}"); + yield break; + } + + using UnityWebRequest webRequest = UnityWebRequest.Post(uri, json); + webRequest.redirectLimit = 0; + + if (json != null && json.Length > 0) + { + webRequest.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json)) { contentType = "application/json" }; + } + webRequest.downloadHandler = new DownloadHandlerBuffer(); + + yield return webRequest.SendWebRequest(); + + //check for redirect + if (webRequest.responseCode >= 300 && webRequest.responseCode < 400) + { + string redirectUrl = webRequest.GetResponseHeader("Location"); + Multiplayer.LogWarning($"Lobby Server redirected, check address is up to date: '{redirectUrl}'"); + + if (redirectUrl != null && redirectUrl.StartsWith("https://") && redirectUrl.Replace("https://", "http://") == uri) + { + yield return SendWebRequest(redirectUrl, json, onSuccess, onError, ++depth); + } + } + else + { + if (webRequest.isNetworkError || webRequest.isHttpError) + { + Multiplayer.LogError($"SendWebRequest({uri}) responseCode: {webRequest.responseCode}, Error: {webRequest.error}\r\n{webRequest.downloadHandler.text}"); + onError?.Invoke(webRequest); + } + else + { + Multiplayer.Log($"Received: {webRequest.downloadHandler.text}"); + onSuccess?.Invoke(webRequest); + } + } + } + + private IEnumerator SendWebRequestGET(string uri, Action onSuccess, Action onError, int depth = 0) + { + if (depth > REDIRECT_MAX) + { + Multiplayer.LogError($"Reached maximum redirects: {uri}"); + yield break; + } + + using UnityWebRequest webRequest = UnityWebRequest.Get(uri); + webRequest.redirectLimit = 0; + webRequest.downloadHandler = new DownloadHandlerBuffer(); + + yield return webRequest.SendWebRequest(); + + //check for redirect + if (webRequest.responseCode >= 300 && webRequest.responseCode < 400) + { + string redirectUrl = webRequest.GetResponseHeader("Location"); + Multiplayer.LogWarning($"Lobby Server redirected, check address is up to date: '{redirectUrl}'"); + + if (redirectUrl != null && redirectUrl.StartsWith("https://") && redirectUrl.Replace("https://", "http://") == uri) + { + yield return SendWebRequestGET(redirectUrl, onSuccess, onError, ++depth); + } + } + else + { + if (webRequest.isNetworkError || webRequest.isHttpError) + { + Multiplayer.LogError($"SendWebRequest({uri}) responseCode: {webRequest.responseCode}, Error: {webRequest.error}\r\n{webRequest.downloadHandler.text}"); + onError?.Invoke(webRequest); + } + else + { + Multiplayer.Log($"Received: {webRequest.downloadHandler.text}"); + onSuccess?.Invoke(webRequest); + } + } + } + #endregion + public static string GetStaticIPv6Address() + { + foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) + { + bool flag = !networkInterface.Supports(NetworkInterfaceComponent.IPv6) || networkInterface.OperationalStatus != OperationalStatus.Up || networkInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback || networkInterface.NetworkInterfaceType == NetworkInterfaceType.Tunnel; + if (!flag) + { + foreach (UnicastIPAddressInformation unicastIPAddressInformation in networkInterface.GetIPProperties().UnicastAddresses) + { + bool flag2 = unicastIPAddressInformation.Address.AddressFamily == AddressFamily.InterNetworkV6; + if (flag2) + { + bool flag3 = !unicastIPAddressInformation.Address.IsIPv6LinkLocal && !unicastIPAddressInformation.Address.IsIPv6SiteLocal && unicastIPAddressInformation.IsDnsEligible; + if (flag3) + { + return unicastIPAddressInformation.Address.ToString(); + } + } + } + } + } + return null; + } + + public static string GetLocalIPv4Address() + { + foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) + { + bool flag = !networkInterface.Supports(NetworkInterfaceComponent.IPv4) || networkInterface.OperationalStatus != OperationalStatus.Up || networkInterface.NetworkInterfaceType == NetworkInterfaceType.Loopback; + if (!flag) + { + IPInterfaceProperties properties = networkInterface.GetIPProperties(); + if (properties.GatewayAddresses.Count == 0) + continue; + + foreach (UnicastIPAddressInformation unicastIPAddressInformation in properties.UnicastAddresses) + { + bool flag2 = unicastIPAddressInformation.Address.AddressFamily == AddressFamily.InterNetwork; + if (flag2) + { + return unicastIPAddressInformation.Address.ToString(); + } + } + } + } + return null; + } + + #region LAN Discovery + public void StartDiscoveryServer() + { + server.Log($"StartDiscoveryServer()"); + discoveryListener = new EventBasedNetListener(); + discoveryManager = new NetManager(discoveryListener) + { + IPv6Enabled = true, + UnconnectedMessagesEnabled = true, + BroadcastReceiveEnabled = true, + + }; + packetProcessor = new NetPacketProcessor(); + + discoveryListener.NetworkReceiveUnconnectedEvent += OnNetworkReceiveUnconnected; + + packetProcessor.RegisterNestedType(LobbyServerData.Serialize, LobbyServerData.Deserialize); + packetProcessor.SubscribeReusable(OnUnconnectedDiscoveryPacket); + + //start listening for discovery packets + int successPort = discoveryPorts.FirstOrDefault(port => + discoveryManager.Start(IPAddress.Any, IPAddress.IPv6Any, port)); + + if (successPort != 0) + server.Log($"Discovery server started on port {successPort}"); + else + server.LogError("Failed to start discovery server on any port"); + } + protected NetDataWriter WritePacket(T packet) where T : class, new() + { + cachedWriter.Reset(); + packetProcessor.Write(cachedWriter, packet); + return cachedWriter; + } + protected void SendUnconnectedPacket(T packet, string ipAddress, int port) where T : class, new() + { + discoveryManager.SendUnconnectedMessage(WritePacket(packet), ipAddress, port); + } + public void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) + { + //server.Log($"LobbyServerManager.OnNetworkReceiveUnconnected({remoteEndPoint}, {messageType})"); + try + { + packetProcessor.ReadAllPackets(reader, remoteEndPoint); + } + catch (ParseException e) + { + server.LogWarning($"LobbyServerManager.OnNetworkReceiveUnconnected() Failed to parse packet: {e.Message}"); + } + } + + private void OnUnconnectedDiscoveryPacket(UnconnectedDiscoveryPacket packet, IPEndPoint endPoint) + { + //server.LogDebug(()=>$"OnUnconnectedDiscoveryPacket({packet.PacketType}, {endPoint.Address},{endPoint.Port})"); + + if (!packet.IsResponse) + { + packet.IsResponse = true; + packet.Data = server.serverData; + } + + SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); + } + #endregion +} diff --git a/Multiplayer/Networking/Managers/Server/NetworkServer.cs b/Multiplayer/Networking/Managers/Server/NetworkServer.cs index f5129b2..93f855e 100644 --- a/Multiplayer/Networking/Managers/Server/NetworkServer.cs +++ b/Multiplayer/Networking/Managers/Server/NetworkServer.cs @@ -14,8 +14,10 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using Multiplayer.Components.Networking.Jobs; using Multiplayer.Networking.Data; using Multiplayer.Networking.Packets.Clientbound; +using Multiplayer.Networking.Packets.Clientbound.Jobs; using Multiplayer.Networking.Packets.Clientbound.SaveGame; using Multiplayer.Networking.Packets.Clientbound.Train; using Multiplayer.Networking.Packets.Clientbound.World; @@ -25,125 +27,222 @@ using Multiplayer.Utils; using UnityEngine; using UnityModManagerNet; +using System.Net; +using Multiplayer.Networking.Packets.Serverbound.Train; +using Multiplayer.Networking.Packets.Unconnected; +using System.Text; +using Multiplayer.Networking.Data.Train; +using Multiplayer.Networking.TransportLayers; -namespace Multiplayer.Networking.Listeners; + +namespace Multiplayer.Networking.Managers.Server; public class NetworkServer : NetworkManager { + public Action PlayerDisconnect; protected override string LogPrefix => "[Server]"; - private readonly Queue joinQueue = new(); - private readonly Dictionary serverPlayers = new(); - private readonly Dictionary netPeers = new(); + private readonly Queue joinQueue = new(); + private readonly Dictionary serverPlayers = []; + private readonly Dictionary Peers = []; + + private LobbyServerManager lobbyServerManager; + public bool isSinglePlayer; + public LobbyServerData serverData; + public RerailController rerailController; public IReadOnlyCollection ServerPlayers => serverPlayers.Values; - public int PlayerCount => netManager.ConnectedPeersCount; + public int PlayerCount => ServerPlayers.Count; - private static NetPeer selfPeer => NetworkLifecycle.Instance.Client?.selfPeer; - public static byte SelfId => (byte)selfPeer.Id; + private static ITransportPeer SelfPeer => NetworkLifecycle.Instance.Client?.SelfPeer; + public static byte SelfId => (byte)SelfPeer.Id; private readonly ModInfo[] serverMods; public readonly IDifficulty Difficulty; private bool IsLoaded; - public NetworkServer(IDifficulty difficulty, Settings settings) : base(settings) + //we don't care if the client doesn't have these mods + public static string[] modWhiteList = ["RuntimeUnityEditor", "BookletOrganizer"]; + + public NetworkServer(IDifficulty difficulty, Settings settings, bool isSinglePlayer, LobbyServerData serverData) : base(settings) { + LogDebug(()=>$"NetworkServer Constructor"); + this.isSinglePlayer = isSinglePlayer; + this.serverData = serverData; + Difficulty = difficulty; - serverMods = ModInfo.FromModEntries(UnityModManager.modEntries); + + serverMods = ModInfo.FromModEntries(UnityModManager.modEntries) + .Where(mod => !modWhiteList.Contains(mod.Id)).ToArray(); + + } - public bool Start(int port) + public override bool Start(int port) { + //setup paint theme lookup cache + PaintThemeLookup.Instance.CheckInstance(); + WorldStreamingInit.LoadingFinished += OnLoaded; - return netManager.Start(port); + + Multiplayer.Log($"Starting server..."); + //Try to get our static IPv6 Address we will need this for IPv6 NAT punching to be reliable + if (IPAddress.TryParse(LobbyServerManager.GetStaticIPv6Address(), out IPAddress ipv6Address)) + { + //start the connection, IPv4 messages can come from anywhere, IPv6 messages need to specifically come from the static IPv6 + return base.Start(IPAddress.Any, ipv6Address,port); + + } + + //we're not running IPv6, start as normal + return base.Start(port); + } + + public override void Stop() + { + if (lobbyServerManager != null) + { + lobbyServerManager.RemoveFromLobbyServer(); + UnityEngine.Object.Destroy(lobbyServerManager); + } + + //Alert all clients (except h + var packet = WritePacket(new ClientboundDisconnectPacket()); + foreach (var peer in Peers.Values) + { + if (peer != SelfPeer) + peer?.Disconnect(packet); + } + + base.Stop(); } protected override void Subscribe() { - netPacketProcessor.SubscribeReusable(OnServerboundClientLoginPacket); - netPacketProcessor.SubscribeReusable(OnServerboundClientReadyPacket); - netPacketProcessor.SubscribeReusable(OnServerboundSaveGameDataRequestPacket); - netPacketProcessor.SubscribeReusable(OnServerboundPlayerPositionPacket); - netPacketProcessor.SubscribeReusable(OnServerboundPlayerCarPacket); - netPacketProcessor.SubscribeReusable(OnServerboundTimeAdvancePacket); + //Client management + netPacketProcessor.SubscribeReusable(OnServerboundClientLoginPacket); + + //World sync + netPacketProcessor.SubscribeReusable(OnServerboundClientReadyPacket); + netPacketProcessor.SubscribeReusable(OnServerboundSaveGameDataRequestPacket); + netPacketProcessor.SubscribeReusable(OnServerboundTimeAdvancePacket); + + + netPacketProcessor.SubscribeReusable(OnServerboundPlayerPositionPacket); netPacketProcessor.SubscribeReusable(OnServerboundTrainSyncRequestPacket); - netPacketProcessor.SubscribeReusable(OnServerboundTrainDeleteRequestPacket); - netPacketProcessor.SubscribeReusable(OnServerboundTrainRerailRequestPacket); - netPacketProcessor.SubscribeReusable(OnServerboundLicensePurchaseRequestPacket); - netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); - netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); - netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); - netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); - netPacketProcessor.SubscribeReusable(OnCommonMuConnectedPacket); - netPacketProcessor.SubscribeReusable(OnCommonMuDisconnectedPacket); - netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); - netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); - netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); - netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + netPacketProcessor.SubscribeReusable(OnServerboundTrainDeleteRequestPacket); + netPacketProcessor.SubscribeReusable(OnServerboundTrainRerailRequestPacket); + netPacketProcessor.SubscribeReusable(OnServerboundLicensePurchaseRequestPacket); + netPacketProcessor.SubscribeReusable(OnCommonChangeJunctionPacket); + netPacketProcessor.SubscribeReusable(OnCommonRotateTurntablePacket); + netPacketProcessor.SubscribeReusable(OnCommonCouplerInteractionPacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainCouplePacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainUncouplePacket); + netPacketProcessor.SubscribeReusable(OnCommonHoseConnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonHoseDisconnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonMuConnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonMuDisconnectedPacket); + netPacketProcessor.SubscribeReusable(OnCommonCockFiddlePacket); + netPacketProcessor.SubscribeReusable(OnCommonBrakeCylinderReleasePacket); + netPacketProcessor.SubscribeReusable(OnCommonHandbrakePositionPacket); + netPacketProcessor.SubscribeReusable(OnCommonPaintThemePacket); + netPacketProcessor.SubscribeReusable(OnServerboundAddCoalPacket); + netPacketProcessor.SubscribeReusable(OnServerboundFireboxIgnitePacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainPortsPacket); + netPacketProcessor.SubscribeReusable(OnCommonTrainFusesPacket); + netPacketProcessor.SubscribeReusable(OnServerboundJobValidateRequestPacket); + netPacketProcessor.SubscribeReusable(OnCommonChatPacket); + netPacketProcessor.SubscribeReusable(OnUnconnectedPingPacket); + netPacketProcessor.SubscribeNetSerializable(OnCommonItemChangePacket); } private void OnLoaded() { + //Debug.Log($"Server loaded, isSinglePlayer: {isSinglePlayer} isPublic: {isPublic}"); + if (!isSinglePlayer) + { + lobbyServerManager = NetworkLifecycle.Instance.GetOrAddComponent(); + } + Log($"Server loaded, processing {joinQueue.Count} queued players"); IsLoaded = true; + while (joinQueue.Count > 0) { - NetPeer peer = joinQueue.Dequeue(); - if (peer.ConnectionState == ConnectionState.Connected) + ITransportPeer peer = joinQueue.Dequeue(); + + // Assuming the `peer.ConnectionState` property exists and is being checked + if (peer.ConnectionState.Equals(TransportConnectionState.Connected)) + { + System.Console.WriteLine("Connection is established."); OnServerboundClientReadyPacket(null, peer); + } + else + { + System.Console.WriteLine("Connection is not established."); + } } } - public bool TryGetServerPlayer(NetPeer peer, out ServerPlayer player) + public bool TryGetServerPlayer(ITransportPeer peer, out ServerPlayer player) { return serverPlayers.TryGetValue((byte)peer.Id, out player); } + public bool TryGetServerPlayer(byte id, out ServerPlayer player) + { + return serverPlayers.TryGetValue(id, out player); + } - public bool TryGetNetPeer(byte id, out NetPeer peer) + public bool TryGetPeer(byte id, out ITransportPeer peer) { - return netPeers.TryGetValue(id, out peer); + return Peers.TryGetValue(id, out peer); } #region Net Events - public override void OnPeerConnected(NetPeer peer) - { } + public override void OnPeerConnected(ITransportPeer peer) + { + } - public override void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) + public override void OnPeerDisconnected(ITransportPeer peer, DisconnectReason disconnectReason) { byte id = (byte)peer.Id; - Log($"Player {(serverPlayers.TryGetValue(id, out ServerPlayer player) ? player : id)} disconnected: {disconnectInfo.Reason}"); + Log($"Player {(serverPlayers.TryGetValue(id, out ServerPlayer player) ? player : id)} disconnected: {disconnectReason}"); if (WorldStreamingInit.isLoaded) SaveGameManager.Instance.UpdateInternalData(); serverPlayers.Remove(id); - netPeers.Remove(id); - netManager.SendToAll(WritePacket(new ClientboundPlayerDisconnectPacket { + Peers.Remove(id); + + SendPacketToAll(new ClientboundPlayerDisconnectPacket + { Id = id - }), DeliveryMethod.ReliableUnordered); + }, DeliveryMethod.ReliableUnordered); + + PlayerDisconnect?.Invoke(id); } - public override void OnNetworkLatencyUpdate(NetPeer peer, int latency) + public override void OnNetworkLatencyUpdate(ITransportPeer peer, int latency) { - ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() { + ClientboundPingUpdatePacket clientboundPingUpdatePacket = new() + { Id = (byte)peer.Id, Ping = latency }; SendPacketToAll(clientboundPingUpdatePacket, DeliveryMethod.ReliableUnordered, peer); - SendPacket(peer, new ClientboundTickSyncPacket { + SendPacket(peer, new ClientboundTickSyncPacket + { ServerTick = NetworkLifecycle.Instance.Tick }, DeliveryMethod.ReliableUnordered); } - public override void OnConnectionRequest(ConnectionRequest request) + public override void OnConnectionRequest(NetDataReader requestData, IConnectionRequest request) { - netPacketProcessor.ReadAllPackets(request.Data, request); + LogDebug(() => $"NetworkServer OnConnectionRequest"); + netPacketProcessor.ReadAllPackets(requestData, request); } #endregion @@ -153,14 +252,31 @@ public override void OnConnectionRequest(ConnectionRequest request) private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in netPeers) - kvp.Value.Send(writer, deliveryMethod); + foreach (KeyValuePair kvp in Peers) + kvp.Value?.Send(writer, deliveryMethod); } - private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, NetPeer excludePeer) where T : class, new() + private void SendPacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : class, new() { NetDataWriter writer = WritePacket(packet); - foreach (KeyValuePair kvp in netPeers) + foreach (KeyValuePair kvp in Peers) + { + if (kvp.Key == excludePeer.Id) + continue; + kvp.Value.Send(writer, deliveryMethod); + } + } + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod) where T : INetSerializable, new() + { + NetDataWriter writer = WriteNetSerializablePacket(packet); + foreach (KeyValuePair kvp in Peers) + kvp.Value.Send(writer, deliveryMethod); + } + + private void SendNetSerializablePacketToAll(T packet, DeliveryMethod deliveryMethod, ITransportPeer excludePeer) where T : INetSerializable, new() + { + NetDataWriter writer = WriteNetSerializablePacket(packet); + foreach (KeyValuePair kvp in Peers) { if (kvp.Key == excludePeer.Id) continue; @@ -168,111 +284,266 @@ public override void OnConnectionRequest(ConnectionRequest request) } } + public void KickPlayer(ITransportPeer peer) + { + //peer.Send(WritePacket(new ClientboundDisconnectPacket()),DeliveryMethod.ReliableUnordered); + peer.Disconnect(WritePacket(new ClientboundDisconnectPacket { Kicked = true })); + } public void SendGameParams(GameParams gameParams) { - SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, selfPeer); + SendPacketToAll(ClientboundGameParamsPacket.FromGameParams(gameParams), DeliveryMethod.ReliableOrdered, SelfPeer); + } + + public void SendSpawnTrainset(List set, bool autoCouple, bool sendToAll, ITransportPeer sendTo = null) + { + + LogDebug(() => + { + StringBuilder sb = new(); + + sb.Append($"SendSpawnTrainSet() Sending trainset {set?.FirstOrDefault()?.GetNetId()} with {set?.Count} cars"); + + TrainCar[] noNetId = set?.Where(car => car.GetNetId() == 0).ToArray(); + + if (noNetId.Length > 0) + sb.AppendLine($"Erroneous cars!: {string.Join(", ", noNetId.Select(car => $"{{{car?.ID}, {car?.CarGUID}, {car.logicCar != null}}}"))}"); + + return sb.ToString(); + + }); + + var packet = ClientboundSpawnTrainSetPacket.FromTrainSet(set, autoCouple); + + if (!sendToAll) + { + if (sendTo == null) + LogError($"SendSpawnTrainSet() Trying to send to null peer!"); + else + SendPacket(sendTo, packet, DeliveryMethod.ReliableOrdered); + } + else + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendSpawnTrainCar(NetworkedTrainCar networkedTrainCar) { - SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, selfPeer); + SendPacketToAll(ClientboundSpawnTrainCarPacket.FromTrainCar(networkedTrainCar), DeliveryMethod.ReliableOrdered, SelfPeer); } - public void SendDestroyTrainCar(TrainCar trainCar) + public void SendDestroyTrainCar(ushort netId, ITransportPeer peer = null) { - SendPacketToAll(new ClientboundDestroyTrainCarPacket { - NetId = trainCar.GetNetId() - }, DeliveryMethod.ReliableOrdered, selfPeer); + //ushort netID = trainCar.GetNetId(); + LogDebug(() => $"SendDestroyTrainCar({netId})"); + + if (netId == 0) + { + Multiplayer.LogWarning($"SendDestroyTrainCar failed. netId {netId}"); + return; + } + + var packet = new ClientboundDestroyTrainCarPacket{ NetId = netId }; + + if (peer == null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + else + SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); } public void SendTrainsetPhysicsUpdate(ClientboundTrainsetPhysicsPacket packet, bool reliable) { - SendPacketToAll(packet, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable, selfPeer); + SendPacketToAll(packet, reliable ? DeliveryMethod.ReliableOrdered : DeliveryMethod.Unreliable, SelfPeer); + } + + public void SendBrakeState(ushort netId, float mainReservoirPressure, float brakePipePressure, float brakeCylinderPressure, float overheatPercent, float overheatReductionFactor, float temperature) + { + SendPacketToAll(new ClientboundBrakeStateUpdatePacket + { + NetId = netId, + MainReservoirPressure = mainReservoirPressure, + BrakePipePressure = brakePipePressure, + BrakeCylinderPressure = brakeCylinderPressure, + OverheatPercent = overheatPercent, + OverheatReductionFactor = overheatReductionFactor, + Temperature = temperature + }, DeliveryMethod.ReliableOrdered, SelfPeer); + + //Multiplayer.LogDebug(()=> $"Sending Brake Pressures netId {netId}: {mainReservoirPressure}, {independentPipePressure}, {brakePipePressure}, {brakeCylinderPressure}"); + } + + public void SendFireboxState(ushort netId, float fireboxContents, bool fireboxOn) + { + SendPacketToAll(new ClientboundFireboxStatePacket + { + NetId = netId, + Contents = fireboxContents, + IsOn = fireboxOn + }, DeliveryMethod.ReliableOrdered, SelfPeer); + + Multiplayer.LogDebug(() => $"Sending Firebox States netId {netId}: {fireboxContents}, {fireboxOn}"); } public void SendCargoState(TrainCar trainCar, ushort netId, bool isLoading, byte cargoModelIndex) { Car logicCar = trainCar.logicCar; CargoType cargoType = isLoading ? logicCar.CurrentCargoTypeInCar : logicCar.LastUnloadedCargoType; - SendPacketToAll(new ClientboundCargoStatePacket { + SendPacketToAll(new ClientboundCargoStatePacket + { NetId = netId, IsLoading = isLoading, CargoType = (ushort)cargoType, CargoAmount = logicCar.LoadedCargoAmount, CargoModelIndex = cargoModelIndex, WarehouseMachineId = logicCar.CargoOriginWarehouse?.ID - }, DeliveryMethod.ReliableOrdered, selfPeer); + }, DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendCarHealthUpdate(ushort netId, float health) { - SendPacketToAll(new ClientboundCarHealthUpdatePacket { + SendPacketToAll(new ClientboundCarHealthUpdatePacket + { NetId = netId, Health = health - }, DeliveryMethod.ReliableOrdered, selfPeer); + }, DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendRerailTrainCar(ushort netId, ushort rerailTrack, Vector3 worldPos, Vector3 forward) { - SendPacketToAll(new ClientboundRerailTrainPacket { + SendPacketToAll(new ClientboundRerailTrainPacket + { NetId = netId, TrackId = rerailTrack, Position = worldPos, Forward = forward - }, DeliveryMethod.ReliableOrdered, selfPeer); + }, DeliveryMethod.ReliableOrdered, SelfPeer); } public void SendWindowsBroken(ushort netId, Vector3 forceDirection) { - SendPacketToAll(new ClientboundWindowsBrokenPacket { + SendPacketToAll(new ClientboundWindowsBrokenPacket + { NetId = netId, ForceDirection = forceDirection - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendWindowsRepaired(ushort netId) { - SendPacketToAll(new ClientboundWindowsBrokenPacket { + SendPacketToAll(new ClientboundWindowsRepairedPacket + { NetId = netId - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendMoney(float amount) { - SendPacketToAll(new ClientboundMoneyPacket { + SendPacketToAll(new ClientboundMoneyPacket + { Amount = amount - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendLicense(string id, bool isJobLicense) { - SendPacketToAll(new ClientboundLicenseAcquiredPacket { + SendPacketToAll(new ClientboundLicenseAcquiredPacket + { Id = id, IsJobLicense = isJobLicense - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendGarage(string id) { - SendPacketToAll(new ClientboundGarageUnlockPacket { + SendPacketToAll(new ClientboundGarageUnlockPacket + { Id = id - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); } public void SendDebtStatus(bool hasDebt) { - SendPacketToAll(new ClientboundDebtStatusPacket { + SendPacketToAll(new ClientboundDebtStatusPacket + { HasDebt = hasDebt - }, DeliveryMethod.ReliableUnordered, selfPeer); + }, DeliveryMethod.ReliableUnordered, SelfPeer); + } + + public void SendJobsCreatePacket(NetworkedStationController networkedStation, NetworkedJob[] jobs, ITransportPeer peer = null) + { + Multiplayer.Log($"Sending JobsCreatePacket for stationNetId {networkedStation.NetId} with {jobs.Count()} jobs"); + + var packet = ClientboundJobsCreatePacket.FromNetworkedJobs(networkedStation, jobs); + + if (peer ==null) + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, SelfPeer); + else + SendPacket(peer, packet, DeliveryMethod.ReliableOrdered); + } + + public void SendJobsUpdatePacket(ushort stationNetId, NetworkedJob[] jobs) + { + Multiplayer.Log($"Sending JobsUpdatePacket for stationNetId {stationNetId} with {jobs.Count()} jobs"); + SendPacketToAll(ClientboundJobsUpdatePacket.FromNetworkedJobs(stationNetId, jobs), DeliveryMethod.ReliableOrdered, SelfPeer); + } + + public void SendItemsChangePacket(List items, ServerPlayer player) + { + Multiplayer.Log($"Sending SendItemsChangePacket with {items.Count()} items to {player.Username}"); + + if (Peers.TryGetValue(player.Id, out ITransportPeer peer) && peer != SelfPeer) + { + SendNetSerializablePacket(peer, new CommonItemChangePacket { Items = items }, + DeliveryMethod.ReliableOrdered); + } + } + + public void SendChat(string message, ITransportPeer exclude = null) + { + + if (exclude != null) + { + NetworkLifecycle.Instance.Server.SendPacketToAll(new CommonChatPacket + { + message = message + }, DeliveryMethod.ReliableUnordered, exclude); + } + else + { + NetworkLifecycle.Instance.Server.SendPacketToAll(new CommonChatPacket + { + message = message + }, DeliveryMethod.ReliableUnordered); + } + } + + public void SendWhisper(string message, ITransportPeer recipient) + { + if (message != null || recipient != null) + { + NetworkLifecycle.Instance.Server.SendPacket(recipient, new CommonChatPacket + { + message = message + }, DeliveryMethod.ReliableUnordered); + } + } #endregion #region Listeners - private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, ConnectionRequest request) + private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, IConnectionRequest request) { - packet.Username = packet.Username.Truncate(Settings.MAX_USERNAME_LENGTH); + // clean up username - remove leading/trailing white space, swap spaces for underscores and truncate + packet.Username = packet.Username.Trim().Replace(' ', '_').Truncate(Settings.MAX_USERNAME_LENGTH); + string overrideUsername = packet.Username; + + //ensure the username is unique + int uniqueName = ServerPlayers.Where(player => player.OriginalUsername.ToLower() == packet.Username.ToLower()).Count(); + + if (uniqueName > 0) + { + overrideUsername += uniqueName; + } Guid guid; try @@ -287,12 +558,13 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - Log($"Processing login packet for {packet.Username} ({guid.ToString()}){(Multiplayer.Settings.LogIps ? $" at {request.RemoteEndPoint.Address}" : "")}"); + Log($"Processing login packet for {packet.Username} ({guid}){(Multiplayer.Settings.LogIps ? $" at {request.RemoteEndPoint.Address}" : "")}"); if (Multiplayer.Settings.Password != packet.Password) { LogWarning("Denied login due to invalid password!"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundLoginResponsePacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__INVALID_PASSWORD_KEY }; request.Reject(WritePacket(denyPacket)); @@ -302,31 +574,35 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, if (packet.BuildMajorVersion != BuildInfo.BUILD_VERSION_MAJOR) { LogWarning($"Denied login to incorrect game version! Got: {packet.BuildMajorVersion}, expected: {BuildInfo.BUILD_VERSION_MAJOR}"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundLoginResponsePacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__GAME_VERSION_KEY, - ReasonArgs = new[] { BuildInfo.BUILD_VERSION_MAJOR.ToString(), packet.BuildMajorVersion.ToString() } + ReasonArgs = [BuildInfo.BUILD_VERSION_MAJOR.ToString(), packet.BuildMajorVersion.ToString()] }; request.Reject(WritePacket(denyPacket)); return; } - if (netManager.ConnectedPeersCount >= Multiplayer.Settings.MaxPlayers) + if (PlayerCount >= Multiplayer.Settings.MaxPlayers || isSinglePlayer && PlayerCount >= 1) { LogWarning("Denied login due to server being full!"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundLoginResponsePacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__FULL_SERVER_KEY }; request.Reject(WritePacket(denyPacket)); return; } - ModInfo[] clientMods = packet.Mods; + ModInfo[] clientMods = packet.Mods.Where(mod => !modWhiteList.Contains(mod.Id)).ToArray(); if (!serverMods.SequenceEqual(clientMods)) { ModInfo[] missing = serverMods.Except(clientMods).ToArray(); ModInfo[] extra = clientMods.Except(serverMods).ToArray(); + LogWarning($"Denied login due to mod mismatch! {missing.Length} missing, {extra.Length} extra"); - ClientboundServerDenyPacket denyPacket = new() { + ClientboundLoginResponsePacket denyPacket = new() + { ReasonKey = Locale.DISCONN_REASON__MODS_KEY, Missing = missing, Extra = extra @@ -335,20 +611,29 @@ private void OnServerboundClientLoginPacket(ServerboundClientLoginPacket packet, return; } - NetPeer peer = request.Accept(); + ITransportPeer peer = request.Accept(); - ServerPlayer serverPlayer = new() { + ServerPlayer serverPlayer = new() + { Id = (byte)peer.Id, - Username = packet.Username, + Username = overrideUsername, + OriginalUsername = packet.Username, Guid = guid }; serverPlayers.Add(serverPlayer.Id, serverPlayer); + + ClientboundLoginResponsePacket acceptPacket = new() + { + Accepted = true, + }; + + SendPacket(peer, acceptPacket, DeliveryMethod.ReliableUnordered); } - private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataRequestPacket packet, NetPeer peer) + private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataRequestPacket packet, ITransportPeer peer) { - if (netPeers.ContainsKey((byte)peer.Id)) + if (Peers.ContainsKey((byte)peer.Id)) { LogWarning("Denied save game data request from already connected peer!"); return; @@ -360,8 +645,9 @@ private void OnServerboundSaveGameDataRequestPacket(ServerboundSaveGameDataReque SendPacket(peer, ClientboundSaveGameDataPacket.CreatePacket(player), DeliveryMethod.ReliableOrdered); } - private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, NetPeer peer) + private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, ITransportPeer peer) { + byte peerId = (byte)peer.Id; // Allow clients to connect before the server is fully loaded @@ -377,17 +663,20 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, AppUtil.Instance.RequestSystemOnValueChanged(0.0f); // Allow the player to receive packets - netPeers.Add(peerId, peer); + Peers.Add(peerId, peer); // Send the new player to all other players ServerPlayer serverPlayer = serverPlayers[peerId]; - ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() { + ClientboundPlayerJoinedPacket clientboundPlayerJoinedPacket = new() + { Id = peerId, Username = serverPlayer.Username, - Guid = serverPlayer.Guid.ToByteArray() + //Guid = serverPlayer.Guid.ToByteArray() }; SendPacketToAll(clientboundPlayerJoinedPacket, DeliveryMethod.ReliableOrdered, peer); + ChatManager.ServerMessage(serverPlayer.Username + " joined the game", null, peer); + Log($"Client {peer.Id} is ready. Sending world state"); // No need to sync the world state if the player is the host @@ -403,16 +692,47 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, SendPacket(peer, WeatherDriver.Instance.GetSaveData().ToObject(), DeliveryMethod.ReliableOrdered); // Send junctions and turntables - SendPacket(peer, new ClientboundRailwayStatePacket { - SelectedJunctionBranches = NetworkedJunction.IndexedJunctions.Select(j => (byte)j.Junction.selectedBranch).ToArray(), + SendPacket(peer, new ClientboundRailwayStatePacket + { + SelectedJunctionBranches = NetworkedJunction.IndexedJunctions.Select(j => j.Junction.selectedBranch).ToArray(), TurntableRotations = NetworkedTurntable.IndexedTurntables.Select(j => j.TurntableRailTrack.currentYRotation).ToArray() }, DeliveryMethod.ReliableOrdered); // Send trains foreach (Trainset set in Trainset.allSets) { - LogDebug(() => $"Sending trainset {set.firstCar.GetNetId()} with {set.cars.Count} cars"); - SendPacket(peer, ClientboundSpawnTrainSetPacket.FromTrainSet(set), DeliveryMethod.ReliableOrdered); + try + { + SendSpawnTrainset(set.cars, false, false, peer); + } + catch (Exception e) + { + LogWarning($"Exception when trying to send train set spawn data for [{set?.firstCar?.ID}, {set?.firstCar?.GetNetId()}]\r\n{e.Message}\r\n{e.StackTrace}"); + } + } + + // Sync Stations (match NetIDs with StationIDs) - we could do this the same as junctions but juntions may need to be upgraded to work this way - future planning for mod integration + SendPacket(peer, new ClientBoundStationControllerLookupPacket(NetworkedStationController.GetAll().ToArray()), DeliveryMethod.ReliableOrdered); + + //send jobs + foreach (StationController station in StationController.allStations) + { + if (NetworkedStationController.GetFromStationController(station, out NetworkedStationController netStation)) + { + //only send active jobs (available or in progress) - new clients don't need to know about old jobs + NetworkedJob[] jobs = netStation.NetworkedJobs + .Where(j => j.Job.State == JobState.Available || j.Job.State == JobState.InProgress) + .ToArray(); + + for (int i = 0; i < jobs.Length; i++) + { + SendJobsCreatePacket(netStation, [jobs[i]]); + } + } + else + { + LogError($"Sending job packets... Failed to get NetworkedStation from station"); + } } // Send existing players @@ -420,132 +740,212 @@ private void OnServerboundClientReadyPacket(ServerboundClientReadyPacket packet, { if (player.Id == peer.Id) continue; - SendPacket(peer, new ClientboundPlayerJoinedPacket { + SendPacket(peer, new ClientboundPlayerJoinedPacket + { Id = player.Id, Username = player.Username, - Guid = player.Guid.ToByteArray(), - TrainCar = player.CarId, + //Guid = player.Guid.ToByteArray(), + CarID = player.CarId, Position = player.RawPosition, Rotation = player.RawRotationY }, DeliveryMethod.ReliableOrdered); } // All data has been sent, allow the client to load into the world. + Log($"Sending Remove Loading Screen to {serverPlayer.Username}"); SendPacket(peer, new ClientboundRemoveLoadingScreenPacket(), DeliveryMethod.ReliableOrdered); serverPlayer.IsLoaded = true; } - private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket packet, NetPeer peer) + private void OnServerboundPlayerPositionPacket(ServerboundPlayerPositionPacket packet, ITransportPeer peer) { if (TryGetServerPlayer(peer, out ServerPlayer player)) { + player.CarId = packet.CarID; player.RawPosition = packet.Position; player.RawRotationY = packet.RotationY; + } - ClientboundPlayerPositionPacket clientboundPacket = new() { + ClientboundPlayerPositionPacket clientboundPacket = new() + { Id = (byte)peer.Id, Position = packet.Position, MoveDir = packet.MoveDir, RotationY = packet.RotationY, - IsJumpingIsOnCar = packet.IsJumpingIsOnCar + IsJumpingIsOnCar = packet.IsJumpingIsOnCar, + CarID = packet.CarID }; SendPacketToAll(clientboundPacket, DeliveryMethod.Sequenced, peer); } - private void OnServerboundPlayerCarPacket(ServerboundPlayerCarPacket packet, NetPeer peer) - { - if (packet.CarId != 0 && !NetworkedTrainCar.Get(packet.CarId, out NetworkedTrainCar _)) - return; - - if (TryGetServerPlayer(peer, out ServerPlayer player)) - player.CarId = packet.CarId; - - ClientboundPlayerCarPacket clientboundPacket = new() { - Id = (byte)peer.Id, - CarId = packet.CarId - }; - - SendPacketToAll(clientboundPacket, DeliveryMethod.ReliableOrdered, peer); - } - - private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, NetPeer peer) + private void OnServerboundTimeAdvancePacket(ServerboundTimeAdvancePacket packet, ITransportPeer peer) { - SendPacketToAll(new ClientboundTimeAdvancePacket { + SendPacketToAll(new ClientboundTimeAdvancePacket + { amountOfTimeToSkipInSeconds = packet.amountOfTimeToSkipInSeconds }, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonChangeJunctionPacket(CommonChangeJunctionPacket packet, NetPeer peer) + private void OnCommonChangeJunctionPacket(CommonChangeJunctionPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, NetPeer peer) + private void OnCommonRotateTurntablePacket(CommonRotateTurntablePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, NetPeer peer) + private void OnCommonCouplerInteractionPacket(CommonCouplerInteractionPacket packet, ITransportPeer peer) + { + //todo: add validation that to ensure the client is near the coupler - this packet may also be used for remote operations and may need to factor that in in the future + if(NetworkedTrainCar.Get(packet.NetId, out var netTrainCar)) + { + if(netTrainCar.Server_ValidateCouplerInteraction(packet, peer)) + { + //passed validation, send to all but the originator + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + } + else + { + Multiplayer.LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {peer.Id}) Sending validation failure"); + //failed validation notify client + SendPacket( + peer, + new CommonCouplerInteractionPacket + { + NetId = packet.NetId, + Flags = (ushort)CouplerInteractionType.NoAction, + IsFrontCoupler = packet.IsFrontCoupler, + } + ,DeliveryMethod.ReliableOrdered + ); + } + } + else + { + Multiplayer.LogDebug(() => $"OnCommonCouplerInteractionPacket([{packet.Flags}, {netTrainCar.CurrentID}, {packet.NetId}], {peer.Id}) Sending destroy"); + //Car doesn't exist, tell client to delete it + SendDestroyTrainCar(packet.NetId, peer); + } + + } + private void OnCommonTrainCouplePacket(CommonTrainCouplePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet, NetPeer peer) + private void OnCommonTrainUncouplePacket(CommonTrainUncouplePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet, NetPeer peer) + private void OnCommonHoseConnectedPacket(CommonHoseConnectedPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet, NetPeer peer) + private void OnCommonHoseDisconnectedPacket(CommonHoseDisconnectedPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet, NetPeer peer) + private void OnCommonMuConnectedPacket(CommonMuConnectedPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet, NetPeer peer) + private void OnCommonMuDisconnectedPacket(CommonMuDisconnectedPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet, NetPeer peer) + private void OnCommonCockFiddlePacket(CommonCockFiddlePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet, NetPeer peer) + private void OnCommonBrakeCylinderReleasePacket(CommonBrakeCylinderReleasePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableUnordered, peer); } - private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packet, NetPeer peer) + private void OnCommonHandbrakePositionPacket(CommonHandbrakePositionPacket packet, ITransportPeer peer) + { + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); + } + + private void OnCommonPaintThemePacket(CommonPaintThemePacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, NetPeer peer) + private void OnServerboundAddCoalPacket(ServerboundAddCoalPacket packet, ITransportPeer peer) + { + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) + return; + + //is value valid? + if (float.IsNaN(packet.CoalMassDelta)) + return; + + if (!NetworkLifecycle.Instance.IsHost(peer)) + { + float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; + + //is player close enough to add coal? + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) + networkedTrainCar.firebox?.fireboxCoalControlPort.ExternalValueUpdate(packet.CoalMassDelta); + } + + } + + private void OnServerboundFireboxIgnitePacket(ServerboundFireboxIgnitePacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; - if (!NetworkLifecycle.Instance.IsHost(peer) && !networkedTrainCar.Server_ValidateClientSimFlowPacket(player, packet)) + + if (!NetworkLifecycle.Instance.IsHost(peer)) + { + //is player close enough to ignite firebox? + float carLength = CarSpawner.Instance.carLiveryToCarLength[networkedTrainCar.TrainCar.carLivery]; + if ((player.WorldPosition - networkedTrainCar.transform.position).sqrMagnitude <= carLength * carLength) + networkedTrainCar.firebox?.Ignite(); + } + } + + private void OnCommonTrainPortsPacket(CommonTrainPortsPacket packet, ITransportPeer peer) + { + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + return; + if (!NetworkedTrainCar.Get(packet.NetId, out NetworkedTrainCar networkedTrainCar)) return; + //if not the host && validation fails then ignore packet + if (!NetworkLifecycle.Instance.IsHost(peer)) + { + bool flag = networkedTrainCar.Server_ValidateClientSimFlowPacket(player, packet); + + //LogDebug(() => $"OnCommonTrainPortsPacket from {player.Username}, Not host, valid: {flag}"); + if (!flag) + { + return; + } + } + SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } - private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet, NetPeer peer) + private void OnCommonTrainFusesPacket(CommonTrainFusesPacket packet, ITransportPeer peer) { SendPacketToAll(packet, DeliveryMethod.ReliableOrdered, peer); } @@ -556,7 +956,7 @@ private void OnServerboundTrainSyncRequestPacket(ServerboundTrainSyncRequestPack networkedTrainCar.Server_DirtyAllState(); } - private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequestPacket packet, NetPeer peer) + private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequestPacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; @@ -591,7 +991,7 @@ private void OnServerboundTrainDeleteRequestPacket(ServerboundTrainDeleteRequest CarSpawner.Instance.DeleteCar(trainCar); } - private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequestPacket packet, NetPeer peer) + private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequestPacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; @@ -602,7 +1002,11 @@ private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequest TrainCar trainCar = networkedTrainCar.TrainCar; Vector3 position = packet.Position + WorldMover.currentMove; - float cost = RerailController.CalculatePrice((networkedTrainCar.transform.position - position).magnitude, trainCar.carType, Globals.G.GameParams.RerailMaxPrice); + + //Check if player is a Newbie (currently shared with all players) + float cost = TutorialHelper.InRestrictedMode || rerailController != null && rerailController.isPlayerNewbie ? 0f : + RerailController.CalculatePrice((networkedTrainCar.transform.position - position).magnitude, trainCar.carType, Globals.G.GameParams.RerailMaxPrice); + if (!Inventory.Instance.RemoveMoney(cost)) { LogWarning($"{player.Username} tried to rerail a train without enough money to do so!"); @@ -612,7 +1016,7 @@ private void OnServerboundTrainRerailRequestPacket(ServerboundTrainRerailRequest trainCar.Rerail(networkedRailTrack.RailTrack, position, packet.Forward); } - private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchaseRequestPacket packet, NetPeer peer) + private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchaseRequestPacket packet, ITransportPeer peer) { if (!TryGetServerPlayer(peer, out ServerPlayer player)) return; @@ -648,5 +1052,97 @@ private void OnServerboundLicensePurchaseRequestPacket(ServerboundLicensePurchas LicenseManager.Instance.AcquireGeneralLicense(generalLicense); } + + private void OnServerboundJobValidateRequestPacket(ServerboundJobValidateRequestPacket packet, ITransportPeer peer) + { + Log($"OnServerboundJobValidateRequestPacket(): {packet.JobNetId}"); + + if (!NetworkedJob.Get(packet.JobNetId, out NetworkedJob networkedJob)) + { + LogWarning($"OnServerboundJobValidateRequestPacket() NetworkedJob not found: {packet.JobNetId}"); + + SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = true }, DeliveryMethod.ReliableOrdered); + return; + } + + if (!TryGetServerPlayer(peer, out ServerPlayer player)) + { + LogWarning($"OnServerboundJobValidateRequestPacket() ServerPlayer not found: {peer.Id}"); + return; + } + + //Find the station and validator + if (!NetworkedStationController.Get(packet.StationNetId, out NetworkedStationController networkedStationController) || networkedStationController.JobValidator == null) + { + LogWarning($"OnServerboundJobValidateRequestPacket() JobValidator not found. StationNetId: {packet.StationNetId}, StationController found: {networkedStationController != null}, JobValidator found: {networkedStationController?.JobValidator != null}"); + return; + } + + LogDebug(() => $"OnServerboundJobValidateRequestPacket() Validating {packet.JobNetId}, Validation Type: {packet.validationType} overview: {networkedJob.JobOverview != null}, booklet: {networkedJob.JobBooklet != null}"); + switch (packet.validationType) + { + case ValidationType.JobOverview: + networkedStationController.JobValidator.ProcessJobOverview(networkedJob.JobOverview.GetTrackedItem()); + break; + + case ValidationType.JobBooklet: + networkedStationController.JobValidator.ValidateJob(networkedJob.JobBooklet.GetTrackedItem()); + break; + } + + //SendPacket(peer, new ClientboundJobValidateResponsePacket { JobNetId = packet.JobNetId, Invalid = false }, DeliveryMethod.ReliableUnordered); + } + + private void OnCommonChatPacket(CommonChatPacket packet, ITransportPeer peer) + { + ChatManager.ProcessMessage(packet.message, peer); + } + #endregion + + #region Unconnected Packet Handling + private void OnUnconnectedPingPacket(UnconnectedPingPacket packet, IPEndPoint endPoint) + { + //Multiplayer.Log($"OnUnconnectedPingPacket({endPoint.Address})"); + //SendUnconnectedPacket(packet, endPoint.Address.ToString(), endPoint.Port); + } + + private void OnCommonItemChangePacket(CommonItemChangePacket packet, ITransportPeer peer) + { + //if(!TryGetServerPlayer(peer, out var player)) + // return; + + //LogDebug(()=>$"OnCommonItemChangePacket({packet?.Items?.Count}, {peer.Id} (\"{player.Username}\"))"); + + //Multiplayer.LogDebug(() => + //{ + // string debug = ""; + + // foreach (var item in packet?.Items) + // { + // debug += "UpdateType: " + item?.UpdateType + "\r\n"; + // debug += "itemNetId: " + item?.ItemNetId + "\r\n"; + // debug += "PrefabName: " + item?.PrefabName + "\r\n"; + // debug += "Equipped: " + item?.ItemState + "\r\n"; + // debug += "Position: " + item?.ItemPosition + "\r\n"; + // debug += "Rotation: " + item?.ItemRotation + "\r\n"; + // debug += "ThrowDirection: " + item?.ThrowDirection + "\r\n"; + // debug += "Player: " + item?.Player + "\r\n"; + // debug += "CarNetId: " + item?.CarNetId + "\r\n"; + // debug += "AttachedFront: " + item?.AttachedFront + "\r\n"; + + // debug += "States:"; + + // if (item.States != null) + // foreach (var state in item?.States) + // debug += "\r\n\t" + state.Key + ": " + state.Value; + // } + + // return debug; + //} + + //); + + //NetworkedItemManager.Instance.ReceiveSnapshots(packet.Items, player); + } #endregion } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundDisconnectPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundDisconnectPacket.cs new file mode 100644 index 0000000..b39519d --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundDisconnectPacket.cs @@ -0,0 +1,6 @@ +namespace Multiplayer.Networking.Packets.Clientbound; + +public class ClientboundDisconnectPacket +{ + public bool Kicked { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundServerDenyPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs similarity index 80% rename from Multiplayer/Networking/Packets/Clientbound/ClientboundServerDenyPacket.cs rename to Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs index 4f77ed3..4e6553f 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundServerDenyPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundLoginResponsePacket.cs @@ -3,8 +3,9 @@ namespace Multiplayer.Networking.Packets.Clientbound; -public class ClientboundServerDenyPacket +public class ClientboundLoginResponsePacket { + public bool Accepted { get; set; } public string ReasonKey { get; set; } public string[] ReasonArgs { get; set; } public ModInfo[] Missing { get; set; } = Array.Empty(); diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerCarPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerCarPacket.cs deleted file mode 100644 index 10c0c4c..0000000 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerCarPacket.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Multiplayer.Networking.Packets.Clientbound; - -public class ClientboundPlayerCarPacket -{ - public byte Id { get; set; } - public ushort CarId { get; set; } -} diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs index 409c3f5..befc8c3 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerJoinedPacket.cs @@ -6,8 +6,8 @@ public class ClientboundPlayerJoinedPacket { public byte Id { get; set; } public string Username { get; set; } - public byte[] Guid { get; set; } - public ushort TrainCar { get; set; } + //public byte[] Guid { get; set; } + public ushort CarID { get; set; } public Vector3 Position { get; set; } public float Rotation { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs index bf27e5a..6abe79f 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundPlayerPositionPacket.cs @@ -9,6 +9,7 @@ public class ClientboundPlayerPositionPacket public Vector2 MoveDir { get; set; } public float RotationY { get; set; } public byte IsJumpingIsOnCar { get; set; } + public ushort CarID { get; set; } public bool IsJumping => (IsJumpingIsOnCar & 1) != 0; public bool IsOnCar => (IsJumpingIsOnCar & 2) != 0; diff --git a/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs b/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs index 6ecf96d..551bb1d 100644 --- a/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/ClientboundSaveGameDataPacket.cs @@ -43,6 +43,17 @@ public static ClientboundSaveGameDataPacket CreatePacket(ServerPlayer player) JObject playerData = NetworkedSaveGameManager.Instance.Server_GetPlayerData(data, player.Guid); + Multiplayer.LogDebug(() => + { + string unlockedGen = string.Join(", ", UnlockablesManager.Instance.UnlockedGeneralLicenses); + string packetGen = string.Join(", ", data.GetStringArray(SaveGameKeys.Licenses_General)); + + string unlockedJob = string.Join(", ", UnlockablesManager.Instance.UnlockedJobLicenses); + string packetJob = string.Join(", ", data.GetStringArray(SaveGameKeys.Licenses_Jobs)); + + return $"ClientboundSaveGameDataPacket.CreatePacket() UnlockedGen: {{{unlockedGen}}}, PacketGen: {{{packetGen}}}, UnlockedJob: {{{unlockedJob}}}, PacketJob: {{{packetJob}}}"; + }); + return new ClientboundSaveGameDataPacket { GameMode = data.GetString(SaveGameKeys.Game_mode), SerializedDifficulty = difficulty.ToString(Formatting.None), diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs new file mode 100644 index 0000000..e489af2 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobValidateResponsePacket.cs @@ -0,0 +1,8 @@ + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobValidateResponsePacket +{ + public ushort JobNetId { get; set; } + public bool Invalid { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs new file mode 100644 index 0000000..bd51543 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsCreatePacket.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ClientboundJobsCreatePacket +{ + public ushort StationNetId { get; set; } + public JobData[] Jobs { get; set; } + + public static ClientboundJobsCreatePacket FromNetworkedJobs(NetworkedStationController netStation, NetworkedJob[] jobs) + { + List jobData = []; + foreach (var job in jobs) + { + JobData jd = JobData.FromJob(netStation, job); + Multiplayer.Log($"JobData: jobNetId: {jd.NetID}, jobId: {jd.ID}, itemNetId {jd.ItemNetID}"); + jobData.Add(jd); + } + + return new ClientboundJobsCreatePacket + { + StationNetId = netStation.NetId, + Jobs = jobData.ToArray() + }; + } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs new file mode 100644 index 0000000..b6f8bbe --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Jobs/ClientboundJobsUpdatePacket.cs @@ -0,0 +1,63 @@ +using Multiplayer.Networking.Data; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using System.Collections.Generic; + +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; +public class ClientboundJobsUpdatePacket +{ + public ushort StationNetId { get; set; } + public JobUpdateStruct[] JobUpdates { get; set; } + + + public static ClientboundJobsUpdatePacket FromNetworkedJobs(ushort stationNetID, NetworkedJob[] jobs) + { + Multiplayer.Log($"ClientboundJobsUpdatePacket.FromNetworkedJobs({stationNetID}, {jobs.Length})"); + + List jobData = new List(); + foreach (var job in jobs) + { + ushort validationStationNetId = 0; + ushort validationItemNetId = 0; + ItemPositionData itemPositionData = new ItemPositionData(); + + if (NetworkedStationController.GetFromJobValidator(job.JobValidator, out NetworkedStationController netValidationStation)) + validationStationNetId = netValidationStation.NetId; + + switch (job.Cause) + { + case NetworkedJob.DirtyCause.JobOverview: + validationItemNetId = job.JobOverview.NetId; + itemPositionData = ItemPositionData.FromItem(job.JobOverview); + break; + case NetworkedJob.DirtyCause.JobBooklet: + validationItemNetId = job.JobBooklet.NetId; + itemPositionData = ItemPositionData.FromItem(job.JobBooklet); + break; + case NetworkedJob.DirtyCause.JobReport: + validationItemNetId = job.JobReport.NetId; + itemPositionData = ItemPositionData.FromItem(job.JobReport); + break; + } + + JobUpdateStruct data = new JobUpdateStruct + { + JobNetID = job.NetId, + JobState = job.Job.State, + StartTime = job.Job.startTime, + FinishTime = job.Job.finishTime, + ValidationStationId = validationStationNetId, + ItemNetID = validationItemNetId, + ItemPositionData = itemPositionData + }; + + jobData.Add(data); + } + + return new ClientboundJobsUpdatePacket + { + StationNetId = stationNetID, + JobUpdates = jobData.ToArray() + }; + } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakeStateUpdatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakeStateUpdatePacket.cs new file mode 100644 index 0000000..d75f571 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundBrakeStateUpdatePacket.cs @@ -0,0 +1,13 @@ +namespace Multiplayer.Networking.Packets.Clientbound.Train; + +public class ClientboundBrakeStateUpdatePacket +{ + public ushort NetId { get; set; } + public float MainReservoirPressure { get; set; } + public float BrakePipePressure { get; set; } + public float BrakeCylinderPressure { get; set; } + + public float OverheatPercent { get; set; } + public float OverheatReductionFactor { get; set; } + public float Temperature { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs new file mode 100644 index 0000000..bbf0250 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundFireboxStatePacket.cs @@ -0,0 +1,8 @@ +namespace Multiplayer.Networking.Packets.Clientbound.Train; + +public class ClientboundFireboxStatePacket +{ + public ushort NetId { get; set; } + public float Contents { get; set; } + public bool IsOn { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs index 122de31..0d69e5f 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainCarPacket.cs @@ -1,5 +1,5 @@ using Multiplayer.Components.Networking.Train; -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Networking.Packets.Clientbound.Train; diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs index e81d535..6d9d396 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundSpawnTrainSetPacket.cs @@ -1,15 +1,27 @@ -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; +using System.Collections.Generic; namespace Multiplayer.Networking.Packets.Clientbound.Train; public class ClientboundSpawnTrainSetPacket { public TrainsetSpawnPart[] SpawnParts { get; set; } + public bool AutoCouple { get; set; } - public static ClientboundSpawnTrainSetPacket FromTrainSet(Trainset trainset) + //public static ClientboundSpawnTrainSetPacket FromTrainSet(Trainset trainset, bool autoCouple) + //{ + // return new ClientboundSpawnTrainSetPacket { + // SpawnParts = TrainsetSpawnPart.FromTrainSet(trainset), + // AutoCouple = autoCouple + // }; + //} + + public static ClientboundSpawnTrainSetPacket FromTrainSet(List trainset, bool autoCouple) { return new ClientboundSpawnTrainSetPacket { - SpawnParts = TrainsetSpawnPart.FromTrainSet(trainset) + SpawnParts = TrainsetSpawnPart.FromTrainSet(trainset), + AutoCouple = autoCouple + }; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs index aee1b0f..05903fa 100644 --- a/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs +++ b/Multiplayer/Networking/Packets/Clientbound/Train/ClientboundTrainsetPhysicsPacket.cs @@ -1,10 +1,11 @@ -using Multiplayer.Networking.Data; +using Multiplayer.Networking.Data.Train; namespace Multiplayer.Networking.Packets.Clientbound.Train; public class ClientboundTrainsetPhysicsPacket { - public int NetId { get; set; } + public int FirstNetId { get; set; } + public int LastNetId { get; set; } public uint Tick { get; set; } public TrainsetMovementPart[] TrainsetParts { get; set; } } diff --git a/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs new file mode 100644 index 0000000..e159634 --- /dev/null +++ b/Multiplayer/Networking/Packets/Clientbound/World/ClientboundStationControllerLookupPacket.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; + +namespace Multiplayer.Networking.Packets.Clientbound.World; + +public class ClientBoundStationControllerLookupPacket +{ + public ushort[] NetID { get; set; } + public string[] StationID { get; set; } + + public ClientBoundStationControllerLookupPacket() { } + + public ClientBoundStationControllerLookupPacket(ushort[] netID, string[] stationID) + { + if (netID == null) throw new ArgumentNullException(nameof(netID)); + if (stationID == null) throw new ArgumentNullException(nameof(stationID)); + if (netID.Length != stationID.Length) throw new ArgumentException("Arrays must have the same length"); + + NetID = netID; + StationID = stationID; + } + + public ClientBoundStationControllerLookupPacket(KeyValuePair[] NetIDtoStationID) + { + if (NetIDtoStationID == null) + throw new ArgumentNullException(nameof(NetIDtoStationID)); + + NetID = new ushort[NetIDtoStationID.Length]; + StationID = new string[NetIDtoStationID.Length]; + + for (int i = 0; i < NetIDtoStationID.Length; i++) + { + NetID[i] = NetIDtoStationID[i].Key; + StationID[i] = NetIDtoStationID[i].Value; + } + } +} diff --git a/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs b/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs new file mode 100644 index 0000000..1c511ad --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonChatPacket.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Packets.Common; + +public class CommonChatPacket +{ + + public string message { get; set; } + +} diff --git a/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs new file mode 100644 index 0000000..0445a11 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/CommonItemChangePacket.cs @@ -0,0 +1,128 @@ +using LiteNetLib.Utils; +using Multiplayer.Networking.Data; +using System.Collections.Generic; +using System; + +namespace Multiplayer.Networking.Packets.Common; + +public class CommonItemChangePacket : INetSerializable +{ + private const int COMPRESS_AFTER_COUNT = 50; + + public List Items = new List(); + + public void Deserialize(NetDataReader reader) + { + + Items.Clear(); + + //Multiplayer.LogDebug(()=>"CommonItemChangePacket.Deserialize()"); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Deserialize()\r\nBytes: {BitConverter.ToString(reader.RawData).Replace("-", " ")}"); + + try + { + bool compressed = reader.GetBool(); + if (compressed) + { + DeserializeCompressed(reader); + } + else + { + DeserializeRaw(reader); + } + + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Deserialize() post-itemCount {Items?.Count} "); + } + catch (Exception ex) + { + Multiplayer.LogError($"Error in CommonItemChangePacket.Deserialize: {ex.Message}"); + } + } + + private void DeserializeCompressed(NetDataReader reader) + { + int itemCount = reader.GetInt(); + byte[] compressedData = reader.GetBytesWithLength(); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.DeserializeCompressed() itemCount {itemCount} length: {compressedData.Length}"); + + byte[] decompressedData = PacketCompression.Decompress(compressedData); + //Multiplayer.Log($"CommonItemChangePacket.DeserializeCompressed() Compressed: {compressedData.Length} Decompressed: {decompressedData.Length}"); + + NetDataReader decompressedReader = new NetDataReader(decompressedData); + + //Items.Capacity = itemCount; + + for (int i = 0; i < itemCount; i++) + { + var item = new ItemUpdateData(); + item.Deserialize(decompressedReader); + Items.Add(item); + } + } + + private void DeserializeRaw(NetDataReader reader) + { + int itemCount = reader.GetInt(); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.DeserializeRaw() itemCount: {itemCount}"); + + for (int i = 0; i < itemCount; i++) + { + var item = new ItemUpdateData(); + item.Deserialize(reader); + Items.Add(item); + } + } + + public void Serialize(NetDataWriter writer) + { + //Multiplayer.LogDebug(() => "CommonItemChangePacket.Serialize()"); + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Serialize() Data Before\r\nBytes: {BitConverter.ToString(writer.CopyData()).Replace("-", " ")}"); + + try + { + if (Items.Count > COMPRESS_AFTER_COUNT) + { + SerializeCompressed(writer); + } + else + { + SerializeRaw(writer); + } + + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Serialize() Data After\r\nBytes: {BitConverter.ToString(writer.CopyData()).Replace("-", " ")}"); + } + catch (Exception ex) + { + Multiplayer.LogError($"CommonItemChangePacket.Serialize: {ex.Message}\r\n{ex.StackTrace}"); + } + } + + private void SerializeCompressed(NetDataWriter writer) + { + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Serialize() Compressing. Item Count: {Items.Count}"); + writer.Put(true); // compressed data stream + writer.Put(Items.Count); + + NetDataWriter dataWriter = new NetDataWriter(); + + foreach (var item in Items) + { + item.Serialize(dataWriter); + } + + byte[] compressedData = PacketCompression.Compress(dataWriter.Data); + //Multiplayer.LogDebug(() => $"Uncompressed: {dataWriter.Length} Compressed: {compressedData.Length}"); + writer.PutBytesWithLength(compressedData); + } + + private void SerializeRaw(NetDataWriter writer) + { + //Multiplayer.LogDebug(() => $"CommonItemChangePacket.Serialize() Raw. Item Count: {Items.Count}"); + writer.Put(false); // uncompressed data stream + writer.Put(Items.Count); + foreach (var item in Items) + { + item.Serialize(writer); + } + } +} diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonCouplerInteractionPacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonCouplerInteractionPacket.cs new file mode 100644 index 0000000..8766f28 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/Train/CommonCouplerInteractionPacket.cs @@ -0,0 +1,13 @@ +using System; + +namespace Multiplayer.Networking.Packets.Common.Train; + + +public class CommonCouplerInteractionPacket +{ + public ushort NetId { get; set; } + public ushort OtherNetId { get; set; } + public bool IsFrontCoupler { get; set; } + public bool IsFrontOtherCoupler { get; set; } + public ushort Flags { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs new file mode 100644 index 0000000..236ef10 --- /dev/null +++ b/Multiplayer/Networking/Packets/Common/Train/CommonPaintThemePacket.cs @@ -0,0 +1,8 @@ +namespace Multiplayer.Networking.Packets.Common.Train; + +public class CommonPaintThemePacket +{ + public ushort NetId { get; set; } + public byte TargetArea { get; set; } + public sbyte PaintThemeId { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs index 3600589..ce04a4c 100644 --- a/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs +++ b/Multiplayer/Networking/Packets/Common/Train/CommonTrainCouplePacket.cs @@ -4,7 +4,9 @@ public class CommonTrainCouplePacket { public ushort NetId { get; set; } public bool IsFrontCoupler { get; set; } + public byte State { get; set; } public ushort OtherNetId { get; set; } + public byte OtherState { get; set; } public bool OtherCarIsFrontCoupler { get; set; } public bool PlayAudio { get; set; } public bool ViaChainInteraction { get; set; } diff --git a/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs b/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs index 590c7b0..de54339 100644 --- a/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs +++ b/Multiplayer/Networking/Packets/Common/Train/CommonTrainUncouplePacket.cs @@ -4,6 +4,8 @@ public class CommonTrainUncouplePacket { public ushort NetId { get; set; } public bool IsFrontCoupler { get; set; } + public byte State { get; set; } + public byte OtherState { get; set; } public bool PlayAudio { get; set; } public bool ViaChainInteraction { get; set; } public bool DueToBrokenCouple { get; set; } diff --git a/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs new file mode 100644 index 0000000..8e51f85 --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Jobs/ServerboundJobValidateRequestPacket.cs @@ -0,0 +1,9 @@ +using Multiplayer.Networking.Data; +namespace Multiplayer.Networking.Packets.Clientbound.Jobs; + +public class ServerboundJobValidateRequestPacket +{ + public ushort JobNetId { get; set; } + public ushort StationNetId { get; set; } + public ValidationType validationType { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs deleted file mode 100644 index 8ca39e9..0000000 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerCarPacket.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Multiplayer.Networking.Packets.Serverbound; - -public class ServerboundPlayerCarPacket -{ - public ushort CarId { get; set; } -} diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerPositionPacket.cs b/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerPositionPacket.cs index b4f1f3c..c13d141 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerPositionPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/ServerboundPlayerPositionPacket.cs @@ -8,4 +8,5 @@ public class ServerboundPlayerPositionPacket public Vector2 MoveDir { get; set; } public float RotationY { get; set; } public byte IsJumpingIsOnCar { get; set; } + public ushort CarID { get; set; } } diff --git a/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundAddCoalPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundAddCoalPacket.cs new file mode 100644 index 0000000..a3a1763 --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundAddCoalPacket.cs @@ -0,0 +1,7 @@ +namespace Multiplayer.Networking.Packets.Serverbound.Train; + +public class ServerboundAddCoalPacket +{ + public ushort NetId { get; set; } + public float CoalMassDelta { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundFireboxIgnitePacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundFireboxIgnitePacket.cs new file mode 100644 index 0000000..c9ededb --- /dev/null +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundFireboxIgnitePacket.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Networking.Packets.Serverbound.Train; + +public class ServerboundFireboxIgnitePacket +{ + public ushort NetId { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainDeleteRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainDeleteRequestPacket.cs similarity index 60% rename from Multiplayer/Networking/Packets/Serverbound/ServerboundTrainDeleteRequestPacket.cs rename to Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainDeleteRequestPacket.cs index bcf2023..0de0e6e 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainDeleteRequestPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainDeleteRequestPacket.cs @@ -1,4 +1,4 @@ -namespace Multiplayer.Networking.Packets.Serverbound; +namespace Multiplayer.Networking.Packets.Serverbound.Train; public class ServerboundTrainDeleteRequestPacket { diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainRerailRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainRerailRequestPacket.cs similarity index 79% rename from Multiplayer/Networking/Packets/Serverbound/ServerboundTrainRerailRequestPacket.cs rename to Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainRerailRequestPacket.cs index 8b5da12..62aefad 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainRerailRequestPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainRerailRequestPacket.cs @@ -1,6 +1,6 @@ using UnityEngine; -namespace Multiplayer.Networking.Packets.Serverbound; +namespace Multiplayer.Networking.Packets.Serverbound.Train; public class ServerboundTrainRerailRequestPacket { diff --git a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainSyncRequestPacket.cs b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainSyncRequestPacket.cs similarity index 60% rename from Multiplayer/Networking/Packets/Serverbound/ServerboundTrainSyncRequestPacket.cs rename to Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainSyncRequestPacket.cs index be4950e..91ec9dc 100644 --- a/Multiplayer/Networking/Packets/Serverbound/ServerboundTrainSyncRequestPacket.cs +++ b/Multiplayer/Networking/Packets/Serverbound/Train/ServerboundTrainSyncRequestPacket.cs @@ -1,4 +1,4 @@ -namespace Multiplayer.Networking.Packets.Serverbound; +namespace Multiplayer.Networking.Packets.Serverbound.Train; public class ServerboundTrainSyncRequestPacket { diff --git a/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs b/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs new file mode 100644 index 0000000..2c01c51 --- /dev/null +++ b/Multiplayer/Networking/Packets/Unconnected/UnconnectedDiscoveryPacket.cs @@ -0,0 +1,10 @@ +using LiteNetLib.Utils; +using Multiplayer.Networking.Data; + +namespace Multiplayer.Networking.Packets.Unconnected; + +public class UnconnectedDiscoveryPacket +{ + public bool IsResponse { get; set; } = false; + public LobbyServerData Data { get; set; } +} diff --git a/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs b/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs new file mode 100644 index 0000000..5ee2159 --- /dev/null +++ b/Multiplayer/Networking/Packets/Unconnected/UnconnectedPingPacket.cs @@ -0,0 +1,6 @@ +namespace Multiplayer.Networking.Packets.Unconnected; + +public class UnconnectedPingPacket +{ + public byte[] ServerID { get; set; } +} diff --git a/Multiplayer/Networking/Serialization/QuaternionSerializer.cs b/Multiplayer/Networking/Serialization/QuaternionSerializer.cs new file mode 100644 index 0000000..cd95a4d --- /dev/null +++ b/Multiplayer/Networking/Serialization/QuaternionSerializer.cs @@ -0,0 +1,20 @@ +using LiteNetLib.Utils; +using UnityEngine; + +namespace Multiplayer.Networking.Serialization; + +public static class QuaternionSerializer +{ + public static void Serialize(NetDataWriter writer, Quaternion quat) + { + writer.Put(quat.x); + writer.Put(quat.y); + writer.Put(quat.z); + writer.Put(quat.w); + } + + public static Quaternion Deserialize(NetDataReader reader) + { + return new Quaternion(reader.GetFloat(), reader.GetFloat(), reader.GetFloat(), reader.GetFloat()); + } +} diff --git a/Multiplayer/Networking/TransportLayers/ITransport.cs b/Multiplayer/Networking/TransportLayers/ITransport.cs new file mode 100644 index 0000000..39c4ec1 --- /dev/null +++ b/Multiplayer/Networking/TransportLayers/ITransport.cs @@ -0,0 +1,55 @@ +using LiteNetLib; +using System.Net.Sockets; +using System.Net; +using System; +using LiteNetLib.Utils; + +namespace Multiplayer.Networking.TransportLayers; +public interface ITransport +{ + NetStatistics Statistics { get; } + bool IsRunning { get; } + + + bool Start(); + bool Start(int port); + bool Start(IPAddress ipv4, IPAddress ipv6, int port); + void Stop(bool sendDisconnectPackets); + void PollEvents(); + void UpdateSettings(Settings settings); + + // Connection management + ITransportPeer Connect(string address, int port, NetDataWriter data); + void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod); + + // Events + event Action OnConnectionRequest; + event Action OnPeerConnected; + event Action OnPeerDisconnected; + event Action OnNetworkReceive; + event Action OnNetworkError; + event Action OnNetworkLatencyUpdate; +} + +public interface IConnectionRequest +{ + ITransportPeer Accept(); + void Reject(NetDataWriter data = null); + IPEndPoint RemoteEndPoint { get; } +} + +public interface ITransportPeer +{ + int Id { get; } + TransportConnectionState ConnectionState { get; } + void Send(NetDataWriter writer, DeliveryMethod deliveryMethod); + void Disconnect(NetDataWriter data = null); +} + +public enum TransportConnectionState +{ + Connected, + Connecting, + Disconnected, + Disconnecting +} diff --git a/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs new file mode 100644 index 0000000..ab8209c --- /dev/null +++ b/Multiplayer/Networking/TransportLayers/LiteNetLibTransport.cs @@ -0,0 +1,231 @@ +using LiteNetLib; +using LiteNetLib.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; + +namespace Multiplayer.Networking.TransportLayers; + +public class LiteNetLibTransport : ITransport, INetEventListener +{ + public NetStatistics Statistics => netManager.Statistics; + public bool IsRunning => netManager?.IsRunning ?? false; + + public event Action OnConnectionRequest; + public event Action OnPeerConnected; + public event Action OnPeerDisconnected; + public event Action OnNetworkReceive; + public event Action OnNetworkError; + public event Action OnNetworkLatencyUpdate; + + private readonly Dictionary netPeerToPeer = []; + + private readonly NetManager netManager; + + #region ITransport + public LiteNetLibTransport() + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.LiteNetLibTransport()"); + netManager = new NetManager(this) + { + DisconnectTimeout = 10000, + UnconnectedMessagesEnabled = true, + BroadcastReceiveEnabled = true, + }; + } + + public bool Start() + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Start()"); + return netManager.Start(); + } + + public bool Start(int port) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Start({port})"); + return netManager.Start(port); + } + + public bool Start(IPAddress ipv4, IPAddress ipv6, int port) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Start({ipv4}, {ipv6}, {port})"); + return netManager.Start(ipv4, ipv6, port); + } + + public void Stop(bool sendDisconnectPackets) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.Stop()"); + netManager.Stop(sendDisconnectPackets); + } + + public void PollEvents() + { + netManager.PollEvents(); + } + + public ITransportPeer Connect(string address, int port, NetDataWriter data) + { + var netPeer = netManager.Connect(address, port, data); + var peer = new LiteNetLibPeer(netPeer); + + //Multiplayer.LogDebug(() => $"LiteNetLibTransport.Connect length: {data.Length}. packet: {BitConverter.ToString(data.Data)}"); + + netPeerToPeer[netPeer] = peer; + return peer; + } + + public void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod) + { + var litePeer = (LiteNetLibPeer)peer; + litePeer.Send(writer, deliveryMethod); + } + #endregion + + #region INetEventListener + void INetEventListener.OnConnectionRequest(ConnectionRequest request) + { + //Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnConnectionRequest({request.RemoteEndPoint})"); + OnConnectionRequest?.Invoke(request.Data, new LiteNetLibConnectionRequest(request, this)); + } + + void INetEventListener.OnPeerConnected(NetPeer netPeer) + { + var peer = new LiteNetLibPeer(netPeer); + + netPeerToPeer[netPeer] = peer; + + OnPeerConnected?.Invoke(peer); + } + + void INetEventListener.OnPeerDisconnected(NetPeer netPeer, DisconnectInfo disconnectInfo) + { + if(!netPeerToPeer.TryGetValue(netPeer, out var peer)) + return; + + OnPeerDisconnected?.Invoke(peer, disconnectInfo.Reason); + + netPeerToPeer.Remove(netPeer); + CleanupPeerDictionaries(); + } + + + void INetEventListener.OnNetworkReceive(NetPeer netPeer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) + { + //Multiplayer.LogDebug(() => $"LiteNetLibTransport.OnNetworkReceive({netPeer?.Id})"); + + if (netPeerToPeer.TryGetValue(netPeer, out var peer)) + { + //Multiplayer.LogDebug(() => $"LiteNetLibTransport.OnNetworkReceive({netPeer?.Id}) peer: {peer != null}"); + OnNetworkReceive?.Invoke(peer, reader, channelNumber, deliveryMethod); + } + } + + void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketError) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnNetworkError({endPoint}, {socketError})"); + OnNetworkError?.Invoke(endPoint, socketError); + } + + void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.OnNetworkReceiveUnconnected({remoteEndPoint}, {messageType})"); + } + + void INetEventListener.OnNetworkLatencyUpdate(NetPeer netPeer, int latency) + { + if (netPeerToPeer.TryGetValue(netPeer, out var peer)) + OnNetworkLatencyUpdate?.Invoke(peer, latency); + } + + #endregion + + public void UpdateSettings(Settings settings) + { + Multiplayer.LogDebug(() => $"LiteNetLibTransport.INetEventListener.UpdateSettings()"); + //only look at LiteNetLib settings + netManager.NatPunchEnabled = settings.EnableNatPunch; + netManager.AutoRecycle = settings.ReuseNetPacketReaders; + netManager.UseNativeSockets = settings.UseNativeSockets; + netManager.EnableStatistics = settings.ShowStats; + netManager.SimulatePacketLoss = settings.SimulatePacketLoss; + netManager.SimulateLatency = settings.SimulateLatency; + netManager.SimulationPacketLossChance = settings.SimulationPacketLossChance; + netManager.SimulationMinLatency = settings.SimulationMinLatency; + netManager.SimulationMaxLatency = settings.SimulationMaxLatency; + } + + private void CleanupPeerDictionaries() + { + var nullPeers = netPeerToPeer.Where(kvp => kvp.Key == null || kvp.Value == null).ToList(); + foreach (var pair in nullPeers) + { + netPeerToPeer.Remove(pair.Key); + } + } + public void RegisterPeer(NetPeer netPeer, LiteNetLibPeer peer) + { + netPeerToPeer[netPeer] = peer; + } + +} + +public class LiteNetLibConnectionRequest : IConnectionRequest +{ + private readonly ConnectionRequest request; + private readonly LiteNetLibTransport transport; + + public LiteNetLibConnectionRequest(ConnectionRequest request, LiteNetLibTransport transport) + { + this.request = request; + this.transport = transport; + } + + public ITransportPeer Accept() + { + var netPeer = request.Accept(); + var peer = new LiteNetLibPeer(netPeer); + transport.RegisterPeer(netPeer, peer); + + return peer; + } + + public void Reject(NetDataWriter data = null) + { + request.Reject(data); + } + + public IPEndPoint RemoteEndPoint => request.RemoteEndPoint; +} + +public class LiteNetLibPeer : ITransportPeer +{ + private readonly NetPeer peer; + public int Id => peer.Id; + + public LiteNetLibPeer(NetPeer peer) + { + this.peer = peer; + } + + public void Send(NetDataWriter writer, DeliveryMethod deliveryMethod) + { + peer.Send(writer, deliveryMethod); + } + + public void Disconnect(NetDataWriter data = null) + { + peer.Disconnect(data); + } + + public TransportConnectionState ConnectionState => peer.ConnectionState switch + { + LiteNetLib.ConnectionState.Connected => TransportConnectionState.Connected, + LiteNetLib.ConnectionState.Outgoing => TransportConnectionState.Connecting, + LiteNetLib.ConnectionState.Disconnected => TransportConnectionState.Disconnected, + LiteNetLib.ConnectionState.ShutdownRequested => TransportConnectionState.Disconnecting, + _ => TransportConnectionState.Disconnected + }; + +} diff --git a/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs new file mode 100644 index 0000000..69d206d --- /dev/null +++ b/Multiplayer/Networking/TransportLayers/SteamworksTransport.cs @@ -0,0 +1,422 @@ +using LiteNetLib.Utils; +using LiteNetLib; +using System; +using System.Net.Sockets; +using System.Net; +using Steamworks; +using System.Collections.Generic; +using Steamworks.Data; +using System.Runtime.InteropServices; + + +namespace Multiplayer.Networking.TransportLayers; + +public class SteamWorksTransport : ITransport +{ + public NetStatistics Statistics => new NetStatistics(); + public bool IsRunning { get; private set; } + + public event Action OnConnectionRequest; + public event Action OnPeerConnected; + public event Action OnPeerDisconnected; + public event Action OnNetworkReceive; + public event Action OnNetworkError; + public event Action OnNetworkLatencyUpdate; + + private readonly List servers = []; + private SteamClientManager client; + + + private readonly Dictionary peerIdToPeer = []; + private readonly Dictionary connectionToPeer = []; + + private int nextPeerId = 1; + + #region ITransport + public SteamWorksTransport() + { + //static fields for SteamNetworking + } + + public bool Start() + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start()"); + return true;//return Start(0); + } + + public bool Start(int port) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start({port})"); + + var server = SteamNetworkingSockets.CreateNormalSocket(NetAddress.AnyIp((ushort)port)); + if (server != null) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start({port}) Normal not null"); + server.transport = this; + servers.Add(server); + IsRunning = true; + } + + server = SteamNetworkingSockets.CreateRelaySocket(); + + if (server != null) + { + server.transport = this; + servers.Add(server); + IsRunning = true; + + Multiplayer.Log($"SteamId: {Steamworks.Data.NetIdentity.LocalHost}"); + } + + + return IsRunning; + } + + public bool Start(IPAddress ipv4, IPAddress ipv6, int port) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Start({ipv4}, {ipv6}, {port})"); + return Start(port); + } + + public void Stop(bool sendDisconnectPackets) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.Stop()"); + + client?.Close(true); + + + while (servers.Count > 0) + { + if (servers[0] != null) + { + foreach (var connection in servers[0].Connected) + connection.Close(true, (int)NetConnectionEnd.App_Generic); + } + + servers.RemoveAt(0); + } + } + + public void PollEvents() + { + SteamClient.RunCallbacks(); + + client?.Receive(); + + + foreach (var server in servers) + { + server?.Receive(); + } + + //update pings + foreach (var kvp in connectionToPeer) + { + var peer = kvp.Value; + var connection = kvp.Key; + + if(peer != null && connection != null) + OnNetworkLatencyUpdate?.Invoke(peer, connection.QuickStatus().Ping / 2); //nromalise to match LiteNetLib's implementation + } + } + + public ITransportPeer Connect(string address, int port, NetDataWriter data) + { + //Multiplayer.LogDebug(() => $"SteamWorksTransport.Connect({address}, {port}, {data.Length})"); + + if (port < 0) + return ConnectRelay(address, data); + else + return ConnectNative(address, port, data); + } + + public ITransportPeer ConnectNative(string address, int port, NetDataWriter data) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectNative({address}, {port}, {data.Length})"); + + var add = NetAddress.From(address, (ushort)port); + + + //Multiplayer.LogDebug(() => $"SteamWorksTransport.Connect packet: {BitConverter.ToString(data.Data)}"); + + // Create connection manager for client + client = SteamNetworkingSockets.ConnectNormal(add); + client.transport = this; + client.loginPacket = data; + client.peer = CreatePeer(client.Connection); + + return client.peer; + } + + public ITransportPeer ConnectRelay(string steamID, NetDataWriter data) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectRelay({steamID})"); + + SteamId id = new(); + if (!ulong.TryParse(steamID, out id.Value)) + { + Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectRelay({steamID}) failed to parse"); + return null; + } + + + //Multiplayer.LogDebug(() => $"SteamWorksTransport.ConnectRelay packet: {BitConverter.ToString(data.Data)}"); + + // Create connection manager for client + client = SteamNetworkingSockets.ConnectRelay(id); + client.transport = this; + client.loginPacket = data; + client.peer = CreatePeer(client.Connection); + + return client.peer; + } + + + public void Send(ITransportPeer peer, NetDataWriter writer, DeliveryMethod deliveryMethod) + { + //Multiplayer.LogDebug(() => $"SteamWorksTransport.Send({peer.Id}, {deliveryMethod})"); + peer.Send(writer, deliveryMethod); + } + + public void UpdateSettings(Settings settings) + { + //todo: implement any settings + } + + #endregion + + #region SteamManagers + public class SteamServerManager : SocketManager + { + public SteamWorksTransport transport; + + public override void OnConnecting(Connection connection, ConnectionInfo info) + { + + //Multiplayer.LogDebug(() => $"SteamServerManager.OnConnecting({connection}, {info})"); + connection.Accept(); + } + + public override void OnConnected(Connection connection, ConnectionInfo info) + { + //Multiplayer.LogDebug(() => $"SteamServerManager.OnConnected({connection}, {info})"); + base.OnConnected(connection, info); + + var peer = transport.CreatePeer(connection); + peer.connectionRequest = new SteamConnectionRequest(connection, info, peer); + transport?.OnPeerConnected?.Invoke(peer); + } + + public override void OnDisconnected(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) + { + //Multiplayer.LogDebug(() => $"SteamServerManager.OnDisconnected({connection}, {info})"); + base.OnDisconnected(connection, info); + var peer = transport.GetPeer(connection); + + transport?.OnPeerDisconnected?.Invoke(peer, NetConnectionEndToDisconnectReason(info.EndReason)); + } + + public override void OnMessage(Steamworks.Data.Connection connection, Steamworks.Data.NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel) + { + //Multiplayer.LogDebug(() => $"SteamServerManager.OnMessage({connection}, {identity}, , {size}, {messageNum}, {recvTime}, {channel})"); + + var peer = transport.GetPeer(connection); + + byte[] buffer = new byte[size]; + Marshal.Copy(data, buffer, 0, size); + + + //Multiplayer.LogDebug(() => $"SteamServerManager.Received packet: {BitConverter.ToString(buffer)}"); + + var reader = new NetDataReader(buffer, 0, size); + if (peer.connectionRequest != null) + { + transport?.OnConnectionRequest?.Invoke(reader, peer.connectionRequest); + peer.connectionRequest = null; + return; + } + + transport?.OnNetworkReceive?.Invoke(peer, reader, (byte)channel, DeliveryMethod.ReliableOrdered); + + //base.OnMessage(connection,identity,data,size,messageNum,recvTime,channel); + } + + public override void OnConnectionChanged(Steamworks.Data.Connection connection, Steamworks.Data.ConnectionInfo info) + { + //Multiplayer.LogDebug(() => $"SteamServerManager.OnConnectionChanged({connection}, {info})"); + base.OnConnectionChanged(connection, info); + if (transport.GetPeer(connection) is SteamPeer peer) + { + peer.OnConnectionStatusChanged(info.State); + } + } + } + + public class SteamClientManager : ConnectionManager + { + public SteamWorksTransport transport; + public NetDataWriter loginPacket; + public SteamPeer peer; + + public override void OnConnected(ConnectionInfo info) + { + Multiplayer.LogDebug(() => $"SteamClientManager.OnConnected({info})"); + base.OnConnected(info); + transport.IsRunning = true; + peer.Send(loginPacket, DeliveryMethod.ReliableUnordered); + transport?.OnPeerConnected?.Invoke(peer); + } + + public override void OnConnecting(ConnectionInfo info) + { + //Multiplayer.LogDebug(() => $"SteamClientManager.OnConnecting({info})"); + base.OnConnecting(info); + } + + public override void OnDisconnected(ConnectionInfo info) + { + Multiplayer.LogDebug(() => $"SteamClientManager.OnDisconnected({info.EndReason})"); + base.OnDisconnected(info); + transport?.OnPeerDisconnected?.Invoke(peer, NetConnectionEndToDisconnectReason(info.EndReason)); + } + + public override void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) + { + //Multiplayer.LogDebug(() => $"SteamClientManager.Connection(,{size}, {messageNum}, {recvTime}, {channel})"); + + byte[] buffer = new byte[size]; + Marshal.Copy(data, buffer, 0, size); + + var reader = new NetDataReader(buffer, 0, size); + transport?.OnNetworkReceive?.Invoke(peer, reader, (byte)channel, DeliveryMethod.ReliableOrdered); + //base.OnMessage(data, size, messageNum, recvTime, channel); + } + + public override void OnConnectionChanged(ConnectionInfo info) + { + base.OnConnectionChanged(info); + peer?.OnConnectionStatusChanged(info.State); + } + } + #endregion + + private SteamPeer CreatePeer(Connection connection) + { + var peer = new SteamPeer(nextPeerId++, connection); + connectionToPeer[connection] = peer; + peerIdToPeer[peer.Id] = peer; + return peer; + } + + private SteamPeer GetPeer(Connection connection) + { + return connectionToPeer.TryGetValue(connection, out var peer) ? peer : null; + } + + public static DisconnectReason NetConnectionEndToDisconnectReason(NetConnectionEnd reason) + { + return reason switch + { + NetConnectionEnd.Remote_Timeout => DisconnectReason.Timeout, + NetConnectionEnd.Misc_Timeout => DisconnectReason.Timeout, + NetConnectionEnd.Remote_BadProtocolVersion => DisconnectReason.InvalidProtocol, + NetConnectionEnd.Remote_BadCrypt => DisconnectReason.ConnectionFailed, + NetConnectionEnd.Remote_BadCert => DisconnectReason.ConnectionRejected, + NetConnectionEnd.Local_OfflineMode => DisconnectReason.NetworkUnreachable, + NetConnectionEnd.Local_NetworkConfig => DisconnectReason.NetworkUnreachable, + NetConnectionEnd.Misc_P2P_NAT_Firewall => DisconnectReason.PeerToPeerConnection, + NetConnectionEnd.Local_P2P_ICE_NoPublicAddresses => DisconnectReason.PeerNotFound, + NetConnectionEnd.Remote_P2P_ICE_NoPublicAddresses => DisconnectReason.PeerNotFound, + NetConnectionEnd.Misc_PeerSentNoConnection => DisconnectReason.PeerNotFound, + NetConnectionEnd.App_Generic => DisconnectReason.DisconnectPeerCalled, + _ => DisconnectReason.ConnectionFailed + }; + } +} + +public class SteamConnectionRequest : IConnectionRequest +{ + private readonly Connection connection; + private readonly ConnectionInfo connectionInfo; + private readonly SteamPeer peer; + + public SteamConnectionRequest(Connection connection, ConnectionInfo connectionInfo, SteamPeer peer) + { + this.connection = connection; + this.connectionInfo = connectionInfo; + this.peer = peer; + } + + public ITransportPeer Accept() + { + return peer; + } + public void Reject(NetDataWriter data = null) + { + if (data != null) + peer?.Send(data, DeliveryMethod.ReliableUnordered); + + connection.Close(true); + } + + public IPEndPoint RemoteEndPoint => new(IPAddress.Any, 0); +} + + +public class SteamPeer : ITransportPeer +{ + private readonly Connection connection; + private TransportConnectionState _currentState; + public SteamConnectionRequest connectionRequest; + public int Id { get; } + + public SteamPeer(int id, Connection connection) + { + Id = (int)id; + this.connection = connection; + } + + public void Send(NetDataWriter writer, DeliveryMethod deliveryMethod) + { + //Multiplayer.LogDebug(() => $"SteamPeer.Send({writer.Data.Length})\r\n{Environment.StackTrace}"); + // Map LiteNetLib delivery method to Steam's SendType + SendType sendType = deliveryMethod switch + { + DeliveryMethod.ReliableOrdered => SendType.Reliable, + DeliveryMethod.ReliableUnordered => SendType.Reliable, + DeliveryMethod.Unreliable => SendType.Unreliable, + DeliveryMethod.ReliableSequenced => SendType.Reliable, + DeliveryMethod.Sequenced => SendType.Unreliable, + _ => SendType.Reliable + }; + + connection.SendMessage(writer.Data, 0, writer.Length, sendType); + } + + public void Disconnect(NetDataWriter data = null) + { + if (data != null) + Send(data, DeliveryMethod.ReliableUnordered); + + connection.Close(true); + } + + public void OnConnectionStatusChanged(Steamworks.ConnectionState state) + { + + _currentState = state switch + { + Steamworks.ConnectionState.Connected => TransportConnectionState.Connected, + Steamworks.ConnectionState.Connecting => TransportConnectionState.Connecting, + Steamworks.ConnectionState.FindingRoute => TransportConnectionState.Connecting, + Steamworks.ConnectionState.ClosedByPeer => TransportConnectionState.Disconnected, + Steamworks.ConnectionState.ProblemDetectedLocally => TransportConnectionState.Disconnected, + Steamworks.ConnectionState.FinWait => TransportConnectionState.Disconnecting, + Steamworks.ConnectionState.Linger => TransportConnectionState.Disconnecting, + Steamworks.ConnectionState.Dead => TransportConnectionState.Disconnected, + Steamworks.ConnectionState.None => TransportConnectionState.Disconnected, + _ => TransportConnectionState.Disconnected + }; + } + public TransportConnectionState ConnectionState => _currentState; +} diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs index 0cd194a..731595b 100644 --- a/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCarDeleterPatch.cs @@ -3,6 +3,7 @@ using DV.InventorySystem; using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; using UnityEngine; @@ -21,14 +22,18 @@ private static bool OnUse_Prefix(CommsRadioCarDeleter __instance) return true; if (Inventory.Instance.PlayerMoney < __instance.removePrice) return true; - if (__instance.carToDelete.Networked().HasPlayers) + + __instance.carToDelete.TryNetworked(out NetworkedTrainCar networkedTrainCar); + + if (networkedTrainCar == null || networkedTrainCar != null && (networkedTrainCar.HasPlayers || networkedTrainCar.NetId == 0)) { + Multiplayer.LogDebug(() => $"CommsRadioCarDeleter unable to delete car: {__instance.carToDelete.name}, hasPlayer: {networkedTrainCar?.HasPlayers}, netId {networkedTrainCar?.NetId} "); CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); __instance.ClearFlags(); return false; } - NetworkLifecycle.Instance.Client.SendTrainDeleteRequest(__instance.carToDelete.GetNetId()); + NetworkLifecycle.Instance.Client.SendTrainDeleteRequest(networkedTrainCar.NetId); CoroutineManager.Instance.StartCoroutine(PlaySoundsLater(__instance, __instance.carToDelete.transform.position, __instance.removePrice > 0)); __instance.ClearFlags(); @@ -37,7 +42,7 @@ private static bool OnUse_Prefix(CommsRadioCarDeleter __instance) private static IEnumerator PlaySoundsLater(CommsRadioCarDeleter __instance, Vector3 trainPosition, bool playMoneyRemovedSound = true) { - yield return new WaitForSecondsRealtime(NetworkLifecycle.Instance.Client.Ping * 2); + yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 3f)/1000); if (playMoneyRemovedSound && __instance.moneyRemovedSound != null) __instance.moneyRemovedSound.Play2D(); // The TrainCar may already be deleted when we're done waiting, so we play the sound manually. @@ -57,7 +62,7 @@ private static bool OnUpdate_Prefix(CommsRadioCarDeleter __instance) if (!Physics.Raycast(__instance.signalOrigin.position, __instance.signalOrigin.forward, out __instance.hit, CommsRadioCarDeleter.SIGNAL_RANGE, __instance.trainCarMask)) return true; TrainCar car = TrainCar.Resolve(__instance.hit.transform.root); - if (car != null && !car.Networked().HasPlayers) + if (car != null && car.TryNetworked(out NetworkedTrainCar networkedTrainCar) && !networkedTrainCar.HasPlayers) return true; __instance.PointToCar(null); return false; diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCarSpawnerPatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCarSpawnerPatch.cs new file mode 100644 index 0000000..092e587 --- /dev/null +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCarSpawnerPatch.cs @@ -0,0 +1,43 @@ +using System.Collections; +using DV; +using DV.InventorySystem; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Patches.CommsRadio; + + +[HarmonyPatch(typeof(CommsRadioCarSpawner))] +public static class CommsRadioCarSpawnerPatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(CommsRadioCarSpawner.OnUse))] + private static bool OnUse_Prefix(CommsRadioCarSpawner __instance) + { + if (__instance.state != CommsRadioCarSpawner.State.PickDestination) + return true; + if (NetworkLifecycle.Instance.IsHost()) + return true; + + //temporarily disable client spawning + CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); + __instance.ClearFlags(); + return false; + + } +} + + //private static IEnumerator PlaySoundsLater(CommsRadioCarDeleter __instance, Vector3 trainPosition, bool playMoneyRemovedSound = true) + //{ + // yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 3f)/1000); + // if (playMoneyRemovedSound && __instance.moneyRemovedSound != null) + // __instance.moneyRemovedSound.Play2D(); + // // The TrainCar may already be deleted when we're done waiting, so we play the sound manually. + // __instance.removeCarSound.Play(trainPosition, minDistance: CommsRadioController.CAR_AUDIO_SOURCE_MIN_DISTANCE, parent: WorldMover.Instance.originShiftParent); + // CommsRadioController.PlayAudioFromRadio(__instance.confirmSound, __instance.transform); + //} + + diff --git a/Multiplayer/Patches/CommsRadio/CommsRadioCrewVehiclePatch.cs b/Multiplayer/Patches/CommsRadio/CommsRadioCrewVehiclePatch.cs new file mode 100644 index 0000000..1d20128 --- /dev/null +++ b/Multiplayer/Patches/CommsRadio/CommsRadioCrewVehiclePatch.cs @@ -0,0 +1,62 @@ +using System.Collections; +using DV; +using DV.InventorySystem; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Patches.CommsRadio; + +[HarmonyPatch(typeof(CommsRadioCrewVehicle))] +public static class CommsRadioCrewVehiclePatch +{ + [HarmonyPrefix] + [HarmonyPatch(nameof(CommsRadioCrewVehicle.OnUse))] + private static bool OnUse_Prefix(CommsRadioCrewVehicle __instance) + { + if (__instance.CurrentState != CommsRadioCrewVehicle.State.ConfirmSummon) + return true; + if (NetworkLifecycle.Instance.IsHost()) + return true; + if (Inventory.Instance.PlayerMoney < __instance.SummonPrice) + return true; + + //temporarily disable client spawning + CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); + __instance.ClearFlags(); + return false; + + /* + if(!NetworkedRailTrack.TryGetFromRailTrack(__instance.destinationTrack, out NetworkedRailTrack netRailTrack)) + { + Multiplayer.LogError($"CommsRadioCrewVehicle unable to spawn car, NetworkedRailTrack not found for: {__instance.destinationTrack.name}"); + CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); + __instance.ClearFlags(); + return false; + } + + Vector3 absPos = (Vector3)__instance.closestPointOnDestinationTrack.Value.position; + Vector3 fwd = __instance.closestPointOnDestinationTrack.Value.forward; + + NetworkLifecycle.Instance.Client.SendTrainSpawnRequest(__instance.selectedCar.livery.id, netRailTrack.NetId, absPos, fwd); + + CoroutineManager.Instance.StartCoroutine(PlaySoundsLater(__instance, absPos, __instance.SummonPrice > 0)); + __instance.ClearFlags(); + + */ + return false; + } + + private static IEnumerator PlaySoundsLater(CommsRadioCrewVehicle __instance, Vector3 trainPosition, bool playMoneyRemovedSound = true) + { + yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 3f)/1000); + if (playMoneyRemovedSound && __instance.moneyRemovedSound != null) + __instance.moneyRemovedSound.Play2D(); + // The TrainCar may already be deleted when we're done waiting, so we play the sound manually. + //__instance.removeCarSound.Play(trainPosition, minDistance: CommsRadioController.CAR_AUDIO_SOURCE_MIN_DISTANCE, parent: WorldMover.Instance.originShiftParent); + CommsRadioController.PlayAudioFromRadio(__instance.confirmSound, __instance.transform); + } +} diff --git a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs index 06c4ab7..3534dab 100644 --- a/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs +++ b/Multiplayer/Patches/CommsRadio/RerailControllerPatch.cs @@ -3,6 +3,7 @@ using DV.InventorySystem; using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; using Multiplayer.Utils; using UnityEngine; @@ -12,6 +13,16 @@ namespace Multiplayer.Patches.CommsRadio; [HarmonyPatch(typeof(RerailController))] public static class RerailControllerPatch { + [HarmonyPostfix] + [HarmonyPatch(nameof(RerailController.Awake))] + private static void OnAwake_Prefix(RerailController __instance) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + NetworkLifecycle.Instance.Server.rerailController = __instance; + } + [HarmonyPrefix] [HarmonyPatch(nameof(RerailController.OnUse))] private static bool OnUse_Prefix(RerailController __instance) @@ -25,8 +36,19 @@ private static bool OnUse_Prefix(RerailController __instance) if (Inventory.Instance.PlayerMoney < __instance.rerailPrice) return true; + __instance.carToRerail.TryNetworked(out NetworkedTrainCar networkedTrainCar); + + if (networkedTrainCar == null || networkedTrainCar != null && networkedTrainCar.NetId == 0) + { + Multiplayer.LogDebug(() => $"RerailController unable to rerail car: {__instance.carToRerail.name}, netId {networkedTrainCar?.NetId} "); + //CommsRadioController.PlayAudioFromRadio(__instance.cancelSound, __instance.transform); + __instance.ClearFlags(); + return false; + } + + NetworkLifecycle.Instance.Client.SendTrainRerailRequest( - __instance.carToRerail.GetNetId(), + networkedTrainCar.NetId, NetworkedRailTrack.GetFromRailTrack(__instance.rerailTrack).NetId, __instance.rerailPointWorldAbsPosition, __instance.rerailPointWorldForward @@ -39,7 +61,7 @@ private static bool OnUse_Prefix(RerailController __instance) private static IEnumerator PlayerSoundsLater(RerailController __instance) { - yield return new WaitForSecondsRealtime(NetworkLifecycle.Instance.Client.Ping * 2); + yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 3f)/1000); if (__instance.moneyRemovedSound != null) __instance.moneyRemovedSound.Play2D(); CommsRadioController.PlayAudioFromCar(__instance.rerailingSound, __instance.carToRerail); @@ -57,7 +79,7 @@ private static bool OnUpdate_Prefix(RerailController __instance) if (!Physics.Raycast(__instance.signalOrigin.position, __instance.signalOrigin.forward, out __instance.hit, RerailController.SIGNAL_RANGE, __instance.trainCarMask)) return true; TrainCar car = TrainCar.Resolve(__instance.hit.transform.root); - if (car != null && car.IsRerailAllowed && !car.Networked().HasPlayers) + if (car != null && car.IsRerailAllowed && car.TryNetworked(out NetworkedTrainCar networkedTrainCar) && !networkedTrainCar.HasPlayers) return true; __instance.PointToCar(null); return false; diff --git a/Multiplayer/Patches/Jobs/BookletCreatorPatch.cs b/Multiplayer/Patches/Jobs/BookletCreatorPatch.cs new file mode 100644 index 0000000..9bccf2d --- /dev/null +++ b/Multiplayer/Patches/Jobs/BookletCreatorPatch.cs @@ -0,0 +1,72 @@ +using DV.Booklets; +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using UnityEngine; + + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(BookletCreator))] +public static class BookletCreator_Patch +{ + [HarmonyPatch(nameof(BookletCreator.CreateJobOverview))] + [HarmonyPostfix] + private static void CreateJobOverview(JobOverview __result, Job job) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"BookletCreatorJob_Patch.CreateJobOverview() NetworkedJob not found for Job ID: {job.ID}"); + } + else + { + NetworkedItem netItem = __result.GetOrAddComponent(); + netItem.Initialize(__result, 0, false); + networkedJob.JobOverview = netItem; + } + } + + [HarmonyPatch(nameof(BookletCreator.CreateJobBooklet))] + [HarmonyPostfix] + private static void CreateJobBooklet(JobBooklet __result, Job job) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"CreateJobBooklet() NetworkedJob not found for Job ID: {job.ID}"); + } + else + { + NetworkedItem netItem = __result.GetOrAddComponent(); + netItem.Initialize(__result, 0, false); + networkedJob.JobBooklet = netItem; + } + } + + [HarmonyPatch(nameof(BookletCreator.CreateJobReport))] + [HarmonyPostfix] + private static void CreateJobReport(JobReport __result, Job job) + { + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (!NetworkedJob.TryGetFromJob(job, out NetworkedJob networkedJob)) + { + Multiplayer.LogError($"CreateJobReport() NetworkedJob not found for Job ID: {job.ID}"); + } + else + { + NetworkedItem netItem = __result.GetOrAddComponent(); + netItem.Initialize(__result, 0, false); + networkedJob.JobReport = netItem; + } + } +} diff --git a/Multiplayer/Patches/Jobs/JobBookletPatch.cs b/Multiplayer/Patches/Jobs/JobBookletPatch.cs new file mode 100644 index 0000000..6dc12f1 --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobBookletPatch.cs @@ -0,0 +1,40 @@ +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; + + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(JobBooklet))] +public static class JobBooklet_Patch +{ + //[HarmonyPatch(nameof(JobBooklet.AssignJob))] + //[HarmonyPostfix] + //private static void AssignJob(JobBooklet __instance, Job jobToAssign) + //{ + // if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + // { + // Multiplayer.LogError($"JobBooklet.AssignJob() NetworkedJob not found for Job ID: {__instance.job?.ID}"); + // return; + // } + + // networkedJob.JobBooklet = __instance; + // if(networkedJob.TryGetComponent(out NetworkedItem netItem)) + // networkedJob.ValidationItem = netItem; + //} + + + [HarmonyPatch(nameof(JobBooklet.DestroyJobBooklet))] + [HarmonyPrefix] + private static void DestroyJobBooklet(JobBooklet __instance) + { + if (__instance == null || __instance.job == null) + return; + + if (!NetworkedJob.TryGetFromJob(__instance?.job, out NetworkedJob networkedJob)) + Multiplayer.LogError($"JobBooklet.DestroyJobBooklet() NetworkedJob not found for Job ID: {__instance?.job?.ID}"); + else + networkedJob.JobBooklet = null; + } +} diff --git a/Multiplayer/Patches/Jobs/JobOverviewPatch.cs b/Multiplayer/Patches/Jobs/JobOverviewPatch.cs new file mode 100644 index 0000000..c5ba156 --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobOverviewPatch.cs @@ -0,0 +1,45 @@ +using DV; +using DV.Interaction; +using DV.Logic.Job; +using DV.ThingTypes; +using DV.Utils; +using HarmonyLib; +using Multiplayer.Components; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Utils; +using System.Collections; +using Unity.Jobs; +using UnityEngine; +using static UnityEngine.GraphicsBuffer; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(JobOverview))] +public static class JobOverview_Patch +{ + //[HarmonyPatch(nameof(JobOverview.Start))] + //[HarmonyPostfix] + //private static void Start(JobOverview __instance) + //{ + // if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + // { + // Multiplayer.LogError($"JobOverview.Start() NetworkedJob not found for Job ID: {__instance.job?.ID}"); + // __instance.DestroyJobOverview(); + // return; + // } + + // networkedJob.JobOverview = __instance; + //} + + + [HarmonyPatch(nameof(JobOverview.DestroyJobOverview))] + [HarmonyPrefix] + private static void DestroyJobOverview(JobOverview __instance) + { + if (!NetworkedJob.TryGetFromJob(__instance.job, out NetworkedJob networkedJob)) + Multiplayer.LogError($"JobOverview.DestroyJobOverview() NetworkedJob not found for Job ID: {__instance.job}"); + else + networkedJob.JobOverview = null; + } +} diff --git a/Multiplayer/Patches/Jobs/JobPatch.cs b/Multiplayer/Patches/Jobs/JobPatch.cs new file mode 100644 index 0000000..be3f6a4 --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobPatch.cs @@ -0,0 +1,21 @@ +using DV.Interaction; +using DV.Logic.Job; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Multiplayer.Patches.Jobs; + +//[HarmonyPatch(typeof(Job), nameof(Job.ExpireJob))] +//public static class JobPatch +//{ +// private static bool Prefix(Job __instance) +// { +// Multiplayer.LogWarning($"Trying to expire {__instance.ID}\r\n"+ new System.Diagnostics.StackTrace()); +// return false; +// } +//} + diff --git a/Multiplayer/Patches/Jobs/JobValidatorPatch.cs b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs new file mode 100644 index 0000000..79c7a90 --- /dev/null +++ b/Multiplayer/Patches/Jobs/JobValidatorPatch.cs @@ -0,0 +1,137 @@ +using System.Collections; +using DV.ThingTypes; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Jobs; +using Multiplayer.Components.Networking.World; +using Multiplayer.Networking.Data; +using UnityEngine; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(JobValidator))] +public static class JobValidator_Patch +{ + [HarmonyPatch(nameof(JobValidator.Start))] + [HarmonyPostfix] + private static void Start(JobValidator __instance) + { + //Multiplayer.Log($"JobValidator Awake!"); + NetworkedStationController.QueueJobValidator(__instance); + } + + + [HarmonyPatch(nameof(JobValidator.ProcessJobOverview))] + [HarmonyPrefix] + private static bool ProcessJobOverview(JobValidator __instance, JobOverview jobOverview) + { + + if(__instance.bookletPrinter.IsOnCooldown) + { + __instance.bookletPrinter.PlayErrorSound(); + return false; + } + + if(!NetworkedJob.TryGetFromJob(jobOverview.job, out NetworkedJob networkedJob) || jobOverview.job.State != JobState.Available) + { + NetworkLifecycle.Instance.Client.LogWarning($"Processing JobOverview {jobOverview?.job?.ID} {(networkedJob == null ? "NetworkedJob not found!, " : "")}Job state: {jobOverview?.job?.State}"); + __instance.bookletPrinter.PlayErrorSound(); + jobOverview.DestroyJobOverview(); + return false; + } + + if (NetworkLifecycle.Instance.IsHost()) + { + NetworkLifecycle.Instance.Server.Log($"Processing JobOverview {jobOverview?.job?.ID}"); + networkedJob.JobValidator = __instance; + return true; + } + + if (!networkedJob.ValidatorRequestSent) + SendValidationRequest(__instance, networkedJob, ValidationType.JobOverview); + + return false; + } + + + [HarmonyPatch(nameof(JobValidator.ValidateJob))] + [HarmonyPrefix] + private static bool ValidateJob_Prefix(JobValidator __instance, JobBooklet jobBooklet) + { + if (__instance.bookletPrinter.IsOnCooldown) + { + __instance.bookletPrinter.PlayErrorSound(); + return false; + } + + if (!NetworkedJob.TryGetFromJob(jobBooklet.job, out NetworkedJob networkedJob) || jobBooklet.job.State != JobState.InProgress) + { + NetworkLifecycle.Instance.Client.LogWarning($"Validating Job {jobBooklet?.job?.ID} {(networkedJob == null ? "NetworkedJob not found!, " : "")}Job state: {jobBooklet?.job?.State}"); + __instance.bookletPrinter.PlayErrorSound(); + jobBooklet.DestroyJobBooklet(); + return false; + } + + if (NetworkLifecycle.Instance.IsHost()) + { + NetworkLifecycle.Instance.Server.Log($"Validating Job {jobBooklet?.job?.ID}"); + networkedJob.JobValidator = __instance; + return true; + } + + if (!networkedJob.ValidatorRequestSent) + SendValidationRequest(__instance, networkedJob, ValidationType.JobBooklet); + + return false; + } + + private static void SendValidationRequest(JobValidator validator,NetworkedJob netJob, ValidationType type) + { + //find the current station we're at + if (NetworkedStationController.GetFromJobValidator(validator, out NetworkedStationController networkedStation)) + { + //Set initial job state parameters + netJob.ValidatorRequestSent = true; + netJob.ValidatorResponseReceived = false; + netJob.ValidationAccepted = false; + netJob.JobValidator = validator; + netJob.ValidationType = type; + + NetworkLifecycle.Instance.Client.SendJobValidateRequest(netJob, networkedStation); + CoroutineManager.Instance.StartCoroutine(AwaitResponse(validator, netJob)); + } + else + { + NetworkLifecycle.Instance.Client.LogError($"Failed to validate {type} for {netJob?.Job?.ID}. NetworkedStation not found!"); + validator.bookletPrinter.PlayErrorSound(); + } + } + private static IEnumerator AwaitResponse(JobValidator validator, NetworkedJob networkedJob) + { + yield return new WaitForSecondsRealtime((NetworkLifecycle.Instance.Client.Ping * 4f)/1000); + + bool received = networkedJob.ValidatorResponseReceived; + bool accepted = networkedJob.ValidationAccepted; + + var receivedStr = received ? "received" : "timed out"; + var acceptedStr = accepted ? " Accepted" : " Rejected"; + + NetworkLifecycle.Instance.Client.Log($"Job Validation Response {receivedStr} for {networkedJob?.Job?.ID}.{acceptedStr}"); + + if (networkedJob == null) + { + validator.bookletPrinter.PlayErrorSound(); + yield break; + } + + if(!received || !accepted) + { + validator.bookletPrinter.PlayErrorSound(); + } + + networkedJob.ValidatorRequestSent = false; + networkedJob.ValidatorResponseReceived = false; + networkedJob.ValidationAccepted = false; + + } +} diff --git a/Multiplayer/Patches/Jobs/StationControllerPatch.cs b/Multiplayer/Patches/Jobs/StationControllerPatch.cs new file mode 100644 index 0000000..dc89c2d --- /dev/null +++ b/Multiplayer/Patches/Jobs/StationControllerPatch.cs @@ -0,0 +1,25 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(StationController))] +public static class StationController_Patch +{ + [HarmonyPatch(nameof(StationController.Awake))] + [HarmonyPostfix] + public static void Awake(StationController __instance) + { + __instance.gameObject.AddComponent(); + } + + [HarmonyPatch(nameof(StationController.ExpireAllAvailableJobsInStation))] + [HarmonyPrefix] + public static bool ExpireAllAvailableJobsInStation(StationController __instance) + { + return NetworkLifecycle.Instance.IsHost(); + } + + +} diff --git a/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs new file mode 100644 index 0000000..e77279f --- /dev/null +++ b/Multiplayer/Patches/Jobs/StationJobGenerationRangePatch.cs @@ -0,0 +1,40 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using UnityEngine; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(StationJobGenerationRange), nameof(StationJobGenerationRange.PlayerSqrDistanceFromStationCenter), MethodType.Getter)] +public static class StationJobGenerationRange_PlayerSqrDistanceFromStationCenter_Patch +{ + private static bool Prefix(StationJobGenerationRange __instance, ref float __result) + { + if (!NetworkLifecycle.Instance.IsHost()) + return true; + + Vector3 anchor = __instance.stationCenterAnchor.position; + + __result = anchor.AnyPlayerSqrMag(); + + //Multiplayer.Log($"PlayerSqrDistanceFromStationCenter() {__result}"); + + return false; + } +} + +[HarmonyPatch(typeof(StationJobGenerationRange), nameof(StationJobGenerationRange.PlayerSqrDistanceFromStationOffice), MethodType.Getter)] +public static class StationJobGenerationRange_PlayerSqrDistanceFromStationOffice_Patch +{ + private static bool Prefix(StationJobGenerationRange __instance, ref float __result) + { + if (!NetworkLifecycle.Instance.IsHost()) + return true; + + Vector3 anchor = __instance.transform.position; + + __result = anchor.AnyPlayerSqrMag(); + + return false; + } +} diff --git a/Multiplayer/Patches/Jobs/StationPatch.cs b/Multiplayer/Patches/Jobs/StationPatch.cs new file mode 100644 index 0000000..add95b2 --- /dev/null +++ b/Multiplayer/Patches/Jobs/StationPatch.cs @@ -0,0 +1,25 @@ +using DV.Logic.Job; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.World; + +namespace Multiplayer.Patches.Jobs; + +[HarmonyPatch(typeof(Station), nameof(Station.AddJobToStation))] +public static class Station_AddJobToStation_Patch +{ + private static bool Prefix(Station __instance, Job job) + { + Multiplayer.Log($"Station.AddJobToStation() adding NetworkJob for stationId: {__instance.ID}, jobId: {job.ID}"); + + if (NetworkLifecycle.Instance.IsHost()) + { + if(!NetworkedStationController.GetFromStationId(__instance.ID, out NetworkedStationController netStationController)) + return false; + + netStationController.AddJob(job); + } + + return true; + } +} diff --git a/Multiplayer/Patches/World/StationProceduralJobsControllerPatch.cs b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs similarity index 90% rename from Multiplayer/Patches/World/StationProceduralJobsControllerPatch.cs rename to Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs index 217630b..0d82e62 100644 --- a/Multiplayer/Patches/World/StationProceduralJobsControllerPatch.cs +++ b/Multiplayer/Patches/Jobs/StationProceduralJobsControllerPatch.cs @@ -1,7 +1,7 @@ using HarmonyLib; using Multiplayer.Components.Networking; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Jobs; [HarmonyPatch(typeof(StationProceduralJobsController), nameof(StationProceduralJobsController.TryToGenerateJobs))] public static class StationProceduralJobsController_TryToGenerateJobs_Patch diff --git a/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs new file mode 100644 index 0000000..9de5090 --- /dev/null +++ b/Multiplayer/Patches/MainMenu/LauncherControllerPatch.cs @@ -0,0 +1,101 @@ +using System; +using DV.Common; +using DV.UI; +using DV.UI.PresetEditors; +using DV.UIFramework; +using HarmonyLib; +using Multiplayer.Components.MainMenu; +using Multiplayer.Utils; +using UnityEngine; +using UnityEngine.UI; + + +namespace Multiplayer.Patches.MainMenu; + +[HarmonyPatch(typeof(LauncherController))] +public static class LauncherController_Patch +{ + private const int PADDING = 10; + + private static GameObject goHost; + //private static LauncherController lcInstance; + + + + [HarmonyPostfix] + [HarmonyPatch(typeof(LauncherController), "OnEnable")] + private static void OnEnable(LauncherController __instance) + { + + //Multiplayer.Log("LauncherController_Patch()"); + + if (goHost != null) + return; + + GameObject goRun = __instance.FindChildByName("ButtonTextIcon Run"); + + if(goRun != null) + { + goRun.SetActive(false); + goHost = GameObject.Instantiate(goRun); + goRun.SetActive(true); + + goHost.name = "ButtonTextIcon Host"; + goHost.transform.SetParent(goRun.transform.parent, false); + + RectTransform btnHostRT = goHost.GetComponentInChildren(); + + Vector3 curPos = btnHostRT.localPosition; + Vector2 curSize = btnHostRT.sizeDelta; + + btnHostRT.localPosition = new Vector3(curPos.x - curSize.x - PADDING, curPos.y,curPos.z); + + Sprite arrowSprite = GameObject.FindObjectOfType().continueButton.FindChildByName("icon").GetComponent().sprite; + __instance.transform.gameObject.UpdateButton("ButtonTextIcon Host", "ButtonTextIcon Host", Locale.SERVER_BROWSER__HOST_KEY, null, arrowSprite); + + // Set up event listeners + Button btnHost = goHost.GetComponent(); + + btnHost.onClick.AddListener(HostAction); + + goHost.SetActive(true); + + //Multiplayer.Log("LauncherController_Patch() complete"); + } + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LauncherController), "SetData", new Type[] { typeof(ISaveGame), typeof(AUserProfileProvider) , typeof(AScenarioProvider) , typeof(LauncherController.UpdateRequest) })] + private static void SetData(LauncherController __instance, ISaveGame saveGame, AUserProfileProvider userProvider, AScenarioProvider scenarioProvider, LauncherController.UpdateRequest updateCallback) + { + if (RightPaneController_Patch.hgpInstance == null) + return; + + RightPaneController_Patch.hgpInstance.saveGame = saveGame; + RightPaneController_Patch.hgpInstance.userProvider = userProvider; + RightPaneController_Patch.hgpInstance.scenarioProvider = scenarioProvider; + + + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(LauncherController), "SetData", new Type[] { typeof(UIStartGameData), typeof(AUserProfileProvider), typeof(AScenarioProvider), typeof(LauncherController.UpdateRequest) })] + private static void SetData(LauncherController __instance, UIStartGameData startGameData, AUserProfileProvider userProvider, AScenarioProvider scenarioProvider, LauncherController.UpdateRequest updateCallback) + { + if (RightPaneController_Patch.hgpInstance == null) + return; + + RightPaneController_Patch.hgpInstance.startGameData = startGameData; + RightPaneController_Patch.hgpInstance.userProvider = userProvider; + RightPaneController_Patch.hgpInstance.scenarioProvider = scenarioProvider; + + } + + private static void HostAction() + { + //Debug.Log("Host button clicked."); + + RightPaneController_Patch.uIMenuController.SwitchMenu(RightPaneController_Patch.hostMenuIndex); + + } +} diff --git a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs index 0f799cb..7fd486f 100644 --- a/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs +++ b/Multiplayer/Patches/MainMenu/LocalizationManagerPatch.cs @@ -1,20 +1,35 @@ using HarmonyLib; using I2.Loc; -namespace Multiplayer.Patches.MainMenu; - -[HarmonyPatch(typeof(LocalizationManager))] -public static class LocalizationManagerPatch +namespace Multiplayer.Patches.MainMenu { - [HarmonyPrefix] - [HarmonyPatch(nameof(LocalizationManager.TryGetTranslation))] - private static bool TryGetTranslation_Prefix(ref bool __result, string Term, out string Translation) + [HarmonyPatch(typeof(LocalizationManager))] + public static class LocalizationManagerPatch { - Translation = string.Empty; - if (!Term.StartsWith(Locale.PREFIX)) - return true; - Translation = Locale.Get(Term); - __result = Translation == Locale.MISSING_TRANSLATION; - return false; + /// + /// Harmony prefix patch for LocalizationManager.TryGetTranslation. + /// + /// The result to be set by the prefix method. + /// The localization term to be translated. + /// The translated text to be set by the prefix method. + /// False if the custom translation logic handles the term, otherwise true to continue to the original method. + [HarmonyPrefix] + [HarmonyPatch(nameof(LocalizationManager.TryGetTranslation))] + private static bool TryGetTranslation_Prefix(ref bool __result, string Term, out string Translation) + { + Translation = string.Empty; + + // Check if the term starts with the specified locale prefix + if (!Term.StartsWith(Locale.PREFIX)) + return true; + + // Attempt to get the translation for the term + Translation = Locale.Get(Term); + + // If the translation is missing, set the result to true and skip the original method + __result = Translation == Locale.MISSING_TRANSLATION; + return false; + } } } + diff --git a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs index be04935..a26efe2 100644 --- a/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/MainMenuControllerPatch.cs @@ -1,50 +1,70 @@ -using DV.Localization; +using DV.Localization; using DV.UI; using HarmonyLib; using Multiplayer.Utils; using UnityEngine; using UnityEngine.UI; -namespace Multiplayer.Patches.MainMenu; - -[HarmonyPatch(typeof(MainMenuController), "Awake")] -public static class MainMenuController_Awake_Patch +namespace Multiplayer.Patches.MainMenu { - public static GameObject MultiplayerButton; - - private static void Prefix(MainMenuController __instance) + /// + /// Harmony patch for the Awake method of MainMenuController to add a Multiplayer button. + /// + [HarmonyPatch(typeof(MainMenuController), "Awake")] + public static class MainMenuController_Awake_Patch { - GameObject button = __instance.FindChildByName("ButtonSelectable Sessions"); - if (button == null) + public static GameObject multiplayerButton; + + /// + /// Prefix method to run before MainMenuController's Awake method. + /// + /// The instance of MainMenuController. + private static void Prefix(MainMenuController __instance) { - Multiplayer.LogError("Failed to find Sessions button!"); - return; - } + // Find the Sessions button to base the Multiplayer button on + GameObject sessionsButton = __instance.FindChildByName("ButtonSelectable Sessions"); + if (sessionsButton == null) + { + Multiplayer.LogError("Failed to find Sessions button!"); + return; + } - button.SetActive(false); - MultiplayerButton = Object.Instantiate(button, button.transform.parent); - button.SetActive(true); + // Deactivate the sessions button temporarily to duplicate it + sessionsButton.SetActive(false); + multiplayerButton = Object.Instantiate(sessionsButton, sessionsButton.transform.parent); + sessionsButton.SetActive(true); - MultiplayerButton.transform.SetSiblingIndex(button.transform.GetSiblingIndex() + 1); - MultiplayerButton.name = "ButtonSelectable Multiplayer"; + // Configure the new Multiplayer button + multiplayerButton.transform.SetSiblingIndex(sessionsButton.transform.GetSiblingIndex() + 1); + multiplayerButton.name = "ButtonSelectable Multiplayer"; - Localize localize = MultiplayerButton.GetComponentInChildren(); - localize.key = Locale.MAIN_MENU__JOIN_SERVER_KEY; + // Set the localization key for the new button + Localize localize = multiplayerButton.GetComponentInChildren(); + localize.key = Locale.MAIN_MENU__JOIN_SERVER_KEY; - // Reset existing localization components that were added when the Sessions button was initialized. - Object.Destroy(MultiplayerButton.GetComponentInChildren()); - UIElementTooltip tooltip = MultiplayerButton.GetComponent(); - tooltip.disabledKey = null; - tooltip.enabledKey = null; + // Remove existing localization components to reset them + Object.Destroy(multiplayerButton.GetComponentInChildren()); + multiplayerButton.ResetTooltip(); - GameObject icon = MultiplayerButton.FindChildByName("icon"); - if (icon == null) - { - Multiplayer.LogError("Failed to find icon on Sessions button, destroying the Multiplayer button!"); - Object.Destroy(MultiplayerButton); - return; + // Set the icon for the new Multiplayer button + SetButtonIcon(multiplayerButton); } - icon.GetComponent().sprite = Multiplayer.AssetIndex.multiplayerIcon; + /// + /// Sets the icon for the Multiplayer button. + /// + /// The button to set the icon for. + private static void SetButtonIcon(GameObject button) + { + GameObject icon = button.FindChildByName("icon"); + if (icon == null) + { + Multiplayer.LogError("Failed to find icon on Sessions button, destroying the Multiplayer button!"); + Object.Destroy(multiplayerButton); + return; + } + + icon.GetComponent().sprite = Multiplayer.AssetIndex.multiplayerIcon; + } } } diff --git a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs index 33467b4..1a75e91 100644 --- a/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs +++ b/Multiplayer/Patches/MainMenu/RightPaneControllerPatch.cs @@ -1,64 +1,113 @@ -using DV.Localization; +using DV.Localization; using DV.UI; using DV.UIFramework; using HarmonyLib; using Multiplayer.Components.MainMenu; +using Multiplayer.Components.Networking; using Multiplayer.Utils; +using Steamworks; +using System; +using System.Linq; +using System.Reflection; +using TMPro; using UnityEngine; namespace Multiplayer.Patches.MainMenu; -[HarmonyPatch(typeof(RightPaneController), "OnEnable")] -public static class RightPaneController_OnEnable_Patch +[HarmonyPatch(typeof(RightPaneController))] +public static class RightPaneController_Patch { - private static void Prefix(RightPaneController __instance) + public static int hostMenuIndex; + public static int joinMenuIndex; + public static UIMenuController uIMenuController; + public static HostGamePane hgpInstance; + + [HarmonyPatch(nameof(RightPaneController.OnEnable))] + [HarmonyPrefix] + private static void OnEnablePre(RightPaneController __instance) { + uIMenuController = __instance.menuController; + // Check if the multiplayer pane already exists if (__instance.HasChildWithName("PaneRight Multiplayer")) return; - GameObject launcher = __instance.FindChildByName("PaneRight Launcher"); - if (launcher == null) + + // Find the base pane for Load/Save + GameObject basePane = __instance.FindChildByName("PaneRight Load/Save"); + if (basePane == null) { Multiplayer.LogError("Failed to find Launcher pane!"); return; } - launcher.SetActive(false); - GameObject multiplayerPane = Object.Instantiate(launcher, launcher.transform.parent); - launcher.SetActive(true); - + // Create a new multiplayer pane based on the base pane + basePane.SetActive(false); + GameObject multiplayerPane = GameObject.Instantiate(basePane, basePane.transform.parent); + basePane.SetActive(true); multiplayerPane.name = "PaneRight Multiplayer"; - __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); - MainMenuController_Awake_Patch.MultiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; - - Object.Destroy(multiplayerPane.GetComponent()); - Object.Destroy(multiplayerPane.FindChildByName("Thumb Background")); - Object.Destroy(multiplayerPane.FindChildByName("Thumbnail")); - Object.Destroy(multiplayerPane.FindChildByName("Savegame Details Background")); - Object.Destroy(multiplayerPane.FindChildByName("ButtonTextIcon Run")); - - GameObject titleObj = multiplayerPane.FindChildByName("Title"); - if (titleObj == null) - { - Multiplayer.LogError("Failed to find title object!"); - return; - } - titleObj.GetComponentInChildren().key = Locale.SERVER_BROWSER__TITLE_KEY; - Object.Destroy(titleObj.GetComponentInChildren()); + // Add the multiplayer pane to the menu controller + __instance.menuController.controlledMenus.Add(multiplayerPane.GetComponent()); + joinMenuIndex = __instance.menuController.controlledMenus.Count - 1; + UIMenuRequester mpButtonReq = MainMenuController_Awake_Patch.multiplayerButton.GetComponent(); + mpButtonReq.requestedMenuIndex = joinMenuIndex; - multiplayerPane.AddComponent(); + // Clean up unnecessary components and child objects + GameObject.Destroy(multiplayerPane.GetComponent()); + GameObject.Destroy(multiplayerPane.GetComponent()); + multiplayerPane.AddComponent(); + // Create and initialize MainMenuThingsAndStuff MainMenuThingsAndStuff.Create(manager => { + /* PopupManager popupManager = null; __instance.FindPopupManager(ref popupManager); + manager.popupManager = popupManager; manager.renamePopupPrefab = __instance.continueLoadNewController.career.renamePopupPrefab; - manager.okPopupPrefab = __instance.continueLoadNewController.career.okPopupPrefab; + manager.okPopupPrefab = __instance.continueLoadNewController.career.okPopupPrefab;*/ manager.uiMenuController = __instance.menuController; }); - multiplayerPane.SetActive(true); - MainMenuController_Awake_Patch.MultiplayerButton.SetActive(true); + // Activate the multiplayer button + MainMenuController_Awake_Patch.multiplayerButton.SetActive(true); + //Multiplayer.Log("At end!"); + + // Check if the host pane already exists + if (__instance.HasChildWithName("PaneRight Host")) + return; + + if (basePane == null) + { + Multiplayer.LogError("Failed to find Load/Save pane!"); + return; + } + + // Create a new host pane based on the base pane + basePane.SetActive(false); + GameObject hostPane = GameObject.Instantiate(basePane, basePane.transform.parent); + basePane.SetActive(true); + hostPane.name = "PaneRight Host"; + + GameObject.Destroy(hostPane.GetComponent()); + GameObject.Destroy(hostPane.GetComponent()); + hgpInstance = hostPane.GetOrAddComponent(); + + // Add the host pane to the menu controller + __instance.menuController.controlledMenus.Add(hostPane.GetComponent()); + hostMenuIndex = __instance.menuController.controlledMenus.Count - 1; + //MainMenuController_Awake_Patch.multiplayerButton.GetComponent().requestedMenuIndex = __instance.menuController.controlledMenus.Count - 1; + } + + [HarmonyPatch(nameof(RightPaneController.OnEnable))] + [HarmonyPostfix] + private static void OnEnablePost(RightPaneController __instance) + { + //SteamMatchmaking.OnLobbyDataChanged += SteamworksUtils.OnLobbyDataChanged; + SteamMatchmaking.OnLobbyInvite += SteamworksUtils.OnLobbyInviteRequest; + SteamFriends.OnGameLobbyJoinRequested += SteamworksUtils.OnLobbyJoinRequest; + + if (Environment.GetCommandLineArgs().Contains("+connect_lobby")) + SteamworksUtils.JoinFromCommandLine(); } } diff --git a/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs b/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs index 72da15c..98cb07a 100644 --- a/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs +++ b/Multiplayer/Patches/Mods/RemoteDispatchPatch.cs @@ -45,7 +45,7 @@ private static void GetPlayerData_Postfix(ref JObject __result) if (!NetworkLifecycle.Instance.IsClientRunning) return; - foreach (NetworkedPlayer player in NetworkLifecycle.Instance.Client.PlayerManager.Players) + foreach (NetworkedPlayer player in NetworkLifecycle.Instance.Client.ClientPlayerManager.Players) { JObject data = new(); diff --git a/Multiplayer/Patches/PauseMenu/PauseMenuControllerPatch.cs b/Multiplayer/Patches/PauseMenu/PauseMenuControllerPatch.cs new file mode 100644 index 0000000..3552aff --- /dev/null +++ b/Multiplayer/Patches/PauseMenu/PauseMenuControllerPatch.cs @@ -0,0 +1,118 @@ +using DV.Localization; +using DV.UI; +using DV.UIFramework; +using HarmonyLib; +using Multiplayer.Components.MainMenu; +using Multiplayer.Components.Networking; +using Multiplayer.Utils; +using System; +using System.Reflection; +using UnityEngine; +using UnityEngine.UI; + +namespace Multiplayer.Patches.PauseMenu; + + + +[HarmonyPatch(typeof(PauseMenuController))] +public static class PauseMenuController_Patch +{ + private static readonly PopupLocalizationKeys popupQuitLocalizationKeys = new PopupLocalizationKeys + { + positiveKey = "yes", + negativeKey = "no", + labelKey = Locale.PAUSE_MENU_QUIT_KEY + }; + private static readonly PopupLocalizationKeys popupDisconnectLocalizationKeys = new PopupLocalizationKeys + { + positiveKey = "yes", + negativeKey = "no", + labelKey = Locale.PAUSE_MENU_DISCONNECT_KEY + }; + + + [HarmonyPatch(nameof(PauseMenuController.Start))] + [HarmonyPostfix] + private static void Start(PauseMenuController __instance) + { + if(NetworkLifecycle.Instance.IsHost()) + return; + + __instance.loadSaveButton.gameObject.SetActive(false); + __instance.tutorialsButton.gameObject.SetActive(false); + } + + [HarmonyPatch(nameof(PauseMenuController.OnExitLevelClicked))] + [HarmonyPrefix] + private static bool OnExitLevelClicked(PauseMenuController __instance) + { + if(NetworkLifecycle.Instance.IsHost()) + return true; + + + if (!__instance.popupManager.CanShowPopup()) + { + Multiplayer.LogWarning("PauseMenuController.OnExitLevelClicked() PopupManager can't show popups at this moment"); + return false; + } + Popup popupPrefab = __instance.yesNoPopupPrefab; + PopupLocalizationKeys locKeys = popupDisconnectLocalizationKeys; + + __instance.popupManager.ShowPopup(popupPrefab, locKeys).Closed += (PopupResult result) => + { + //Negative = 'No', so we're aborting the disconnect + if (result.closedBy == PopupClosedByAction.Negative) + return; + + //Negative = 'No', so we're aborting the disconnect + if (result.closedBy == PopupClosedByAction.Negative) + return; + + FieldInfo eventField = __instance.GetType().GetField("ExitLevelRequested", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (eventField != null) + { + Delegate eventDelegate = (Delegate)eventField.GetValue(__instance); + if (eventDelegate != null) + eventDelegate.DynamicInvoke(); + } + }; + + return false; + } + + [HarmonyPatch("OnQuitClicked")] + [HarmonyPrefix] + private static bool OnQuitClicked(PauseMenuController __instance) + { + if(NetworkLifecycle.Instance.IsHost()) + return true; + + + if (!__instance.popupManager.CanShowPopup()) + { + Multiplayer.LogWarning("PauseMenuController.OnQuitClicked() PopupManager can't show popups at this moment"); + return false; + } + Popup popupPrefab = __instance.yesNoPopupPrefab; + PopupLocalizationKeys locKeys = popupDisconnectLocalizationKeys; + + __instance.popupManager.ShowPopup(popupPrefab, locKeys).Closed += (PopupResult result) => + { + //Negative = 'No', so we're aborting the disconnect + if (result.closedBy == PopupClosedByAction.Negative) + return; + + FieldInfo eventField = __instance.GetType().GetField("QuitGameRequested", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + if (eventField != null) + { + Delegate eventDelegate = (Delegate)eventField.GetValue(__instance); + if (eventDelegate != null) + eventDelegate.DynamicInvoke(); + } + }; + + return false; + } + + +} diff --git a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs index c394305..8dbd78a 100644 --- a/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs +++ b/Multiplayer/Patches/Player/CustomFirstPersonControllerPatch.cs @@ -1,6 +1,7 @@ using HarmonyLib; using Multiplayer.Components.Networking; using Multiplayer.Utils; +using System; using UnityEngine; namespace Multiplayer.Patches.Player; @@ -8,6 +9,8 @@ namespace Multiplayer.Patches.Player; [HarmonyPatch(typeof(CustomFirstPersonController))] public static class CustomFirstPersonControllerPatch { + private const float ROTATION_THRESHOLD = 0.001f; + private static CustomFirstPersonController fps; private static Vector3 lastPosition; @@ -16,9 +19,10 @@ public static class CustomFirstPersonControllerPatch private static bool isJumping; private static bool isOnCar; + private static TrainCar car; - [HarmonyPostfix] [HarmonyPatch(nameof(CustomFirstPersonController.Awake))] + [HarmonyPostfix] private static void CharacterMovement(CustomFirstPersonController __instance) { fps = __instance; @@ -33,6 +37,7 @@ private static void OnDestroy() { if (UnloadWatcher.isQuitting) return; + NetworkLifecycle.Instance.OnTick -= OnTick; PlayerManager.CarChanged -= OnCarChanged; } @@ -40,25 +45,35 @@ private static void OnDestroy() private static void OnCarChanged(TrainCar trainCar) { isOnCar = trainCar != null; - NetworkLifecycle.Instance.Client.SendPlayerCar(!isOnCar ? (ushort)0 : trainCar.GetNetId()); + car = trainCar; } private static void OnTick(uint tick) { - Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.GetWorldAbsolutePlayerPosition(); + if(UnloadWatcher.isUnloading) + return; + + Vector3 position = isOnCar ? PlayerManager.PlayerTransform.localPosition : PlayerManager.PlayerTransform.GetWorldAbsolutePosition(); float rotationY = (isOnCar ? PlayerManager.PlayerTransform.localEulerAngles : PlayerManager.PlayerTransform.eulerAngles).y; - bool positionOrRotationChanged = lastPosition != position || !Mathf.Approximately(lastRotationY, rotationY); + //bool positionOrRotationChanged = lastPosition != position || !Mathf.Approximately(lastRotationY, rotationY); + + bool positionOrRotationChanged = Vector3.Distance(lastPosition, position) > 0 || Math.Abs(lastRotationY - rotationY) > ROTATION_THRESHOLD; + if (!positionOrRotationChanged && sentFinalPosition) return; lastPosition = position; lastRotationY = rotationY; sentFinalPosition = !positionOrRotationChanged; - NetworkLifecycle.Instance.Client.SendPlayerPosition(lastPosition, PlayerManager.PlayerTransform.InverseTransformDirection(fps.m_MoveDir), lastRotationY, isJumping, isOnCar, isJumping || sentFinalPosition); + + ushort carNetID = isOnCar ? car.GetNetId() : (ushort)0; + + NetworkLifecycle.Instance.Client.SendPlayerPosition(lastPosition, PlayerManager.PlayerTransform.InverseTransformDirection(fps.m_MoveDir), lastRotationY, carNetID, isJumping, isOnCar, isJumping || sentFinalPosition); isJumping = false; } + [HarmonyPostfix] [HarmonyPatch(nameof(CustomFirstPersonController.SetJumpParameters))] private static void SetJumpParameters() diff --git a/Multiplayer/Patches/Player/MapMarkersControllerPatch.cs b/Multiplayer/Patches/Player/MapMarkersControllerPatch.cs new file mode 100644 index 0000000..ffa0f09 --- /dev/null +++ b/Multiplayer/Patches/Player/MapMarkersControllerPatch.cs @@ -0,0 +1,13 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.Player; + +namespace Multiplayer.Patches.World; + +[HarmonyPatch(typeof(MapMarkersController), nameof(MapMarkersController.Awake))] +public static class MapMarkersController_Awake_Patch +{ + private static void Postfix(MapMarkersController __instance) + { + __instance.gameObject.AddComponent(); + } +} diff --git a/Multiplayer/Patches/Player/WorldMapPatch.cs b/Multiplayer/Patches/Player/WorldMapPatch.cs deleted file mode 100644 index ebcd469..0000000 --- a/Multiplayer/Patches/Player/WorldMapPatch.cs +++ /dev/null @@ -1,13 +0,0 @@ -using HarmonyLib; -using Multiplayer.Components.Networking.Player; - -namespace Multiplayer.Patches.World; - -[HarmonyPatch(typeof(WorldMap), nameof(WorldMap.Awake))] -public static class WorldMap_Awake_Patch -{ - private static void Postfix(WorldMap __instance) - { - __instance.gameObject.AddComponent(); - } -} diff --git a/Multiplayer/Patches/Train/BogiePatch.cs b/Multiplayer/Patches/Train/BogiePatch.cs index 89d806c..0b407fa 100644 --- a/Multiplayer/Patches/Train/BogiePatch.cs +++ b/Multiplayer/Patches/Train/BogiePatch.cs @@ -3,7 +3,7 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(Bogie), nameof(Bogie.SetupPhysics))] public static class Bogie_SetupPhysics_Patch @@ -23,17 +23,3 @@ private static bool Prefix() return NetworkLifecycle.Instance.IsHost(); } } - -[HarmonyPatch(typeof(Bogie), nameof(Bogie.SetTrack))] -public static class Bogie_SetTrack_Patch -{ - private static void Prefix(Bogie __instance, int newTrackDirection) - { - if (!__instance.Car.TryNetworked(out NetworkedTrainCar networkedTrainCar)) - return; // When the car first gets spawned in by CarSpawner#SpawnExistingCar, this method gets called before the NetworkedTrainCar component is added to the car. - if (__instance.Car.Bogies[0] == __instance) - networkedTrainCar.Bogie1TrackDirection = newTrackDirection; - else if (__instance.Car.Bogies[1] == __instance) - networkedTrainCar.Bogie2TrackDirection = newTrackDirection; - } -} diff --git a/Multiplayer/Patches/Train/CarSpawnerPatch.cs b/Multiplayer/Patches/Train/CarSpawnerPatch.cs index 06d2ae4..c33227a 100644 --- a/Multiplayer/Patches/Train/CarSpawnerPatch.cs +++ b/Multiplayer/Patches/Train/CarSpawnerPatch.cs @@ -2,19 +2,81 @@ using Multiplayer.Components.Networking; using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; +using System.Collections.Generic; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; -[HarmonyPatch(typeof(CarSpawner), nameof(CarSpawner.PrepareTrainCarForDeleting))] -public static class CarSpawner_PrepareTrainCarForDeleting_Patch +[HarmonyPatch(typeof(CarSpawner))] +public static class CarSpawner_Patch { - private static void Prefix(TrainCar trainCar) + [HarmonyPatch(nameof(CarSpawner.PrepareTrainCarForDeleting))] + [HarmonyPrefix] + private static void PrepareTrainCarForDeleting(TrainCar trainCar) { if (UnloadWatcher.isUnloading) return; - if (!trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) + + if (trainCar == null || !trainCar.TryNetworked(out NetworkedTrainCar networkedTrainCar)) return; + networkedTrainCar.IsDestroying = true; - NetworkLifecycle.Instance.Server?.SendDestroyTrainCar(trainCar); + + NetworkLifecycle.Instance.Server?.SendDestroyTrainCar(networkedTrainCar.NetId); + } + + //Called from + [HarmonyPatch(nameof(CarSpawner.SpawnCars))] + [HarmonyPostfix] + private static void SpawnCars(List __result) + { + if (UnloadWatcher.isUnloading) + return; + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (__result == null || __result.Count == 0) + return; + + //Coupling is delayed by AutoCouple(), so a true trainset for the entire consist doesn't exist yet + Multiplayer.LogDebug(() => $"SpawnCars() {__result?.Count} cars spawned, sending to players"); + NetworkLifecycle.Instance.Server.SendSpawnTrainset(__result, true, true); + + } + + [HarmonyPatch(nameof(CarSpawner.SpawnCarFromRemote))] + [HarmonyPostfix] + private static void SpawnCarFromRemote(TrainCar __result) + { + if (UnloadWatcher.isUnloading) + return; + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (__result == null) + return; + + Multiplayer.LogDebug(() => $"SpawnCarFromRemote() {__result?.carLivery?.name} spawned, sending to players"); + NetworkLifecycle.Instance.Server.SendSpawnTrainset([__result], true, true); + + } + + [HarmonyPatch(nameof(CarSpawner.SpawnCarOnClosestTrack))] + [HarmonyPostfix] + private static void SpawnCarOnClosestTrack(TrainCar __result) + { + if (UnloadWatcher.isUnloading) + return; + + if (!NetworkLifecycle.Instance.IsHost()) + return; + + if (__result == null) + return; + + Multiplayer.LogDebug(() => $"SpawnCarOnClosestTrack() {__result?.carLivery?.name} spawned, sending to players"); + NetworkLifecycle.Instance.Server.SendSpawnTrainset([__result], true, true); + } } diff --git a/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs b/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs index 7b9b49d..156ba56 100644 --- a/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs +++ b/Multiplayer/Patches/Train/CarVisitCheckerPatch.cs @@ -1,6 +1,8 @@ using DV; using HarmonyLib; using Multiplayer.Components.Networking; +using Multiplayer.Components.Networking.Train; +using Multiplayer.Networking.Data; namespace Multiplayer.Patches.World; @@ -9,14 +11,55 @@ public static class CarVisitCheckerPatch { [HarmonyPrefix] [HarmonyPatch(nameof(CarVisitChecker.IsRecentlyVisited), MethodType.Getter)] - private static bool IsRecentlyVisited_Prefix(ref bool __result) + private static bool IsRecentlyVisited_Prefix(CarVisitChecker __instance, ref bool __result) { if (NetworkLifecycle.Instance.IsHost() && NetworkLifecycle.Instance.Server.PlayerCount == 1) - return true; - __result = true; + return true; //playing in "vanilla mode" allow game code to run + + if (!NetworkLifecycle.Instance.IsHost()) + { + //if not the host, we want to keep the car from despawning + __instance.playerIsInCar = true; + __result = true; //Pretend there's a player in the car + return false; //don't run our vanilla game code + } + if (NetworkLifecycle.Instance.Server.ServerPlayers.Count == 0) + { + + //no server players (this should only apply to a dedicated server), don't despawn + __instance.playerIsInCar = true; + __result = true; + return false; + } + + //We are the host, check all players against this car + foreach (ServerPlayer player in NetworkLifecycle.Instance.Server.ServerPlayers) + { + if (NetworkedTrainCar.TryGetFromTrainCar(__instance.car, out NetworkedTrainCar netTC)) + { + if (player.CarId == netTC.NetId) + { + __instance.playerIsInCar = true; + __result = true; + return false; + } + } + else + { + //Car was not found, allow it to despawn + __instance.playerIsInCar = false; + __result = false; + return false; + } + } + + //No one on the car + __instance.playerIsInCar = false; + __result = __instance.recentlyVisitedTimer.RemainingTime > 0f; return false; } + /* [HarmonyPrefix] [HarmonyPatch(nameof(CarVisitChecker.RecentlyVisitedRemainingTime), MethodType.Getter)] private static bool RecentlyVisitedRemainingTime_Prefix(ref float __result) @@ -26,4 +69,5 @@ private static bool RecentlyVisitedRemainingTime_Prefix(ref float __result) __result = CarVisitChecker.RECENTLY_VISITED_TIME_THRESHOLD; return false; } + */ } diff --git a/Multiplayer/Patches/Train/CargoModelControllerPatch.cs b/Multiplayer/Patches/Train/CargoModelControllerPatch.cs index 4e70550..70e2f5c 100644 --- a/Multiplayer/Patches/Train/CargoModelControllerPatch.cs +++ b/Multiplayer/Patches/Train/CargoModelControllerPatch.cs @@ -19,8 +19,9 @@ private static bool Prefix(CargoModelController __instance) private static IEnumerator AddCargoOnceInitialized(CargoModelController controller) { NetworkedTrainCar networkedTrainCar; - while ((networkedTrainCar = controller.trainCar.Networked()) == null) + while (!controller.trainCar.TryNetworked(out networkedTrainCar)) yield return null; + AddCargo(controller, networkedTrainCar); } diff --git a/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs new file mode 100644 index 0000000..a424d23 --- /dev/null +++ b/Multiplayer/Patches/Train/CouplerChainInteractionPatch.cs @@ -0,0 +1,33 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data.Train; + +namespace Multiplayer.Patches.Train; + +[HarmonyPatch(typeof(ChainCouplerInteraction))] +public static class ChainCouplerInteractionPatch +{ + [HarmonyPatch(nameof(ChainCouplerInteraction.OnScrewButtonUsed))] + [HarmonyPostfix] + private static void OnScrewButtonUsed(ChainCouplerInteraction __instance) + { + + Multiplayer.LogDebug(() => $"OnScrewButtonUsed({__instance?.couplerAdapter?.coupler?.train?.ID}) state: {__instance.state}"); + + CouplerInteractionType flag = CouplerInteractionType.Start; + if (__instance.state == ChainCouplerInteraction.State.Attached_Tightening_Couple || __instance.state == ChainCouplerInteraction.State.Attached_Tight) + flag |= CouplerInteractionType.CouplerTighten; + else if (__instance.state == ChainCouplerInteraction.State.Attached_Loosening_Uncouple || __instance.state == ChainCouplerInteraction.State.Attached_Loose) + flag |= CouplerInteractionType.CouplerLoosen; + else + Multiplayer.LogDebug(() => + { + TrainCar car = __instance?.couplerAdapter?.coupler?.train; + return $"OnScrewButtonUsed({car?.ID})\r\n{new System.Diagnostics.StackTrace()}"; + }); + + if (flag != CouplerInteractionType.NoAction) + NetworkLifecycle.Instance.Client.SendCouplerInteraction(flag, __instance?.couplerAdapter?.coupler); + } + +} diff --git a/Multiplayer/Patches/Train/CouplerPatch.cs b/Multiplayer/Patches/Train/CouplerPatch.cs index ffe7b93..de8bffa 100644 --- a/Multiplayer/Patches/Train/CouplerPatch.cs +++ b/Multiplayer/Patches/Train/CouplerPatch.cs @@ -1,52 +1,32 @@ using HarmonyLib; using Multiplayer.Components.Networking; -using Multiplayer.Components.Networking.Train; -using Multiplayer.Utils; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; -[HarmonyPatch(typeof(Coupler), nameof(Coupler.CoupleTo))] -public static class Coupler_CoupleTo_Patch -{ - private static void Postfix(Coupler __instance, Coupler other, bool playAudio, bool viaChainInteraction) - { - if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) - return; - NetworkLifecycle.Instance.Client?.SendTrainCouple(__instance, other, playAudio, viaChainInteraction); - } -} -[HarmonyPatch(typeof(Coupler), nameof(Coupler.Uncouple))] -public static class Coupler_Uncouple_Patch +[HarmonyPatch(typeof(Coupler))] +public static class CouplerPatch { - private static void Postfix(Coupler __instance, bool playAudio, bool calledOnOtherCoupler, bool dueToBrokenCouple, bool viaChainInteraction) + [HarmonyPatch(nameof(Coupler.ConnectAirHose))] + [HarmonyPostfix] + private static void ConnectAirHose(Coupler __instance, Coupler other, bool playAudio) { - if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket || calledOnOtherCoupler) - return; - if (!__instance.train.TryNetworked(out NetworkedTrainCar networkedTrainCar) || networkedTrainCar.IsDestroying) - return; - NetworkLifecycle.Instance.Client?.SendTrainUncouple(__instance, playAudio, dueToBrokenCouple, viaChainInteraction); - } -} + //Multiplayer.LogDebug(() => $"ConnectAirHose([{__instance?.train?.ID}, isFront: {__instance?.isFrontCoupler}])\r\n{new System.Diagnostics.StackTrace()}"); -[HarmonyPatch(typeof(Coupler), nameof(Coupler.ConnectAirHose))] -public static class Coupler_ConnectAirHose_Patch -{ - private static void Postfix(Coupler __instance, Coupler other, bool playAudio) - { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; + NetworkLifecycle.Instance.Client?.SendHoseConnected(__instance, other, playAudio); } -} -[HarmonyPatch(typeof(Coupler), nameof(Coupler.DisconnectAirHose))] -public static class Coupler_DisconnectAirHose_Patch -{ - private static void Postfix(Coupler __instance, bool playAudio) + [HarmonyPatch(nameof(Coupler.DisconnectAirHose))] + [HarmonyPostfix] + private static void DisconnectAirHose(Coupler __instance, bool playAudio) { + //Multiplayer.LogDebug(() => $"DisconnectAirHose([{__instance?.train?.ID}, isFront: {__instance?.isFrontCoupler}])\r\n{new System.Diagnostics.StackTrace()}"); if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - NetworkLifecycle.Instance.Client?.SendHoseDisconnected(__instance, playAudio); + NetworkLifecycle.Instance.Client?.SendHoseDisconnected(__instance, playAudio); } + } diff --git a/Multiplayer/Patches/Train/GarageSpawnerPatch.cs b/Multiplayer/Patches/Train/GarageSpawnerPatch.cs new file mode 100644 index 0000000..ee87e61 --- /dev/null +++ b/Multiplayer/Patches/Train/GarageSpawnerPatch.cs @@ -0,0 +1,20 @@ +using HarmonyLib; +using Multiplayer.Components.Networking; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Multiplayer.Patches.Train; + +[HarmonyPatch(typeof(GarageCarSpawner))] +public static class GarageSpawnerPatch +{ + [HarmonyPatch(nameof(GarageCarSpawner.AllowSpawning))] + [HarmonyPrefix] + private static bool AllowSpawning(GarageCarSpawner __instance) + { + //we don't want the client to also spawn + return NetworkLifecycle.Instance.IsHost(); + } +} diff --git a/Multiplayer/Patches/Train/HoseAndCockPatch.cs b/Multiplayer/Patches/Train/HoseAndCockPatch.cs index e2b68fb..62d33fe 100644 --- a/Multiplayer/Patches/Train/HoseAndCockPatch.cs +++ b/Multiplayer/Patches/Train/HoseAndCockPatch.cs @@ -4,7 +4,7 @@ using Multiplayer.Components.Networking.Train; using Multiplayer.Utils; -namespace Multiplayer.Patches.World; +namespace Multiplayer.Patches.Train; [HarmonyPatch(typeof(HoseAndCock), nameof(HoseAndCock.SetCock))] public static class HoseAndCock_SetCock_Patch @@ -13,10 +13,13 @@ private static void Prefix(HoseAndCock __instance, bool open) { if (UnloadWatcher.isUnloading || NetworkLifecycle.Instance.IsProcessingPacket) return; - Coupler coupler = NetworkedTrainCar.GetCoupler(__instance); - NetworkedTrainCar networkedTrainCar = coupler.train.Networked(); + + if (!NetworkedTrainCar.TryGetCoupler(__instance, out Coupler coupler) || !coupler.train.TryNetworked(out NetworkedTrainCar networkedTrainCar)) + return; + if (networkedTrainCar.IsDestroying) return; + NetworkLifecycle.Instance.Client?.SendCockState(networkedTrainCar.NetId, coupler, open); } } diff --git a/Multiplayer/Patches/Train/LocoRestorationControllerPatch.cs b/Multiplayer/Patches/Train/LocoRestorationControllerPatch.cs new file mode 100644 index 0000000..f0243e0 --- /dev/null +++ b/Multiplayer/Patches/Train/LocoRestorationControllerPatch.cs @@ -0,0 +1,28 @@ +using DV.LocoRestoration; +using HarmonyLib; +using Multiplayer.Components.Networking; + +namespace Multiplayer.Patches.Train; +[HarmonyPatch(typeof(LocoRestorationController))] +public static class LocoRestorationControllerPatch +{ + [HarmonyPatch(nameof(LocoRestorationController.Start))] + [HarmonyPrefix] + private static bool Start(LocoRestorationController __instance) + { + if(NetworkLifecycle.Instance.IsHost()) + return true; + + //TrainCar loco = __instance.loco; + //TrainCar second = __instance.secondCar; + + Multiplayer.LogDebug(() => $"LocoRestorationController.Start()"); + + UnityEngine.Object.Destroy(__instance); + + //CarSpawner.Instance.DeleteCar(loco); + //if(second != null) + // CarSpawner.Instance.DeleteCar(second); + return false; + } +} diff --git a/Multiplayer/Patches/Train/MultipleUnitCablePatch.cs b/Multiplayer/Patches/Train/MultipleUnitCablePatch.cs index edb9dbf..90f752d 100644 --- a/Multiplayer/Patches/Train/MultipleUnitCablePatch.cs +++ b/Multiplayer/Patches/Train/MultipleUnitCablePatch.cs @@ -24,8 +24,8 @@ private static void Postfix(MultipleUnitCable __instance, bool playAudio) { if (NetworkLifecycle.Instance.IsProcessingPacket || UnloadWatcher.isUnloading) return; - NetworkedTrainCar networkedTrainCar = __instance.muModule.train.Networked(); - if (networkedTrainCar.IsDestroying) + + if (__instance.muModule.train.TryNetworked(out NetworkedTrainCar networkedTrainCar) && networkedTrainCar.IsDestroying) return; NetworkLifecycle.Instance.Client?.SendMuDisconnected(networkedTrainCar.NetId, __instance, playAudio); } diff --git a/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs new file mode 100644 index 0000000..4338a00 --- /dev/null +++ b/Multiplayer/Patches/Train/TrainsOptimizerPatch.cs @@ -0,0 +1,51 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DV.Logic.Job; +using DV.Utils; + +namespace Multiplayer.Patches.Train; +[HarmonyPatch(typeof(TrainsOptimizer))] +public static class TrainsOptimizerPatch +{ + [HarmonyPatch(nameof(TrainsOptimizer.ForceOptimizationStateOnCars))] + [HarmonyFinalizer] + public static void ForceOptimizationStateOnCars(TrainsOptimizer __instance, Exception __exception, HashSet carsToProcess, bool forceSleep, bool forceStateOnCloseStationaryCars) + { + if (__exception == null) + return; + + Multiplayer.LogDebug(() => + { + Dictionary logicCarToTrainCar = SingletonBehaviour.Instance.logicCarToTrainCar; + + if (carsToProcess == null) + return $"TrainsOptimizer.ForceOptimizationStateOnCars() carsToProcess is null!"; + + StringBuilder sb = new StringBuilder(); + sb.Append($"TrainsOptimizer.ForceOptimizationStateOnCars() iterating over {carsToProcess?.Count} cars:\r\n"); + + int i=0 ; + foreach (Car car in carsToProcess) + { + if (car == null) + sb.AppendLine($"\tCar {i} is null!"); + else + { + bool result = logicCarToTrainCar.TryGetValue(car, out TrainCar trainCar); + + sb.AppendLine($"\tCar {i} id {car?.ID} found TrainCar: {result}, TC ID: {trainCar?.ID}"); + } + + i++; + } + + + return sb.ToString(); + } + ); + } +} + diff --git a/Multiplayer/Patches/Train/UICouplingHelperPatch.cs b/Multiplayer/Patches/Train/UICouplingHelperPatch.cs new file mode 100644 index 0000000..a85f911 --- /dev/null +++ b/Multiplayer/Patches/Train/UICouplingHelperPatch.cs @@ -0,0 +1,64 @@ +using DV.HUD; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data.Train; +using Newtonsoft.Json.Linq; + +namespace Multiplayer.Patches.Train; + + +[HarmonyPatch(typeof(UICouplingHelper))] +public static class UICouplingHelperPatch +{ + [HarmonyPatch(nameof(UICouplingHelper.HandleCoupling))] + [HarmonyPostfix] + private static void HandleCoupling(UICouplingHelper __instance, Coupler coupler, bool advanced) + { + Multiplayer.LogDebug(() => $"UICouplingHelper.HandleCoupling({coupler?.train?.ID}, {advanced})"); + + if (coupler == null) + return; + + Coupler otherCoupler = null; + CouplerInteractionType interaction = CouplerInteractionType.Start; + + if (coupler.IsCoupled()) + { + interaction |= CouplerInteractionType.CoupleViaUI; + otherCoupler = coupler.coupledTo; + + if(advanced) + { + interaction |= CouplerInteractionType.HoseConnect | CouplerInteractionType.CockOpen; + } + + Multiplayer.LogDebug(() => $"UICouplingHelper.HandleCoupling({coupler?.train?.ID}, {advanced}) coupler is front: {coupler?.isFrontCoupler}, otherCoupler: {otherCoupler?.train?.ID}, otherCoupler is front: {otherCoupler?.isFrontCoupler}, action: {interaction}"); + + if (otherCoupler == null) + return; + + /* fix for bug in vanilla game */ + coupler.SetChainTight(true); + coupler.ChainScript.enabled = false; + coupler.ChainScript.enabled = true; + /* end fix for bug */ + } + else + { + interaction |= CouplerInteractionType.UncoupleViaUI; + + if (advanced) + { + interaction |= CouplerInteractionType.HoseDisconnect | CouplerInteractionType.CockClose; + } + + /* fix for bug in vanilla game */ + coupler.state = ChainCouplerInteraction.State.Parked; + coupler.ChainScript.enabled = false; + coupler.ChainScript.enabled = true; + /* end fix for bug */ + } + + NetworkLifecycle.Instance.Client.SendCouplerInteraction(interaction, coupler, otherCoupler); + } +} diff --git a/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs new file mode 100644 index 0000000..09a6e67 --- /dev/null +++ b/Multiplayer/Patches/Train/UnusedTrainCarDeleterPatch.cs @@ -0,0 +1,110 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using Multiplayer.Utils; +using System.Reflection.Emit; +using UnityEngine; +using static HarmonyLib.Code; +using Multiplayer.Networking.Data; +using DV.ThingTypes; +using DV.Logic.Job; +using DV.Utils; +using Multiplayer.Components.Networking; + + +namespace Multiplayer.Patches.Train; + +[HarmonyPatch(typeof(UnusedTrainCarDeleter))] +public static class UnusedTrainCarDeleterPatch +{ + private const int TARGET_LDARG_1 = 4; + private const int TARGET_SKIPS = 5; + public static TrainCar current; + + [HarmonyPatch("AreDeleteConditionsFulfilled")] + public static IEnumerable Transpiler(IEnumerable instructions) + { + int ldarg_1_Counter = 0; + int skipCtr = 0; + bool foundEntry = false; + bool complete = false; + + foreach (CodeInstruction instruction in instructions) + { + //Multiplayer.LogDebug(() => $"Transpiling: {instruction.ToString()} - ldarg_1_Counter: {ldarg_1_Counter}, found: {foundEntry}, complete: {complete}, skip: {skipCtr}, len: {instruction.opcode.Size} + {instruction.operand}"); + if (instruction.opcode == OpCodes.Ldarg_1 && !foundEntry) + { + ldarg_1_Counter++; + foundEntry = ldarg_1_Counter == TARGET_LDARG_1; + } + else if (foundEntry && !complete) + { + if(instruction.opcode == OpCodes.Callvirt) + { + //allow IL_0083: callvirt and IL_0088: callvirt + yield return instruction; + continue; + } + + if (instruction.opcode == OpCodes.Call) + { + complete = true; + yield return CodeInstruction.Call(typeof(DvExtensions), "AnyPlayerSqrMag", [typeof(Vector3)], null); //inject our method + continue; + } + }else if (complete && skipCtr < TARGET_SKIPS) + { + //skip IL_0092: callvirt + //skip IL_0097: call + //skip IL_009C: stloc.s + //skip IL_009E: ldloca.s + //skip IL_00A0: call + + skipCtr++; + yield return new CodeInstruction(OpCodes.Nop); + continue; + } + + yield return instruction; + } + } + + [HarmonyPatch("InstantConditionalDeleteOfUnusedCars")] + [HarmonyPrefix] + public static bool InstantConditionalDeleteOfUnusedCars(UnusedTrainCarDeleter __instance) + { + if(NetworkLifecycle.Instance.IsHost() && NetworkLifecycle.Instance.Server.PlayerCount == 1) + return true; + + return false; + } +/* + [HarmonyPatch("AreDeleteConditionsFulfilled")] + [HarmonyPrefix] + public static void Prefix(UnusedTrainCarDeleter __instance, TrainCar trainCar) + { + string job=""; + + if (trainCar.IsLoco) + { + foreach (TrainCar car in trainCar.trainset.cars) + { + job += $"{car.ID} {SingletonBehaviour.Instance.GetJobOfCar(car, onlyActiveJobs: true)?.ID}, " ; + } + } + + //Multiplayer.LogDebug(() => $"AreDeleteConditionsFulfilled_Prefix({trainCar?.ID}) Visit Checker: {trainCar?.visitChecker?.IsRecentlyVisited}, Livery: {CarTypes.IsAnyLocomotiveOrTender(trainCar?.carLivery)}, Player Spawned: {trainCar?.playerSpawnedCar} jobs: {job}"); + + current = trainCar; + } + + + [HarmonyPatch("AreDeleteConditionsFulfilled")] + [HarmonyPostfix] + public static void Postfix(UnusedTrainCarDeleter __instance, TrainCar trainCar, bool __result) + { + //Multiplayer.LogDebug(() => $"AreDeleteConditionsFulfilled_Postfix({trainCar?.ID}) = {__result}"); + } + */ + +} diff --git a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs index 40949f2..3f32e50 100644 --- a/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs +++ b/Multiplayer/Patches/Train/WindowsBreakingControllerPatch.cs @@ -15,7 +15,21 @@ public static void BreakWindowsFromCollision_Postfix(WindowsBreakingController _ { if (!NetworkLifecycle.Instance.IsHost()) return; - ushort netId = TrainCar.Resolve(__instance.transform).GetNetId(); + + TrainCar car = TrainCar.Resolve(__instance.transform); + if (car == null) + { + Multiplayer.LogWarning($"BreakWindowsFromCollision failed, unable to resolve TrainCar"); + return; + } + + ushort netId = car.GetNetId(); + if(netId == 0) + { + Multiplayer.LogWarning($"BreakWindowsFromCollision failed, {car.name}"); + return; + } + NetworkLifecycle.Instance.Server.SendWindowsBroken(netId, forceDirection); } @@ -25,7 +39,17 @@ public static void RepairWindows_Postfix(WindowsBreakingController __instance) { if (!NetworkLifecycle.Instance.IsHost()) return; - ushort netId = TrainCar.Resolve(__instance.transform).GetNetId(); + + TrainCar car = TrainCar.Resolve(__instance.transform); + ushort netId = car.GetNetId(); + + if (car == null ||netId == 0) + { + Multiplayer.LogWarning($"RepairWindows_Postfix failed, {car?.name}"); + return; + } + + Multiplayer.LogDebug(()=>$"RepairWindows_Postfix, {car.name}"); NetworkLifecycle.Instance.Server.SendWindowsRepaired(netId); } } diff --git a/Multiplayer/Patches/Util/DVSteamworksPatch.cs b/Multiplayer/Patches/Util/DVSteamworksPatch.cs new file mode 100644 index 0000000..1646e43 --- /dev/null +++ b/Multiplayer/Patches/Util/DVSteamworksPatch.cs @@ -0,0 +1,17 @@ +using HarmonyLib; +using Steamworks; + + +namespace Multiplayer.Patches.Util; + +[HarmonyPatch(typeof(DVSteamworks))] +public static class DVSteamworksPatch +{ + [HarmonyPatch(nameof(DVSteamworks.Awake))] + [HarmonyPostfix] + public static void Awake() + { + if (DVSteamworks.Success) + SteamNetworkingUtils.InitRelayNetworkAccess(); + } +} diff --git a/Multiplayer/Patches/Util/FacepunchNetAddressPatch.cs b/Multiplayer/Patches/Util/FacepunchNetAddressPatch.cs new file mode 100644 index 0000000..afc0c74 --- /dev/null +++ b/Multiplayer/Patches/Util/FacepunchNetAddressPatch.cs @@ -0,0 +1,26 @@ +using HarmonyLib; +using Steamworks.Data; +using System.Net; +using System.Net.Sockets; + +namespace Multiplayer.Patches.Util; + +[HarmonyPatch(typeof(NetAddress))] +public static class FacepunchNetAddressPatch +{ + [HarmonyPatch(nameof(NetAddress.From), new[] { typeof(IPAddress), typeof(ushort) })] + [HarmonyPrefix] + private static bool From(IPAddress address, ushort port, ref NetAddress __result) + { + if (address != null && address.AddressFamily == AddressFamily.InterNetworkV6) + { + Multiplayer.LogDebug(() => $"FacepunchNetAddressPatch.From() IPv6"); + NetAddress cleared = NetAddress.Cleared; + var ipv6Bytes = address.GetAddressBytes(); + NetAddress.InternalSetIPv6(ref cleared, ref ipv6Bytes[0], port); + __result = cleared; + return false; + } + return true; + } +} diff --git a/Multiplayer/Patches/World/Items/FlashlightPatch.cs b/Multiplayer/Patches/World/Items/FlashlightPatch.cs new file mode 100644 index 0000000..3a5f713 --- /dev/null +++ b/Multiplayer/Patches/World/Items/FlashlightPatch.cs @@ -0,0 +1,81 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(FlashlightItem))] +public static class FlashlightItemPatch +{ + [HarmonyPatch(nameof(FlashlightItem.Start))] + static void Postfix(FlashlightItem __instance) + { + var networkedItem = __instance.gameObject.GetOrAddComponent(); + networkedItem.Initialize(__instance); + + // Register the values you want to track with both getters and setters + networkedItem.RegisterTrackedValue( + "originalLightIntensity", + () => __instance.originalLightIntensity, + value => __instance.originalLightIntensity = value, + serverAuthoritative: true //This parameter is driven by the server: true + ); + + //probably not needed as flicker can be handled locally + //networkedItem.RegisterTrackedValue( + // "intensity", + // () => __instance.spotlight.intensity, + // value => __instance.spotlight.intensity = value, + // serverAuthoritative: true //This parameter is driven by the server: true + // ); + + networkedItem.RegisterTrackedValue( + "originalBeamColour", + () => __instance.originalBeamColor.ColorToUInt32(), + value =>__instance.originalBeamColor = value.UInt32ToColor(), + serverAuthoritative: true //This parameter is driven by the server: true + ); + + networkedItem.RegisterTrackedValue( + "beamColour", + () => __instance.beamController.GetBeamColor().ColorToUInt32(), + value => + { + Color colour = value.UInt32ToColor(); + __instance.beamController.SetBeamColor(colour); + __instance.spotlight.color = colour; + }, + serverAuthoritative: true //This parameter is driven by the server: true + ); + + networkedItem.RegisterTrackedValue( + "batteryPower", + () => __instance.battery.currentPower, + value => + { + __instance.battery.currentPower = value; //set the value + __instance.battery.UpdatePower(0f); //process a delta of 0 to force an update + }, + (current, last) => Math.Abs(current - last) >= 1.0f, //Don't communicate updates for changes less than 1f + true //This parameter is driven by the server: true + ); + + networkedItem.RegisterTrackedValue( + "buttonState", + () => (__instance.button.Value > 0f), + value => + { + if (value) + __instance.button.SetValue(1f); + else + __instance.button.SetValue(0f); + + __instance.ToggleFlashlight(value); + } + ); + + networkedItem.FinaliseTrackedValues(); + } +} diff --git a/Multiplayer/Patches/World/Items/GrabHandlerItem.cs b/Multiplayer/Patches/World/Items/GrabHandlerItem.cs new file mode 100644 index 0000000..333e5c7 --- /dev/null +++ b/Multiplayer/Patches/World/Items/GrabHandlerItem.cs @@ -0,0 +1,45 @@ +using DV.Interaction; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(GrabHandlerItem))] +public static class GrabHandlerItem_Patch +{ + [HarmonyPatch(nameof(GrabHandlerItem.Throw))] + [HarmonyPrefix] + private static void Throw(GrabHandlerItem __instance, Vector3 direction) + { + __instance.TryGetComponent(out NetworkedItem netItem); + + if (netItem != null) + { + netItem.OnThrow(direction); + } + + } + + + /** + * Patch below methods to get attach/detach events + */ + + //public void AttachToAttachPoint(Transform attachPoint, bool positionStays) + //{ + // this.TogglePhysics(false); + // base.transform.SetParent(attachPoint, positionStays); + //} + + //// Token: 0x060000A7 RID: 167 RVA: 0x000042EC File Offset: 0x000024EC + //public override void EndInteraction() + //{ + // base.transform.parent = null; + // base.EndInteraction(); + // this.TogglePhysics(true); + //} + + + +} diff --git a/Multiplayer/Patches/World/Items/ItemBasePatch.cs b/Multiplayer/Patches/World/Items/ItemBasePatch.cs new file mode 100644 index 0000000..9c899ec --- /dev/null +++ b/Multiplayer/Patches/World/Items/ItemBasePatch.cs @@ -0,0 +1,21 @@ +using DV.CabControls; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(ItemBase))] +public static class ItemBase_Patch +{ + [HarmonyPatch(nameof(ItemBase.Awake))] + [HarmonyPostfix] + private static void Awake(ItemBase __instance) + { + //Multiplayer.Log($"ItemBase.Awake() ItemSpec: {__instance?.InventorySpecs?.itemPrefabName}"); + var networkedItem = __instance.GetOrAddComponent(); + + //networkedItem.FinaliseTrackedValues(); + return; + } +} diff --git a/Multiplayer/Patches/World/Items/LanternPatch.cs b/Multiplayer/Patches/World/Items/LanternPatch.cs new file mode 100644 index 0000000..01f9764 --- /dev/null +++ b/Multiplayer/Patches/World/Items/LanternPatch.cs @@ -0,0 +1,72 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; +using System.Diagnostics; + +namespace Multiplayer.Patches.World.Items; + +/* +[HarmonyPatch(typeof(Lantern))] +public static class LanternPatch +{ + [HarmonyPatch(nameof(Lantern.Awake))] + [HarmonyPostfix] + static void Awake(Lantern __instance) + { + var networkedItem = __instance?.gameObject?.GetOrAddComponent(); + if (networkedItem == null) + { + Multiplayer.LogError($"LanternAwakePatch.Awake() networkedItem returned null!"); + return; + } + + networkedItem.Initialize(__instance); + } + + [HarmonyPatch(nameof(Lantern.Initialize))] + [HarmonyPostfix] + static void Initialize(Lantern __instance) + { + + var networkedItem = __instance?.gameObject?.GetOrAddComponent(); + + if(networkedItem == null) + { + Multiplayer.LogError($"Lantern.Initialize() networkedItem Not Found!"); + return; + } + + try + { + // Register the values you want to track with both getters and setters + networkedItem.RegisterTrackedValue( + "wickSize", + () => __instance.wickSize, + value => + { + __instance.UpdateWickRelatedLogic(value); + } + ); + + networkedItem.RegisterTrackedValue( + "Ignited", + () => __instance.igniter.enabled, + value => + { + if (value) + __instance.Ignite(1); + else + __instance.OnFlameExtinguished(); + } + ); + + networkedItem.FinaliseTrackedValues(); + + }catch(Exception ex) + { + Multiplayer.LogError($"Lantern.Initialize() {ex.Message}\r\n{ex.StackTrace}"); + } + } +} +*/ diff --git a/Multiplayer/Patches/World/Items/LighterPatch.cs b/Multiplayer/Patches/World/Items/LighterPatch.cs new file mode 100644 index 0000000..ba23a46 --- /dev/null +++ b/Multiplayer/Patches/World/Items/LighterPatch.cs @@ -0,0 +1,89 @@ +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(Lighter))] +public static class LighterPatch +{ + [HarmonyPatch(nameof(Lighter.Start))] + [HarmonyPostfix] + static void Start(Lighter __instance) + { + var netItem = __instance.gameObject.GetOrAddComponent(); + netItem.Initialize(__instance); + + Lighter lighter = __instance; + + // Register the values you want to track with both getters and setters + netItem.RegisterTrackedValue( + "isOpen", + () => lighter.isOpen, + value => + { + bool active = lighter.gameObject.activeInHierarchy; + if (active) + { + if (value) + lighter.OpenLid(); + else + lighter.CloseLid(); + } + else + { + lighter.isOpen = value; + if (!value) + lighter.CloseLid(true); + } + } + ); + + netItem.RegisterTrackedValue( + "Ignited", + () => lighter.IsFireOn(), + value => + { + bool active = lighter.gameObject.activeInHierarchy; + if (active) + { + if (value) + lighter.LightFire(true, true); + else + lighter.flame.UpdateFlameIntensity(0f, true); + } + else + { + if (value && lighter.isOpen) + { + lighter.flame.UpdateFlameIntensity(1f, true); + lighter.OnFlameIgnited(); + + } + else + { + lighter.flame.UpdateFlameIntensity(0f, true); + lighter.OnFlameExtinguished(); + } + } + } + ); + + netItem.FinaliseTrackedValues(); + } + + [HarmonyPatch(nameof(Lighter.OnEnable))] + [HarmonyPostfix] + + static void OnEnable(Lighter __instance) + { + if (__instance.isOpen) + { + __instance.lighterAnimator.Play("lighter_case_top_open", 0); + } + } +} diff --git a/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs b/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs new file mode 100644 index 0000000..d62c924 --- /dev/null +++ b/Multiplayer/Patches/World/Items/RemoteControllerModulePatch.cs @@ -0,0 +1,44 @@ +using DV.RemoteControls; +using HarmonyLib; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data.Train; +using Multiplayer.Utils; +using System; +using UnityEngine; + + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(RemoteControllerModule))] +public static class RemoteControllerModulePatch +{ + [HarmonyPatch(nameof(RemoteControllerModule.RemoteControllerCouple))] + [HarmonyPostfix] + static void RemoteControllerCouple(RemoteControllerModule __instance) + { + NetworkLifecycle.Instance.Client.SendCouplerInteraction((CouplerInteractionType.Start | CouplerInteractionType.CoupleViaRemote), __instance.car.frontCoupler); + } + + [HarmonyPatch(nameof(RemoteControllerModule.Uncouple))] + [HarmonyPrefix] + static void Uncouple(RemoteControllerModule __instance, int selectedCoupler) + { + Multiplayer.LogDebug(() => $"RemoteControllerModule.Uncouple({selectedCoupler})"); + + TrainCar startCar = __instance.car; + + if (startCar == null) + { + Multiplayer.LogWarning($"Trying to Uncouple from Remote with no paired loco"); + return; + } + + Coupler nthCouplerFrom = CouplerLogic.GetNthCouplerFrom((selectedCoupler > 0) ? startCar.frontCoupler : startCar.rearCoupler, Mathf.Abs(selectedCoupler) - 1); + + Multiplayer.LogDebug(() => $"RemoteControllerModule.Uncouple({startCar?.ID}, {selectedCoupler}) nthCouplerFrom: [{nthCouplerFrom?.train?.ID}, {nthCouplerFrom?.train?.GetNetId()}]"); + if (nthCouplerFrom != null) + { + NetworkLifecycle.Instance.Client.SendCouplerInteraction((CouplerInteractionType.Start | CouplerInteractionType.UncoupleViaRemote), nthCouplerFrom); + } + } +} diff --git a/Multiplayer/Patches/World/Items/RespawnOnDropPatch.cs b/Multiplayer/Patches/World/Items/RespawnOnDropPatch.cs new file mode 100644 index 0000000..737c6c1 --- /dev/null +++ b/Multiplayer/Patches/World/Items/RespawnOnDropPatch.cs @@ -0,0 +1,87 @@ +using HarmonyLib; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.Emit; +using System.Text; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(RespawnOnDrop))] +[HarmonyPatch("RespawnOrDestroy")] +[HarmonyPatch(MethodType.Enumerator)] +class RespawnOnDropPatch +{ + static IEnumerable Transpiler(IEnumerable instructions) + { + var codes = new List(instructions); + + return codes; //disable pactch temporarily + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Pre-patch:"); + foreach (var code in codes) + { + sb.AppendLine(code.ToString()); + } + + Debug.Log(sb.ToString()); + + // Find base.gameObject.SetActive(false) + // ldloc.1 NULL[Label10] //this is the 'base' loading to the stack + // call UnityEngine.GameObject UnityEngine.Component::get_gameObject() //call to retrieve the gameObject + // ldc.i4.0 NULL //load a 'false' onto the stack + // callvirt System.Void UnityEngine.GameObject::SetActive(System.Boolean value) //call to SetActive() + + int startIndex = -1; + for (int i = 0; i < codes.Count - 1; i++) + { + if (codes[i].opcode == OpCodes.Ldloc_1 && + codes[i + 1].Calls(AccessTools.Method(typeof(Component), "get_gameObject")) && + codes[i + 2].opcode == OpCodes.Ldc_I4_0 && + codes[i + 3].Calls(AccessTools.Method(typeof(GameObject), "SetActive"))) + { + startIndex = i; + break; + } + } + + if (startIndex < 0) + { + Multiplayer.LogError(() => $"RespawnOnDrop.RespawnOrDestroy() transpiler failed - start index not found!"); + return codes.AsEnumerable(); + } + + // Find SingletonBehaviour.Instance.AddItemToLostAndFound(this.item); + int endIndex = codes.FindIndex(startIndex, x => + x.Calls(AccessTools.Method(typeof(StorageController), "AddItemToLostAndFound"))); + + + if (endIndex < 0) + { + Multiplayer.LogError(() => $"RespawnOnDrop.RespawnOrDestroy() transpiler failed - end index not found!"); + return codes.AsEnumerable(); + } + + + // replace 'else' branch with NOPs rather than trying to patch labels + for (int i = startIndex; i <= endIndex; i++) + { + var newNop = new CodeInstruction(OpCodes.Nop); + newNop.labels.AddRange(codes[i].labels); // Maintain any labels on the original instruction + codes[i] = newNop; + } + + sb = new StringBuilder(); + sb.AppendLine("Post-patch:"); + foreach (var code in codes) + { + sb.AppendLine(code.ToString()); + } + + Debug.Log(sb.ToString()); + + return codes.AsEnumerable(); + } +} + diff --git a/Multiplayer/Patches/World/Items/ShovelPatch.cs b/Multiplayer/Patches/World/Items/ShovelPatch.cs new file mode 100644 index 0000000..5234c99 --- /dev/null +++ b/Multiplayer/Patches/World/Items/ShovelPatch.cs @@ -0,0 +1,55 @@ +using DV.CabControls; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; + +[HarmonyPatch(typeof(Shovel))] +public static class ShovelPatch +{ + [HarmonyPatch(nameof(Shovel.Start))] + [HarmonyPostfix] + static void Start(Shovel __instance) + { + var netItem = __instance.gameObject.GetOrAddComponent(); + + netItem.Initialize(__instance); + + ShovelNonPhysicalCoal shovelNonPhysicalCoal = __instance.GetComponent(); + if( shovelNonPhysicalCoal == null) + { + Multiplayer.LogWarning($"Shovel.Start() netId: {netItem.NetId} Failed to find ShovelNonPhysicalCoal"); + return; + } + + // Register the values you want to track with both getters and setters + netItem.RegisterTrackedValue( + "coalMassCapacity", + () => shovelNonPhysicalCoal.coalMassCapacity, + value => + { + shovelNonPhysicalCoal.coalMassCapacity = value; + } + ); + + netItem.RegisterTrackedValue( + "coalMassLoaded", + () => shovelNonPhysicalCoal.coalMassLoaded, + value => + { + shovelNonPhysicalCoal.coalMassLoaded = value; + } + ); + + netItem.FinaliseTrackedValues(); + } + +} diff --git a/Multiplayer/Patches/World/SaveGameManagerPatch.cs b/Multiplayer/Patches/World/SaveGameManagerPatch.cs index 0c8067f..c014da7 100644 --- a/Multiplayer/Patches/World/SaveGameManagerPatch.cs +++ b/Multiplayer/Patches/World/SaveGameManagerPatch.cs @@ -19,7 +19,7 @@ private static void Postfix(AStartGameData __result) private static void StartServer(IDifficulty difficulty) { - if (NetworkLifecycle.Instance.StartServer(Multiplayer.Settings.Port, difficulty)) + if (NetworkLifecycle.Instance.StartServer(difficulty)) return; NetworkLifecycle.Instance.QueueMainMenuEvent(() => diff --git a/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs b/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs index 3906a85..3bf5ff3 100644 --- a/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs +++ b/Multiplayer/Patches/World/StationLocoSpawnerPatch.cs @@ -5,7 +5,7 @@ using DV.Utils; using HarmonyLib; using Multiplayer.Components.Networking; -using Multiplayer.Networking.Data; +using Multiplayer.Utils; using UnityEngine; namespace Multiplayer.Patches.World; @@ -37,7 +37,8 @@ private static IEnumerator CheckShouldSpawn(StationLocoSpawner __instance) { yield return CHECK_DELAY; - bool anyoneWithinRange = IsAnyoneWithinRange(__instance, __instance.spawnTrackMiddleAnchor.transform.position); + + bool anyoneWithinRange = __instance.spawnTrackMiddleAnchor.transform.position.AnyPlayerSqrMag() < __instance.spawnLocoPlayerSqrDistanceFromTrack; switch (__instance.playerEnteredLocoSpawnRange) { @@ -52,14 +53,6 @@ private static IEnumerator CheckShouldSpawn(StationLocoSpawner __instance) } } - private static bool IsAnyoneWithinRange(StationLocoSpawner stationLocoSpawner, Vector3 targetPosition) - { - foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) - if ((serverPlayer.WorldPosition - targetPosition).sqrMagnitude < stationLocoSpawner.spawnLocoPlayerSqrDistanceFromTrack) - return true; - return false; - } - private static void SpawnLocomotives(StationLocoSpawner stationLocoSpawner) { List carsFullyOnTrack = stationLocoSpawner.locoSpawnTrack.logicTrack.GetCarsFullyOnTrack(); diff --git a/Multiplayer/Patches/World/StorageControllerPatch.cs b/Multiplayer/Patches/World/StorageControllerPatch.cs new file mode 100644 index 0000000..df1e981 --- /dev/null +++ b/Multiplayer/Patches/World/StorageControllerPatch.cs @@ -0,0 +1,82 @@ +using DV.CabControls; +using HarmonyLib; +using Multiplayer.Components.Networking.World; +using Multiplayer.Utils; +using System; +using UnityEngine; + +namespace Multiplayer.Patches.World.Items; +/* +[HarmonyPatch(typeof(StorageController))] +public static class StorageControllerPatch +{ + [HarmonyPatch(nameof(StorageController.AddItemToLostAndFound))] + [HarmonyPrefix] + static void AddItemToLostAndFound(StorageController __instance, ItemBase item) + { + + Multiplayer.LogDebug(() => + { + NetworkedItem.TryGetNetworkedItem(item, out NetworkedItem netItem); + return $"StorageController.AddItemToLostAndFound({item.name}) netId: {netItem?.NetId}\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.RemoveItemFromLostAndFound))] + [HarmonyPrefix] + static void RemoveItemFromLostAndFound(StorageController __instance, ItemBase item) + { + + Multiplayer.LogDebug(() => + { + NetworkedItem.TryGetNetworkedItem(item, out NetworkedItem netItem); + return $"StorageController.RemoveItemFromLostAndFound({item.name}) netId: {netItem?.NetId}\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.RequestLostAndFoundItemActivation))] + [HarmonyPrefix] + static void RequestLostAndFoundItemActivation(StorageController __instance) + { + + Multiplayer.LogDebug(() => + { + return $"StorageController.RequestLostAndFoundItemActivation()\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.MoveItemsFromWorldToLostAndFound))] + [HarmonyPrefix] + static void MoveItemsFromWorldToLostAndFound(StorageController __instance, bool ignoreItemsWithRespawnParents) + { + + Multiplayer.LogDebug(() => + { + return $"StorageController.MoveItemsFromWorldToLostAndFound({ignoreItemsWithRespawnParents})\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.ForceSummonAllWorldItemsToLostAndFound))] + [HarmonyPrefix] + static void ForceSummonAllWorldItemsToLostAndFound(StorageController __instance) + { + + Multiplayer.LogDebug(() => + { + return $"StorageController.ForceSummonAllWorldItemsToLostAndFound()\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + + [HarmonyPatch(nameof(StorageController.RequestItemActivation))] + [HarmonyPrefix] + static void RequestItemActivation(StorageController __instance) + { + + Multiplayer.LogDebug(() => + { + return $"StorageController.RequestItemActivation()\r\n{new System.Diagnostics.StackTrace()}"; + }); + } + +} +*/ diff --git a/Multiplayer/Settings.cs b/Multiplayer/Settings.cs index c01fe67..703b619 100644 --- a/Multiplayer/Settings.cs +++ b/Multiplayer/Settings.cs @@ -1,5 +1,7 @@ -using System; +using System; using Humanizer; +using Multiplayer.Utils; +using Steamworks; using UnityEngine; using UnityModManagerNet; using Console = DV.Console; @@ -14,20 +16,46 @@ public class Settings : UnityModManager.ModSettings, IDrawable public static Action OnSettingsUpdated; + public int SettingsVer = 2; + [Header("Player")] - [Draw("Username", Tooltip = "Your username in-game")] + [Draw("Use Steam Name", Tooltip = "Use your Steam name as your username in-game")] + public bool UseSteamName = true; + public string LastSteamName = string.Empty; + public ulong SteamId = 0; + [Draw("Username", Tooltip = "Your username in-game", VisibleOn = "UseSteamName|false")] public string Username = "Player"; public string Guid = System.Guid.NewGuid().ToString(); [Space(10)] [Header("Server")] + [Draw("Server Name", Tooltip = "Name of your server in the lobby browser.")] + public string ServerName = ""; [Draw("Password", Tooltip = "The password required to join your server. Leave blank for no password.")] public string Password = ""; + [Draw("Public Game", Tooltip = "Public servers are listed in the lobby browser")] + public bool PublicGame = true; [Draw("Max Players", Tooltip = "The maximum number of players that can join your server, including yourself.")] public int MaxPlayers = 4; [Draw("Port", Tooltip = "The port that your server will listen on. You generally don't need to change this.")] public int Port = 7777; + [Draw("Details", Tooltip = "Details shown in the server browser")] + public string Details = ""; + [Space(10)] + [Header("Lobby Server")] + [Draw("Lobby Server address", Tooltip = "Address of lobby server for finding multiplayer games")] + public string LobbyServerAddress = "https://dv.mineit.space";//"http://localhost:8080"; + [Draw("IPv4 Check Address", Tooltip = "Do not modify unless the service is unavailable")] + public string Ipv4AddressCheck = "https://api.ipify.org/"; + [Header("Last Server Connected to by IP")] + [Draw("Last Remote IP", Tooltip = "The IP for the last server connected to by IP.")] + public string LastRemoteIP = ""; + [Draw("Last Remote Port", Tooltip = "The port for the last server connected to by IP.")] + public int LastRemotePort = 7777; + [Draw("Last Remote Password", Tooltip = "The password for the last server connected to by IP.")] + public string LastRemotePassword = ""; + [Space(10)] [Header("Preferences")] [Draw("Show Name Tags", Tooltip = "Whether to show player names above their heads.")] @@ -65,7 +93,7 @@ public class Settings : UnityModManager.ModSettings, IDrawable public int SimulationMinLatency = 30; [Draw("Maximum Latency (ms)", VisibleOn = "SimulateLatency|true")] public int SimulationMaxLatency = 100; - + public bool ForceJson = false; public void Draw(UnityModManager.ModEntry modEntry) { Settings self = this; @@ -76,6 +104,7 @@ public void Draw(UnityModManager.ModEntry modEntry) public override void Save(UnityModManager.ModEntry modEntry) { + LastSteamName = LastSteamName.Trim().Truncate(MAX_USERNAME_LENGTH); Username = Username.Trim().Truncate(MAX_USERNAME_LENGTH); Port = Mathf.Clamp(Port, 1024, 49151); MaxPlayers = Mathf.Clamp(MaxPlayers, 1, byte.MaxValue); @@ -98,4 +127,71 @@ public Guid GetGuid() Guid = guid.ToString(); return guid; } + + public string GetUserName() + { + string username = Username; + + if (Multiplayer.Settings.UseSteamName) + { + if (SteamworksUtils.GetSteamUser(out string steamUsername, out ulong steamId)) + { + Multiplayer.Settings.LastSteamName = steamUsername; + Multiplayer.Settings.SteamId = steamId; + } + + if (Multiplayer.Settings.LastSteamName != string.Empty) + username = Multiplayer.Settings.LastSteamName; + } + + return username; + } + + public static Settings Load(UnityModManager.ModEntry modEntry) + { + Settings data = Settings.Load(modEntry); + + MigrateSettings(ref data); + + data.SettingsVer = GetCurrentVersion(); + + data.Save(modEntry); + + return data; + } + + private static int GetCurrentVersion() + { + return 2; + } + + // Function to handle migrations based on the current version + private static void MigrateSettings(ref Settings data) + { + switch (data.SettingsVer) + { + case 0: + //We want to disable Punch until it's fully implemented + data.EnableNatPunch = false; + data.SettingsVer = 1; + + //Ensure http setting is upgraded to https if using the default lobby server + if(data.LobbyServerAddress == "http://dv.mineit.space") + data.LobbyServerAddress = new Settings().LobbyServerAddress; + + MigrateSettings(ref data); + break; + case 1: + if (data.Ipv4AddressCheck == "http://checkip.dyndns.org") + data.Ipv4AddressCheck = new Settings().Ipv4AddressCheck; + + data.ShowAdvancedSettings = true; + data.DebugLogging = true; + data.ShowPingInNameTags = true; + break; + default: + break; + } + + } } diff --git a/Multiplayer/Utils/Csv.cs b/Multiplayer/Utils/Csv.cs index 560fb24..13943da 100644 --- a/Multiplayer/Utils/Csv.cs +++ b/Multiplayer/Utils/Csv.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -5,124 +6,155 @@ using System.Linq; using System.Text; -namespace Multiplayer.Utils; - -public static class Csv +namespace Multiplayer.Utils { - /// - /// Parses a CSV string into a dictionary of columns, each of which is a dictionary of rows, keyed by the first column. - /// - public static ReadOnlyDictionary> Parse(string data) + public static class Csv { - string[] lines = data.Split('\n'); + /// + /// Parses a CSV string into a dictionary of columns, each of which is a dictionary of rows, keyed by the first column. + /// + public static ReadOnlyDictionary> Parse(string data) + { + // Split the input data into lines + string[] separators = new string[] { "\r\n", "\n" }; + string[] lines = data.Split(separators, StringSplitOptions.RemoveEmptyEntries); - // Dictionary> - OrderedDictionary columns = new(lines.Length - 1); + // Use an OrderedDictionary to preserve the insertion order of keys + var columns = new OrderedDictionary(); - List keys = ParseLine(lines[0]); - foreach (string key in keys) - columns.Add(key, new Dictionary()); + // Parse the header line to get the column keys + List keys = ParseLine(lines[0]); + foreach (string key in keys) + { + if (!string.IsNullOrWhiteSpace(key)) + columns.Add(key, new Dictionary()); + } - for (int i = 1; i < lines.Length; i++) - { - string line = lines[i]; - List values = ParseLine(line); - if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) - continue; - string key = values[0]; - for (int j = 0; j < values.Count; j++) - ((Dictionary)columns[j]).Add(key, values[j]); - } + // Iterate through the remaining lines (rows) + for (int i = 1; i < lines.Length; i++) + { + string line = lines[i]; + List values = ParseLine(line); - return new ReadOnlyDictionary>(columns.Cast() - .ToDictionary(entry => (string)entry.Key, entry => (Dictionary)entry.Value)); - } + if (values.Count == 0 || string.IsNullOrWhiteSpace(values[0])) + continue; - private static List ParseLine(string line) - { - bool inQuotes = false; - bool wasBackslash = false; - List values = new(); - StringBuilder builder = new(); + string rowKey = values[0]; + + //ensure we don't have too many columns + if (values.Count > columns.Count) + { + Multiplayer.LogWarning($"CSV Line {i + 1}: Found {values.Count} columns, expected {columns.Count}\r\n\t{line}"); + continue; + } - void FinishLine() - { - values.Add(builder.ToString()); - builder.Clear(); + // Add the row values to the appropriate column dictionaries + for (int j = 0; j < values.Count && j < keys.Count; j++) + { + string columnKey = keys[j]; + if (!string.IsNullOrWhiteSpace(columnKey)) + { + var columnDict = (Dictionary)columns[columnKey]; + columnDict[rowKey] = values[j]; + } + } + } + + // Convert the OrderedDictionary to a ReadOnlyDictionary + return new ReadOnlyDictionary>( + columns.Cast() + .ToDictionary(entry => (string)entry.Key, entry => (Dictionary)entry.Value) + ); } - foreach (char c in line) + private static List ParseLine(string line) { - if (c == '\n' || (!inQuotes && c == ',')) - { - FinishLine(); - continue; - } + bool inQuotes = false; + bool wasBackslash = false; + List values = new(); + StringBuilder builder = new(); - switch (c) + void FinishValue() { - case '\r': - Multiplayer.LogWarning("Encountered carriage return in CSV! Please use Unix-style line endings (LF)."); - continue; - case '"': - inQuotes = !inQuotes; - continue; - case '\\': - wasBackslash = true; - continue; + values.Add(builder.ToString()); + builder.Clear(); } - if (wasBackslash) + foreach (char c in line) { - wasBackslash = false; - if (c == 'n') + if (c == ',' && !inQuotes) { - builder.Append('\n'); + FinishValue(); continue; } - // Not a special character, so just append the backslash - builder.Append('\\'); - } + switch (c) + { + case '\r': + Multiplayer.LogWarning("Encountered carriage return in CSV! Please use Unix-style line endings (LF)."); + continue; + case '"': + inQuotes = !inQuotes; + continue; + case '\\': + wasBackslash = true; + continue; + } - builder.Append(c); - } + if (wasBackslash) + { + wasBackslash = false; + if (c == 'n') + { + builder.Append('\n'); + continue; + } + + // Not a special character, so just append the backslash + builder.Append('\\'); + } + + builder.Append(c); + } - if (builder.Length > 0) - FinishLine(); + if (builder.Length > 0) + FinishValue(); - return values; - } + return values; + } - public static string Dump(ReadOnlyDictionary> data) - { - StringBuilder result = new("\n"); + public static string Dump(ReadOnlyDictionary> data) + { + StringBuilder result = new("\n"); - foreach (KeyValuePair> column in data) - result.Append($"{column.Key},"); + foreach (KeyValuePair> column in data) + result.Append($"{column.Key},"); - result.Remove(result.Length - 1, 1); - result.Append('\n'); + result.Remove(result.Length - 1, 1); + result.Append('\n'); - int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; + int rowCount = data.Values.FirstOrDefault()?.Count ?? 0; - for (int i = 0; i < rowCount; i++) - { - foreach (KeyValuePair> column in data) - if (column.Value.Count > i) - { - string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); - result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); - } - else + for (int i = 0; i < rowCount; i++) + { + foreach (KeyValuePair> column in data) { - result.Append(','); + if (column.Value.Count > i) + { + string value = column.Value.ElementAt(i).Value.Replace("\n", "\\n"); + result.Append(value.Contains(',') ? $"\"{value}\"," : $"{value},"); + } + else + { + result.Append(','); + } } - result.Remove(result.Length - 1, 1); - result.Append('\n'); - } + result.Remove(result.Length - 1, 1); + result.Append('\n'); + } - return result.ToString(); + return result.ToString(); + } } } diff --git a/Multiplayer/Utils/DvExtensions.cs b/Multiplayer/Utils/DvExtensions.cs index 745ef94..e25ef67 100644 --- a/Multiplayer/Utils/DvExtensions.cs +++ b/Multiplayer/Utils/DvExtensions.cs @@ -1,6 +1,16 @@ using System; +using DV.UI; +using DV.UIFramework; +using DV.Localization; using Multiplayer.Components.Networking.Train; using Multiplayer.Components.Networking.World; +using UnityEngine; +using UnityEngine.UI; +using System.Diagnostics; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; + + namespace Multiplayer.Utils; @@ -10,16 +20,21 @@ public static class DvExtensions public static ushort GetNetId(this TrainCar car) { - ushort netId = car.Networked().NetId; + ushort netId = 0; + + if (car != null && car.TryNetworked(out NetworkedTrainCar networkedTrainCar)) + netId = networkedTrainCar.NetId; +/* if (netId == 0) - throw new InvalidOperationException($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!"); + Multiplayer.LogWarning($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!\r\n" + (Multiplayer.Settings.DebugLogging ? new System.Diagnostics.StackTrace() : ""));*/ + //throw new InvalidOperationException($"NetId for {car.carLivery.id} ({car.ID}) isn't initialized!"); return netId; } - public static NetworkedTrainCar Networked(this TrainCar trainCar) - { - return NetworkedTrainCar.GetFromTrainCar(trainCar); - } + //public static NetworkedTrainCar Networked(this TrainCar trainCar) + //{ + // return NetworkedTrainCar.GetFromTrainCar(trainCar); + //} public static bool TryNetworked(this TrainCar trainCar, out NetworkedTrainCar networkedTrainCar) { @@ -36,4 +51,104 @@ public static NetworkedRailTrack Networked(this RailTrack railTrack) } #endregion + + #region UI + public static GameObject UpdateButton(this GameObject pane, string oldButtonName, string newButtonName, string localeKey, string toolTipKey, Sprite icon) + { + // Find and rename the button + GameObject button = pane.FindChildByName(oldButtonName); + button.name = newButtonName; + + // Update localization and tooltip + if (button.GetComponentInChildren() != null) + { + button.GetComponentInChildren().key = localeKey; + foreach(var child in button.GetComponentsInChildren()) + { + GameObject.Destroy(child); + } + ResetTooltip(button); + button.GetComponentInChildren().UpdateLocalization(); + }else if(button.GetComponentInChildren() != null) + { + button.GetComponentInChildren().enabledKey = localeKey + "__tooltip"; + button.GetComponentInChildren().disabledKey = localeKey + "__tooltip_disabled"; + } + + // Set the button icon if provided + if (icon != null) + { + SetButtonIcon(button, icon); + } + + // Enable button interaction + button.GetComponentInChildren().ToggleInteractable(true); + + return button; + } + + private static void SetButtonIcon(this GameObject button, Sprite icon) + { + // Find and set the icon for the button + GameObject goIcon = button.FindChildByName("[icon]"); + if (goIcon == null) + { + Multiplayer.LogError("Failed to find icon!"); + return; + } + + goIcon.GetComponent().sprite = icon; + } + + public static void ResetTooltip(this GameObject button) + { + // Reset the tooltip keys for the button + UIElementTooltip tooltip = button.GetComponent(); + tooltip.disabledKey = null; + tooltip.enabledKey = null; + + } + + #endregion + + #region Utils + + public static float AnyPlayerSqrMag(this GameObject item) + { + return AnyPlayerSqrMag(item.transform.position); + } + + public static float AnyPlayerSqrMag(this Vector3 anchor) + { + float result = float.MaxValue; + //string origin = new StackTrace().GetFrame(1).GetMethod().Name; + + //Loop through all of the players and return the one thats closest to the anchor + foreach (ServerPlayer serverPlayer in NetworkLifecycle.Instance.Server.ServerPlayers) + { + float sqDist = (serverPlayer.WorldPosition - anchor).sqrMagnitude; + /* + if(origin == "UnusedTrainCarDeleter.AreDeleteConditionsFulfilled_Patch0") + Multiplayer.LogDebug(() => $"AnyPlayerSqrMag(): car: {UnusedTrainCarDeleterPatch.current?.ID}, player: {serverPlayer.Username}, result: {sqDist}"); + */ + if (sqDist < result) + result = sqDist; + } + + /* + if (origin == "UnusedTrainCarDeleter.AreDeleteConditionsFulfilled_Patch0") + Multiplayer.LogDebug(() => $"AnyPlayerSqrMag(): player: result: {result}"); + */ + return result; + } + + public static Vector3 GetWorldAbsolutePosition(this GameObject go) + { + return go.transform.GetWorldAbsolutePosition(); + } + public static Vector3 GetWorldAbsolutePosition(this Transform transform) + { + return transform.position - WorldMover.currentMove; + } + #endregion } diff --git a/Multiplayer/Utils/PacketCompression.cs b/Multiplayer/Utils/PacketCompression.cs new file mode 100644 index 0000000..39baeef --- /dev/null +++ b/Multiplayer/Utils/PacketCompression.cs @@ -0,0 +1,30 @@ +using UnityEngine; +using System.IO; +using System.IO.Compression; +using System.Text; + +public static class PacketCompression +{ + public static byte[] Compress(byte[] data) + { + using (var outputStream = new MemoryStream()) + { + using (var gzipStream = new GZipStream(outputStream, CompressionMode.Compress)) + { + gzipStream.Write(data, 0, data.Length); + } + return outputStream.ToArray(); + } + } + + public static byte[] Decompress(byte[] compressedData) + { + using (var inputStream = new MemoryStream(compressedData)) + using (var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress)) + using (var outputStream = new MemoryStream()) + { + gzipStream.CopyTo(outputStream); + return outputStream.ToArray(); + } + } +} diff --git a/Multiplayer/Utils/SteamWorksUtils.cs b/Multiplayer/Utils/SteamWorksUtils.cs new file mode 100644 index 0000000..f52ab61 --- /dev/null +++ b/Multiplayer/Utils/SteamWorksUtils.cs @@ -0,0 +1,167 @@ +using DV.Localization; +using DV.UIFramework; +using Multiplayer.Components.MainMenu; +using Multiplayer.Components.Networking; +using Multiplayer.Networking.Data; +using Multiplayer.Patches.MainMenu; +using Steamworks; +using Steamworks.Data; +using System; +using System.Linq; + +namespace Multiplayer.Utils; + +public static class SteamworksUtils +{ + public const string LOBBY_MP_MOD_KEY = "MP_MOD"; + public const string LOBBY_NET_LOCATION_KEY = "NetLocation"; + public const string LOBBY_HAS_PASSWORD = "HasPassword"; + + private static bool hasJoinedCL; + + public static bool GetSteamUser(out string username, out ulong steamId) + { + username = null; + steamId = 0; + + try + { + if (!DVSteamworks.Success) + return false; + + if (!SteamClient.IsValid || !SteamClient.SteamId.IsValid) + { + Multiplayer.Log($"Failed to get SteamID. Status: {SteamClient.IsValid}, {SteamClient.SteamId.IsValid}"); + return false; + } + + steamId = SteamClient.SteamId.Value; + username = SteamClient.Name; + + if (SteamApps.IsAppInstalled(DVSteamworks.APP_ID)) + Multiplayer.Log($"Found Steam Name: {username}, steamId {steamId}"); + } + catch(Exception ex) + { + Multiplayer.LogError($"Failed to obtain Steam user.\r\n{ex.StackTrace}"); + } + + return true; + } + + public static void SetLobbyData(Lobby lobby, LobbyServerData data, string[] exclude) + { + var properties = typeof(LobbyServerData).GetProperties().Where(p => !exclude.Contains(p.Name)); + foreach (var prop in properties) + { + var value = prop.GetValue(data)?.ToString() ?? ""; + lobby.SetData(prop.Name, value); + } + } + + public static LobbyServerData GetLobbyData(this Lobby lobby) + { + var data = new LobbyServerData(); + var properties = typeof(LobbyServerData).GetProperties(); + + foreach (var prop in properties) + { + var value = lobby.GetData(prop.Name); + if (string.IsNullOrEmpty(value)) continue; + + var converted = Convert.ChangeType(value, prop.PropertyType); + prop.SetValue(data, converted); + } + + return data; + } + + public static ulong GetLobbyIdFromArgs() + { + string[] args = Environment.GetCommandLineArgs(); + + for (int i = 0; i < args.Length - 1; i++) + if (args[i] == "+connect_lobby") + return ulong.Parse(args[i + 1]); + + return 0; + } + + public static void JoinFromCommandLine() + { + if (hasJoinedCL) + return; + hasJoinedCL = true; + + var id = GetLobbyIdFromArgs(); + var sId = new SteamId + { + Value = id + }; + + var lobby = new Lobby(sId); + lobby.Refresh(); + } + + private static bool CanHandleLobbyRequest() + { + return !NetworkLifecycle.Instance.IsServerRunning && + !NetworkLifecycle.Instance.IsClientRunning; + } + + public static void OnLobbyJoinRequest(Lobby lobby, SteamId id) + { + Multiplayer.Log($"Received lobby join request: {lobby.Id}, {id.Value}"); + + if (!CanHandleLobbyRequest()) + return; + + QueueLobbyInvite(lobby); + } + + public static void OnLobbyInviteRequest(Friend friend, Lobby lobby) + { + Multiplayer.Log($"Received lobby invite: {lobby.Id}"); + + if (!CanHandleLobbyRequest()) + return; + + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + var popup = MainMenuThingsAndStuff.Instance.ShowYesNoPopup(); + + if (popup == null) + return; + + popup.labelTMPro.text = $"{friend.Name} invited you to play!\r\nDo you wish to join?"; + + Localize locPos = popup.positiveButton.GetComponentInChildren(); + locPos.key = "yes"; + locPos.UpdateLocalization(); + + Localize locNeg = popup.negativeButton.GetComponentInChildren(); + locNeg.key = "no"; + locNeg.UpdateLocalization(); + + popup.Closed += (PopupResult result) => + { + if (result.closedBy == PopupClosedByAction.Positive) + QueueLobbyInvite(lobby); + }; + + }); + + NetworkLifecycle.Instance.TriggerMainMenuEventLater(); + } + + public static void QueueLobbyInvite(Lobby lobby) + { + NetworkLifecycle.Instance.QueueMainMenuEvent(() => + { + ServerBrowserPane.lobbyToJoin = lobby; + MainMenuThingsAndStuff.Instance.SwitchToMenu((byte)RightPaneController_Patch.joinMenuIndex); + }); + + NetworkLifecycle.Instance.TriggerMainMenuEventLater(); + } +} diff --git a/Multiplayer/Utils/UnityExtensions.cs b/Multiplayer/Utils/UnityExtensions.cs index ed75e18..c587612 100644 --- a/Multiplayer/Utils/UnityExtensions.cs +++ b/Multiplayer/Utils/UnityExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; @@ -105,4 +105,22 @@ public static T GetOrAddComponent(this Component component) where T : Compone { return component.gameObject.GetOrAddComponent(); } + + public static uint ColorToUInt32(this Color color) + { + uint r = (uint)(color.r * 255); + uint g = (uint)(color.g * 255); + uint b = (uint)(color.b * 255); + uint a = (uint)(color.a * 255); + return (a << 24) | (r << 16) | (g << 8) | b; + } + + public static Color UInt32ToColor(this uint packed) + { + float a = ((packed >> 24) & 0xFF) / 255f; + float r = ((packed >> 16) & 0xFF) / 255f; + float g = ((packed >> 8) & 0xFF) / 255f; + float b = (packed & 0xFF) / 255f; + return new Color(r, g, b, a); + } } diff --git a/MultiplayerAssets/Assets/AssetIndex.asset b/MultiplayerAssets/Assets/AssetIndex.asset index b1c4785..48bf760 100644 --- a/MultiplayerAssets/Assets/AssetIndex.asset +++ b/MultiplayerAssets/Assets/AssetIndex.asset @@ -15,3 +15,7 @@ MonoBehaviour: playerPrefab: {fileID: 1707366875631224182, guid: 720cc4622be79f701b73d41dbf0472ea, type: 3} multiplayerIcon: {fileID: 21300000, guid: 981b3e40e34126c43a32b7a54238d2d6, type: 3} + lockIcon: {fileID: 21300000, guid: b8a707a2b12db584fad32aed46912dd0, type: 3} + refreshIcon: {fileID: 21300000, guid: 7c3f2166549e6e144ae26c8d527d59b0, type: 3} + connectIcon: {fileID: 21300000, guid: dad0fda7f8df3cd41a278a839fe12d23, type: 3} + lanIcon: {fileID: 21300000, guid: 8386cff9a47c8a2409ad12ae6ae2233e, type: 3} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib.meta deleted file mode 100644 index 5a66746..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: cab0035d8a4c26975a2ef22c8eabcc3d -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt deleted file mode 100644 index 6e1e67b..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2020 Ruslan Pyrch - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt.meta deleted file mode 100644 index 7af1302..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LICENSE.txt.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 656a4f684a57cd9e0be9d5459a570f2e -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib.meta deleted file mode 100644 index eee7a72..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 7b9b511c4658e6bc7ae2a737cd046421 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs deleted file mode 100644 index b70c436..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using System.Threading; - -namespace LiteNetLib -{ - internal abstract class BaseChannel - { - protected readonly NetPeer Peer; - protected readonly Queue OutgoingQueue = new Queue(NetConstants.DefaultWindowSize); - private int _isAddedToPeerChannelSendQueue; - - public int PacketsInQueue => OutgoingQueue.Count; - - protected BaseChannel(NetPeer peer) - { - Peer = peer; - } - - public void AddToQueue(NetPacket packet) - { - lock (OutgoingQueue) - { - OutgoingQueue.Enqueue(packet); - } - AddToPeerChannelSendQueue(); - } - - protected void AddToPeerChannelSendQueue() - { - if (Interlocked.CompareExchange(ref _isAddedToPeerChannelSendQueue, 1, 0) == 0) - { - Peer.AddToReliableChannelSendQueue(this); - } - } - - public bool SendAndCheckQueue() - { - bool hasPacketsToSend = SendNextPackets(); - if (!hasPacketsToSend) - Interlocked.Exchange(ref _isAddedToPeerChannelSendQueue, 0); - - return hasPacketsToSend; - } - - protected abstract bool SendNextPackets(); - public abstract bool ProcessPacket(NetPacket packet); - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs.meta deleted file mode 100644 index 2f5c4fa..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/BaseChannel.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 22b92fe7d347801cda171a3652d91c34 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs deleted file mode 100644 index 4a2cdd9..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Net; -using System.Threading; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - internal enum ConnectionRequestResult - { - None, - Accept, - Reject, - RejectForce - } - - public class ConnectionRequest - { - private readonly NetManager _listener; - private int _used; - - public NetDataReader Data => InternalPacket.Data; - - internal ConnectionRequestResult Result { get; private set; } - internal NetConnectRequestPacket InternalPacket; - - public readonly IPEndPoint RemoteEndPoint; - - internal void UpdateRequest(NetConnectRequestPacket connectRequest) - { - //old request - if (connectRequest.ConnectionTime < InternalPacket.ConnectionTime) - return; - - if (connectRequest.ConnectionTime == InternalPacket.ConnectionTime && - connectRequest.ConnectionNumber == InternalPacket.ConnectionNumber) - return; - - InternalPacket = connectRequest; - } - - private bool TryActivate() - { - return Interlocked.CompareExchange(ref _used, 1, 0) == 0; - } - - internal ConnectionRequest(IPEndPoint remoteEndPoint, NetConnectRequestPacket requestPacket, NetManager listener) - { - InternalPacket = requestPacket; - RemoteEndPoint = remoteEndPoint; - _listener = listener; - } - - public NetPeer AcceptIfKey(string key) - { - if (!TryActivate()) - return null; - try - { - if (Data.GetString() == key) - Result = ConnectionRequestResult.Accept; - } - catch - { - NetDebug.WriteError("[AC] Invalid incoming data"); - } - if (Result == ConnectionRequestResult.Accept) - return _listener.OnConnectionSolved(this, null, 0, 0); - - Result = ConnectionRequestResult.Reject; - _listener.OnConnectionSolved(this, null, 0, 0); - return null; - } - - /// - /// Accept connection and get new NetPeer as result - /// - /// Connected NetPeer - public NetPeer Accept() - { - if (!TryActivate()) - return null; - Result = ConnectionRequestResult.Accept; - return _listener.OnConnectionSolved(this, null, 0, 0); - } - - public void Reject(byte[] rejectData, int start, int length, bool force) - { - if (!TryActivate()) - return; - Result = force ? ConnectionRequestResult.RejectForce : ConnectionRequestResult.Reject; - _listener.OnConnectionSolved(this, rejectData, start, length); - } - - public void Reject(byte[] rejectData, int start, int length) - { - Reject(rejectData, start, length, false); - } - - - public void RejectForce(byte[] rejectData, int start, int length) - { - Reject(rejectData, start, length, true); - } - - public void RejectForce() - { - Reject(null, 0, 0, true); - } - - public void RejectForce(byte[] rejectData) - { - Reject(rejectData, 0, rejectData.Length, true); - } - - public void RejectForce(NetDataWriter rejectData) - { - Reject(rejectData.Data, 0, rejectData.Length, true); - } - - public void Reject() - { - Reject(null, 0, 0, false); - } - - public void Reject(byte[] rejectData) - { - Reject(rejectData, 0, rejectData.Length, false); - } - - public void Reject(NetDataWriter rejectData) - { - Reject(rejectData.Data, 0, rejectData.Length, false); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs.meta deleted file mode 100644 index 811d830..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ConnectionRequest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4f53009a5751b2b24a12b6349d4bc0c0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs deleted file mode 100644 index 13d8852..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs +++ /dev/null @@ -1,272 +0,0 @@ -using System.Net; -using System.Net.Sockets; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - /// - /// Type of message that you receive in OnNetworkReceiveUnconnected event - /// - public enum UnconnectedMessageType - { - BasicMessage, - Broadcast - } - - /// - /// Disconnect reason that you receive in OnPeerDisconnected event - /// - public enum DisconnectReason - { - ConnectionFailed, - Timeout, - HostUnreachable, - NetworkUnreachable, - RemoteConnectionClose, - DisconnectPeerCalled, - ConnectionRejected, - InvalidProtocol, - UnknownHost, - Reconnect, - PeerToPeerConnection, - PeerNotFound - } - - /// - /// Additional information about disconnection - /// - public struct DisconnectInfo - { - /// - /// Additional info why peer disconnected - /// - public DisconnectReason Reason; - - /// - /// Error code (if reason is SocketSendError or SocketReceiveError) - /// - public SocketError SocketErrorCode; - - /// - /// Additional data that can be accessed (only if reason is RemoteConnectionClose) - /// - public NetPacketReader AdditionalData; - } - - public interface INetEventListener - { - /// - /// New remote peer connected to host, or client connected to remote host - /// - /// Connected peer object - void OnPeerConnected(NetPeer peer); - - /// - /// Peer disconnected - /// - /// disconnected peer - /// additional info about reason, errorCode or data received with disconnect message - void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo); - - /// - /// Network error (on send or receive) - /// - /// From endPoint (can be null) - /// Socket error - void OnNetworkError(IPEndPoint endPoint, SocketError socketError); - - /// - /// Received some data - /// - /// From peer - /// DataReader containing all received data - /// Number of channel at which packet arrived - /// Type of received packet - void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod); - - /// - /// Received unconnected message - /// - /// From address (IP and Port) - /// Message data - /// Message type (simple, discovery request or response) - void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType); - - /// - /// Latency information updated - /// - /// Peer with updated latency - /// latency value in milliseconds - void OnNetworkLatencyUpdate(NetPeer peer, int latency); - - /// - /// On peer connection requested - /// - /// Request information (EndPoint, internal id, additional data) - void OnConnectionRequest(ConnectionRequest request); - } - - public interface IDeliveryEventListener - { - /// - /// On reliable message delivered - /// - /// - /// - void OnMessageDelivered(NetPeer peer, object userData); - } - - public interface INtpEventListener - { - /// - /// Ntp response - /// - /// - void OnNtpResponse(NtpPacket packet); - } - - public interface IPeerAddressChangedListener - { - /// - /// Called when peer address changed (when AllowPeerAddressChange is enabled) - /// - /// Peer that changed address (with new address) - /// previous IP - void OnPeerAddressChanged(NetPeer peer, IPEndPoint previousAddress); - } - - public class EventBasedNetListener : INetEventListener, IDeliveryEventListener, INtpEventListener, IPeerAddressChangedListener - { - public delegate void OnPeerConnected(NetPeer peer); - public delegate void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo); - public delegate void OnNetworkError(IPEndPoint endPoint, SocketError socketError); - public delegate void OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod); - public delegate void OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType); - public delegate void OnNetworkLatencyUpdate(NetPeer peer, int latency); - public delegate void OnConnectionRequest(ConnectionRequest request); - public delegate void OnDeliveryEvent(NetPeer peer, object userData); - public delegate void OnNtpResponseEvent(NtpPacket packet); - public delegate void OnPeerAddressChangedEvent(NetPeer peer, IPEndPoint previousAddress); - - public event OnPeerConnected PeerConnectedEvent; - public event OnPeerDisconnected PeerDisconnectedEvent; - public event OnNetworkError NetworkErrorEvent; - public event OnNetworkReceive NetworkReceiveEvent; - public event OnNetworkReceiveUnconnected NetworkReceiveUnconnectedEvent; - public event OnNetworkLatencyUpdate NetworkLatencyUpdateEvent; - public event OnConnectionRequest ConnectionRequestEvent; - public event OnDeliveryEvent DeliveryEvent; - public event OnNtpResponseEvent NtpResponseEvent; - public event OnPeerAddressChangedEvent PeerAddressChangedEvent; - - public void ClearPeerConnectedEvent() - { - PeerConnectedEvent = null; - } - - public void ClearPeerDisconnectedEvent() - { - PeerDisconnectedEvent = null; - } - - public void ClearNetworkErrorEvent() - { - NetworkErrorEvent = null; - } - - public void ClearNetworkReceiveEvent() - { - NetworkReceiveEvent = null; - } - - public void ClearNetworkReceiveUnconnectedEvent() - { - NetworkReceiveUnconnectedEvent = null; - } - - public void ClearNetworkLatencyUpdateEvent() - { - NetworkLatencyUpdateEvent = null; - } - - public void ClearConnectionRequestEvent() - { - ConnectionRequestEvent = null; - } - - public void ClearDeliveryEvent() - { - DeliveryEvent = null; - } - - public void ClearNtpResponseEvent() - { - NtpResponseEvent = null; - } - - public void ClearPeerAddressChangedEvent() - { - PeerAddressChangedEvent = null; - } - - void INetEventListener.OnPeerConnected(NetPeer peer) - { - if (PeerConnectedEvent != null) - PeerConnectedEvent(peer); - } - - void INetEventListener.OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) - { - if (PeerDisconnectedEvent != null) - PeerDisconnectedEvent(peer, disconnectInfo); - } - - void INetEventListener.OnNetworkError(IPEndPoint endPoint, SocketError socketErrorCode) - { - if (NetworkErrorEvent != null) - NetworkErrorEvent(endPoint, socketErrorCode); - } - - void INetEventListener.OnNetworkReceive(NetPeer peer, NetPacketReader reader, byte channelNumber, DeliveryMethod deliveryMethod) - { - if (NetworkReceiveEvent != null) - NetworkReceiveEvent(peer, reader, channelNumber, deliveryMethod); - } - - void INetEventListener.OnNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader, UnconnectedMessageType messageType) - { - if (NetworkReceiveUnconnectedEvent != null) - NetworkReceiveUnconnectedEvent(remoteEndPoint, reader, messageType); - } - - void INetEventListener.OnNetworkLatencyUpdate(NetPeer peer, int latency) - { - if (NetworkLatencyUpdateEvent != null) - NetworkLatencyUpdateEvent(peer, latency); - } - - void INetEventListener.OnConnectionRequest(ConnectionRequest request) - { - if (ConnectionRequestEvent != null) - ConnectionRequestEvent(request); - } - - void IDeliveryEventListener.OnMessageDelivered(NetPeer peer, object userData) - { - if (DeliveryEvent != null) - DeliveryEvent(peer, userData); - } - - void INtpEventListener.OnNtpResponse(NtpPacket packet) - { - if (NtpResponseEvent != null) - NtpResponseEvent(packet); - } - - void IPeerAddressChangedListener.OnPeerAddressChanged(NetPeer peer, IPEndPoint previousAddress) - { - if (PeerAddressChangedEvent != null) - PeerAddressChangedEvent(peer, previousAddress); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs.meta deleted file mode 100644 index 926caff..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/INetEventListener.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 93f8b04a076a8f7daafd08dcfb01344a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs deleted file mode 100644 index 2eb09fe..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System; -using System.Net; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - internal sealed class NetConnectRequestPacket - { - public const int HeaderSize = 18; - public readonly long ConnectionTime; - public byte ConnectionNumber; - public readonly byte[] TargetAddress; - public readonly NetDataReader Data; - public readonly int PeerId; - - private NetConnectRequestPacket(long connectionTime, byte connectionNumber, int localId, byte[] targetAddress, NetDataReader data) - { - ConnectionTime = connectionTime; - ConnectionNumber = connectionNumber; - TargetAddress = targetAddress; - Data = data; - PeerId = localId; - } - - public static int GetProtocolId(NetPacket packet) - { - return BitConverter.ToInt32(packet.RawData, 1); - } - - public static NetConnectRequestPacket FromData(NetPacket packet) - { - if (packet.ConnectionNumber >= NetConstants.MaxConnectionNumber) - return null; - - //Getting connection time for peer - long connectionTime = BitConverter.ToInt64(packet.RawData, 5); - - //Get peer id - int peerId = BitConverter.ToInt32(packet.RawData, 13); - - //Get target address - int addrSize = packet.RawData[HeaderSize-1]; - if (addrSize != 16 && addrSize != 28) - return null; - byte[] addressBytes = new byte[addrSize]; - Buffer.BlockCopy(packet.RawData, HeaderSize, addressBytes, 0, addrSize); - - // Read data and create request - var reader = new NetDataReader(null, 0, 0); - if (packet.Size > HeaderSize+addrSize) - reader.SetSource(packet.RawData, HeaderSize + addrSize, packet.Size); - - return new NetConnectRequestPacket(connectionTime, packet.ConnectionNumber, peerId, addressBytes, reader); - } - - public static NetPacket Make(NetDataWriter connectData, SocketAddress addressBytes, long connectTime, int localId) - { - //Make initial packet - var packet = new NetPacket(PacketProperty.ConnectRequest, connectData.Length+addressBytes.Size); - - //Add data - FastBitConverter.GetBytes(packet.RawData, 1, NetConstants.ProtocolId); - FastBitConverter.GetBytes(packet.RawData, 5, connectTime); - FastBitConverter.GetBytes(packet.RawData, 13, localId); - packet.RawData[HeaderSize-1] = (byte)addressBytes.Size; - for (int i = 0; i < addressBytes.Size; i++) - packet.RawData[HeaderSize + i] = addressBytes[i]; - Buffer.BlockCopy(connectData.Data, 0, packet.RawData, HeaderSize + addressBytes.Size, connectData.Length); - return packet; - } - } - - internal sealed class NetConnectAcceptPacket - { - public const int Size = 15; - public readonly long ConnectionTime; - public readonly byte ConnectionNumber; - public readonly int PeerId; - public readonly bool PeerNetworkChanged; - - private NetConnectAcceptPacket(long connectionTime, byte connectionNumber, int peerId, bool peerNetworkChanged) - { - ConnectionTime = connectionTime; - ConnectionNumber = connectionNumber; - PeerId = peerId; - PeerNetworkChanged = peerNetworkChanged; - } - - public static NetConnectAcceptPacket FromData(NetPacket packet) - { - if (packet.Size != Size) - return null; - - long connectionId = BitConverter.ToInt64(packet.RawData, 1); - - //check connect num - byte connectionNumber = packet.RawData[9]; - if (connectionNumber >= NetConstants.MaxConnectionNumber) - return null; - - //check reused flag - byte isReused = packet.RawData[10]; - if (isReused > 1) - return null; - - //get remote peer id - int peerId = BitConverter.ToInt32(packet.RawData, 11); - if (peerId < 0) - return null; - - return new NetConnectAcceptPacket(connectionId, connectionNumber, peerId, isReused == 1); - } - - public static NetPacket Make(long connectTime, byte connectNum, int localPeerId) - { - var packet = new NetPacket(PacketProperty.ConnectAccept, 0); - FastBitConverter.GetBytes(packet.RawData, 1, connectTime); - packet.RawData[9] = connectNum; - FastBitConverter.GetBytes(packet.RawData, 11, localPeerId); - return packet; - } - - public static NetPacket MakeNetworkChanged(NetPeer peer) - { - var packet = new NetPacket(PacketProperty.PeerNotFound, Size-1); - FastBitConverter.GetBytes(packet.RawData, 1, peer.ConnectTime); - packet.RawData[9] = peer.ConnectionNum; - packet.RawData[10] = 1; - FastBitConverter.GetBytes(packet.RawData, 11, peer.RemoteId); - return packet; - } - } -} \ No newline at end of file diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs.meta deleted file mode 100644 index a3dee0c..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/InternalPackets.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 94139cc5687d01e41ac26a582821cd93 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers.meta deleted file mode 100644 index 98e28a7..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 98f2a8c29716ee4e1ac9f90523cde098 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs deleted file mode 100644 index 3ee97d6..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs +++ /dev/null @@ -1,41 +0,0 @@ -using LiteNetLib.Utils; -using System; -using System.Net; - -namespace LiteNetLib.Layers -{ - public sealed class Crc32cLayer : PacketLayerBase - { - public Crc32cLayer() : base(CRC32C.ChecksumSize) - { - - } - - public override void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) - { - if (length < NetConstants.HeaderSize + CRC32C.ChecksumSize) - { - NetDebug.WriteError("[NM] DataReceived size: bad!"); - //Set length to 0 to have netManager drop the packet. - length = 0; - return; - } - - int checksumPoint = length - CRC32C.ChecksumSize; - if (CRC32C.Compute(data, offset, checksumPoint) != BitConverter.ToUInt32(data, checksumPoint)) - { - NetDebug.Write("[NM] DataReceived checksum: bad!"); - //Set length to 0 to have netManager drop the packet. - length = 0; - return; - } - length -= CRC32C.ChecksumSize; - } - - public override void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) - { - FastBitConverter.GetBytes(data, length, CRC32C.Compute(data, offset, length)); - length += CRC32C.ChecksumSize; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs.meta deleted file mode 100644 index 55a2fe8..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/Crc32cLayer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: dfc5584fdbe07f366bce12fcc6651303 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs deleted file mode 100644 index b3d9b3a..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Net; - -namespace LiteNetLib.Layers -{ - public abstract class PacketLayerBase - { - public readonly int ExtraPacketSizeForLayer; - - protected PacketLayerBase(int extraPacketSizeForLayer) - { - ExtraPacketSizeForLayer = extraPacketSizeForLayer; - } - - public abstract void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length); - public abstract void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length); - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs.meta deleted file mode 100644 index 5bfbd7d..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/PacketLayerBase.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 3268d8ee9aff4d539b3e255f9494a6f4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs deleted file mode 100644 index 9b67196..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Net; -using System.Text; - -namespace LiteNetLib.Layers -{ - public class XorEncryptLayer : PacketLayerBase - { - private byte[] _byteKey; - - public XorEncryptLayer() : base(0) - { - - } - - public XorEncryptLayer(byte[] key) : this() - { - SetKey(key); - } - - public XorEncryptLayer(string key) : this() - { - SetKey(key); - } - - public void SetKey(string key) - { - _byteKey = Encoding.UTF8.GetBytes(key); - } - - public void SetKey(byte[] key) - { - if (_byteKey == null || _byteKey.Length != key.Length) - _byteKey = new byte[key.Length]; - Buffer.BlockCopy(key, 0, _byteKey, 0, key.Length); - } - - public override void ProcessInboundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) - { - if (_byteKey == null) - return; - var cur = offset; - for (var i = 0; i < length; i++, cur++) - { - data[cur] = (byte)(data[cur] ^ _byteKey[i % _byteKey.Length]); - } - } - - public override void ProcessOutBoundPacket(ref IPEndPoint endPoint, ref byte[] data, ref int offset, ref int length) - { - if (_byteKey == null) - return; - var cur = offset; - for (var i = 0; i < length; i++, cur++) - { - data[cur] = (byte)(data[cur] ^ _byteKey[i % _byteKey.Length]); - } - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs.meta deleted file mode 100644 index db23074..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Layers/XorEncryptLayer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 912645dcda5efef528a82cd323caff02 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef deleted file mode 100644 index 530c72e..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "LiteNetLib", - "references": [], - "includePlatforms": [], - "excludePlatforms": [], - "allowUnsafeCode": true, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": true, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false -} \ No newline at end of file diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef.meta deleted file mode 100644 index bcaa3e6..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: e9645de4460073e6ab30a874509a9ca9 -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj deleted file mode 100644 index 4a58e9e..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj +++ /dev/null @@ -1,62 +0,0 @@ - - - - LiteNetLib - LiteNetLib - net6.0;net5.0;netcoreapp3.1;netstandard2.0;netstandard2.1 - net471;net6.0;net5.0;netstandard2.0;netstandard2.1;netcoreapp3.1 - true - Library - 7.3 - - true - 1701;1702;1705;1591 - 1.1.0 - Lite reliable UDP library for Mono and .NET - - - - TRACE;DEBUG - - - - TRACE - - - - true - $(DefineConstants);LITENETLIB_UNSAFE - udp reliable-udp network - https://github.com/RevenantX/LiteNetLib/releases/tag/v1.1.0 - git - https://github.com/RevenantX/LiteNetLib - https://github.com/RevenantX/LiteNetLib - MIT - True - 1.1.0 - Ruslan Pyrch - Copyright 2023 Ruslan Pyrch - Lite reliable UDP library for .NET, Mono, and .NET Core - LNL.png - README.md - - - - - - - - - - - - True - \ - - - True - \ - - - - \ No newline at end of file diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj.meta deleted file mode 100644 index 3084696..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/LiteNetLib.csproj.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 47decce79e6fe28a585acafb4b2baf86 -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs deleted file mode 100644 index 0032f3c..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System.Collections.Concurrent; -using System.Net; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - public enum NatAddressType - { - Internal, - External - } - - public interface INatPunchListener - { - void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token); - void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token); - } - - public class EventBasedNatPunchListener : INatPunchListener - { - public delegate void OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token); - public delegate void OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token); - - public event OnNatIntroductionRequest NatIntroductionRequest; - public event OnNatIntroductionSuccess NatIntroductionSuccess; - - void INatPunchListener.OnNatIntroductionRequest(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, string token) - { - if(NatIntroductionRequest != null) - NatIntroductionRequest(localEndPoint, remoteEndPoint, token); - } - - void INatPunchListener.OnNatIntroductionSuccess(IPEndPoint targetEndPoint, NatAddressType type, string token) - { - if (NatIntroductionSuccess != null) - NatIntroductionSuccess(targetEndPoint, type, token); - } - } - - /// - /// Module for UDP NAT Hole punching operations. Can be accessed from NetManager - /// - public sealed class NatPunchModule - { - struct RequestEventData - { - public IPEndPoint LocalEndPoint; - public IPEndPoint RemoteEndPoint; - public string Token; - } - - struct SuccessEventData - { - public IPEndPoint TargetEndPoint; - public NatAddressType Type; - public string Token; - } - - class NatIntroduceRequestPacket - { - public IPEndPoint Internal { [Preserve] get; [Preserve] set; } - public string Token { [Preserve] get; [Preserve] set; } - } - - class NatIntroduceResponsePacket - { - public IPEndPoint Internal { [Preserve] get; [Preserve] set; } - public IPEndPoint External { [Preserve] get; [Preserve] set; } - public string Token { [Preserve] get; [Preserve] set; } - } - - class NatPunchPacket - { - public string Token { [Preserve] get; [Preserve] set; } - public bool IsExternal { [Preserve] get; [Preserve] set; } - } - - private readonly NetManager _socket; - private readonly ConcurrentQueue _requestEvents = new ConcurrentQueue(); - private readonly ConcurrentQueue _successEvents = new ConcurrentQueue(); - private readonly NetDataReader _cacheReader = new NetDataReader(); - private readonly NetDataWriter _cacheWriter = new NetDataWriter(); - private readonly NetPacketProcessor _netPacketProcessor; - private INatPunchListener _natPunchListener; - public const int MaxTokenLength = 256; - - /// - /// Events automatically will be called without PollEvents method from another thread - /// - public bool UnsyncedEvents = false; - - internal NatPunchModule(NetManager socket) - { - _socket = socket; - _netPacketProcessor = new NetPacketProcessor(_socket, MaxTokenLength); - _netPacketProcessor.SubscribeReusable(OnNatIntroductionResponse); - _netPacketProcessor.SubscribeReusable(OnNatIntroductionRequest); - _netPacketProcessor.SubscribeReusable(OnNatPunch); - } - - internal void ProcessMessage(IPEndPoint senderEndPoint, NetPacket packet) - { - lock (_cacheReader) - { - _cacheReader.SetSource(packet.RawData, NetConstants.HeaderSize, packet.Size); - _netPacketProcessor.ReadAllPackets(_cacheReader, senderEndPoint); - } - } - - public void Init(INatPunchListener listener) - { - _natPunchListener = listener; - } - - private void Send(T packet, IPEndPoint target) where T : class, new() - { - _cacheWriter.Reset(); - _cacheWriter.Put((byte)PacketProperty.NatMessage); - _netPacketProcessor.Write(_cacheWriter, packet); - _socket.SendRaw(_cacheWriter.Data, 0, _cacheWriter.Length, target); - } - - public void NatIntroduce( - IPEndPoint hostInternal, - IPEndPoint hostExternal, - IPEndPoint clientInternal, - IPEndPoint clientExternal, - string additionalInfo) - { - var req = new NatIntroduceResponsePacket - { - Token = additionalInfo - }; - - //First packet (server) send to client - req.Internal = hostInternal; - req.External = hostExternal; - Send(req, clientExternal); - - //Second packet (client) send to server - req.Internal = clientInternal; - req.External = clientExternal; - Send(req, hostExternal); - } - - public void PollEvents() - { - if (UnsyncedEvents) - return; - - if (_natPunchListener == null || (_successEvents.IsEmpty && _requestEvents.IsEmpty)) - return; - - while (_successEvents.TryDequeue(out var evt)) - { - _natPunchListener.OnNatIntroductionSuccess( - evt.TargetEndPoint, - evt.Type, - evt.Token); - } - - while (_requestEvents.TryDequeue(out var evt)) - { - _natPunchListener.OnNatIntroductionRequest(evt.LocalEndPoint, evt.RemoteEndPoint, evt.Token); - } - } - - public void SendNatIntroduceRequest(string host, int port, string additionalInfo) - { - SendNatIntroduceRequest(NetUtils.MakeEndPoint(host, port), additionalInfo); - } - - public void SendNatIntroduceRequest(IPEndPoint masterServerEndPoint, string additionalInfo) - { - //prepare outgoing data - string networkIp = NetUtils.GetLocalIp(LocalAddrType.IPv4); - if (string.IsNullOrEmpty(networkIp)) - { - networkIp = NetUtils.GetLocalIp(LocalAddrType.IPv6); - } - - Send( - new NatIntroduceRequestPacket - { - Internal = NetUtils.MakeEndPoint(networkIp, _socket.LocalPort), - Token = additionalInfo - }, - masterServerEndPoint); - } - - //We got request and must introduce - private void OnNatIntroductionRequest(NatIntroduceRequestPacket req, IPEndPoint senderEndPoint) - { - if (UnsyncedEvents) - { - _natPunchListener.OnNatIntroductionRequest( - req.Internal, - senderEndPoint, - req.Token); - } - else - { - _requestEvents.Enqueue(new RequestEventData - { - LocalEndPoint = req.Internal, - RemoteEndPoint = senderEndPoint, - Token = req.Token - }); - } - } - - //We got introduce and must punch - private void OnNatIntroductionResponse(NatIntroduceResponsePacket req) - { - NetDebug.Write(NetLogLevel.Trace, "[NAT] introduction received"); - - // send internal punch - var punchPacket = new NatPunchPacket {Token = req.Token}; - Send(punchPacket, req.Internal); - NetDebug.Write(NetLogLevel.Trace, $"[NAT] internal punch sent to {req.Internal}"); - - // hack for some routers - _socket.Ttl = 2; - _socket.SendRaw(new[] { (byte)PacketProperty.Empty }, 0, 1, req.External); - - // send external punch - _socket.Ttl = NetConstants.SocketTTL; - punchPacket.IsExternal = true; - Send(punchPacket, req.External); - NetDebug.Write(NetLogLevel.Trace, $"[NAT] external punch sent to {req.External}"); - } - - //We got punch and can connect - private void OnNatPunch(NatPunchPacket req, IPEndPoint senderEndPoint) - { - //Read info - NetDebug.Write(NetLogLevel.Trace, $"[NAT] punch received from {senderEndPoint} - additional info: {req.Token}"); - - //Release punch success to client; enabling him to Connect() to Sender if token is ok - if(UnsyncedEvents) - { - _natPunchListener.OnNatIntroductionSuccess( - senderEndPoint, - req.IsExternal ? NatAddressType.External : NatAddressType.Internal, - req.Token - ); - } - else - { - _successEvents.Enqueue(new SuccessEventData - { - TargetEndPoint = senderEndPoint, - Type = req.IsExternal ? NatAddressType.External : NatAddressType.Internal, - Token = req.Token - }); - } - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs.meta deleted file mode 100644 index fa73743..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NatPunchModule.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e21233a9193cb672790e5a08e578e090 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs deleted file mode 100644 index fc846c8..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs +++ /dev/null @@ -1,301 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace LiteNetLib -{ - internal readonly struct NativeAddr : IEquatable - { - //common parts - private readonly long _part1; //family, port, etc - private readonly long _part2; - //ipv6 parts - private readonly long _part3; - private readonly int _part4; - - private readonly int _hash; - - public NativeAddr(byte[] address, int len) - { - _part1 = BitConverter.ToInt64(address, 0); - _part2 = BitConverter.ToInt64(address, 8); - if (len > 16) - { - _part3 = BitConverter.ToInt64(address, 16); - _part4 = BitConverter.ToInt32(address, 24); - } - else - { - _part3 = 0; - _part4 = 0; - } - _hash = (int)(_part1 >> 32) ^ (int)_part1 ^ - (int)(_part2 >> 32) ^ (int)_part2 ^ - (int)(_part3 >> 32) ^ (int)_part3 ^ - _part4; - } - - public override int GetHashCode() - { - return _hash; - } - - public bool Equals(NativeAddr other) - { - return _part1 == other._part1 && - _part2 == other._part2 && - _part3 == other._part3 && - _part4 == other._part4; - } - - public override bool Equals(object obj) - { - return obj is NativeAddr other && Equals(other); - } - - public static bool operator ==(NativeAddr left, NativeAddr right) - { - return left.Equals(right); - } - - public static bool operator !=(NativeAddr left, NativeAddr right) - { - return !left.Equals(right); - } - } - - internal class NativeEndPoint : IPEndPoint - { - public readonly byte[] NativeAddress; - - public NativeEndPoint(byte[] address) : base(IPAddress.Any, 0) - { - NativeAddress = new byte[address.Length]; - Buffer.BlockCopy(address, 0, NativeAddress, 0, address.Length); - - short family = (short)((address[1] << 8) | address[0]); - Port =(ushort)((address[2] << 8) | address[3]); - - if ((NativeSocket.UnixMode && family == NativeSocket.AF_INET6) || (!NativeSocket.UnixMode && (AddressFamily)family == AddressFamily.InterNetworkV6)) - { - uint scope = unchecked((uint)( - (address[27] << 24) + - (address[26] << 16) + - (address[25] << 8) + - (address[24]))); -#if NETCOREAPP || NETSTANDARD2_1 || NETSTANDARD2_1_OR_GREATER - Address = new IPAddress(new ReadOnlySpan(address, 8, 16), scope); -#else - byte[] addrBuffer = new byte[16]; - Buffer.BlockCopy(address, 8, addrBuffer, 0, 16); - Address = new IPAddress(addrBuffer, scope); -#endif - } - else //IPv4 - { - long ipv4Addr = unchecked((uint)((address[4] & 0x000000FF) | - (address[5] << 8 & 0x0000FF00) | - (address[6] << 16 & 0x00FF0000) | - (address[7] << 24))); - Address = new IPAddress(ipv4Addr); - } - } - } - - internal static class NativeSocket - { - static -#if LITENETLIB_UNSAFE - unsafe -#endif - class WinSock - { - private const string LibName = "ws2_32.dll"; - - [DllImport(LibName, SetLastError = true)] - public static extern int recvfrom( - IntPtr socketHandle, - [In, Out] byte[] pinnedBuffer, - [In] int len, - [In] SocketFlags socketFlags, - [Out] byte[] socketAddress, - [In, Out] ref int socketAddressSize); - - [DllImport(LibName, SetLastError = true)] - internal static extern int sendto( - IntPtr socketHandle, -#if LITENETLIB_UNSAFE - byte* pinnedBuffer, -#else - [In] byte[] pinnedBuffer, -#endif - [In] int len, - [In] SocketFlags socketFlags, - [In] byte[] socketAddress, - [In] int socketAddressSize); - } - - static -#if LITENETLIB_UNSAFE - unsafe -#endif - class UnixSock - { - private const string LibName = "libc"; - - [DllImport(LibName, SetLastError = true)] - public static extern int recvfrom( - IntPtr socketHandle, - [In, Out] byte[] pinnedBuffer, - [In] int len, - [In] SocketFlags socketFlags, - [Out] byte[] socketAddress, - [In, Out] ref int socketAddressSize); - - [DllImport(LibName, SetLastError = true)] - internal static extern int sendto( - IntPtr socketHandle, -#if LITENETLIB_UNSAFE - byte* pinnedBuffer, -#else - [In] byte[] pinnedBuffer, -#endif - [In] int len, - [In] SocketFlags socketFlags, - [In] byte[] socketAddress, - [In] int socketAddressSize); - } - - public static readonly bool IsSupported = false; - public static readonly bool UnixMode = false; - - public const int IPv4AddrSize = 16; - public const int IPv6AddrSize = 28; - public const int AF_INET = 2; - public const int AF_INET6 = 10; - - private static readonly Dictionary NativeErrorToSocketError = new Dictionary - { - { 13, SocketError.AccessDenied }, //EACCES - { 98, SocketError.AddressAlreadyInUse }, //EADDRINUSE - { 99, SocketError.AddressNotAvailable }, //EADDRNOTAVAIL - { 97, SocketError.AddressFamilyNotSupported }, //EAFNOSUPPORT - { 11, SocketError.WouldBlock }, //EAGAIN - { 114, SocketError.AlreadyInProgress }, //EALREADY - { 9, SocketError.OperationAborted }, //EBADF - { 125, SocketError.OperationAborted }, //ECANCELED - { 103, SocketError.ConnectionAborted }, //ECONNABORTED - { 111, SocketError.ConnectionRefused }, //ECONNREFUSED - { 104, SocketError.ConnectionReset }, //ECONNRESET - { 89, SocketError.DestinationAddressRequired }, //EDESTADDRREQ - { 14, SocketError.Fault }, //EFAULT - { 112, SocketError.HostDown }, //EHOSTDOWN - { 6, SocketError.HostNotFound }, //ENXIO - { 113, SocketError.HostUnreachable }, //EHOSTUNREACH - { 115, SocketError.InProgress }, //EINPROGRESS - { 4, SocketError.Interrupted }, //EINTR - { 22, SocketError.InvalidArgument }, //EINVAL - { 106, SocketError.IsConnected }, //EISCONN - { 24, SocketError.TooManyOpenSockets }, //EMFILE - { 90, SocketError.MessageSize }, //EMSGSIZE - { 100, SocketError.NetworkDown }, //ENETDOWN - { 102, SocketError.NetworkReset }, //ENETRESET - { 101, SocketError.NetworkUnreachable }, //ENETUNREACH - { 23, SocketError.TooManyOpenSockets }, //ENFILE - { 105, SocketError.NoBufferSpaceAvailable }, //ENOBUFS - { 61, SocketError.NoData }, //ENODATA - { 2, SocketError.AddressNotAvailable }, //ENOENT - { 92, SocketError.ProtocolOption }, //ENOPROTOOPT - { 107, SocketError.NotConnected }, //ENOTCONN - { 88, SocketError.NotSocket }, //ENOTSOCK - { 3440, SocketError.OperationNotSupported }, //ENOTSUP - { 1, SocketError.AccessDenied }, //EPERM - { 32, SocketError.Shutdown }, //EPIPE - { 96, SocketError.ProtocolFamilyNotSupported }, //EPFNOSUPPORT - { 93, SocketError.ProtocolNotSupported }, //EPROTONOSUPPORT - { 91, SocketError.ProtocolType }, //EPROTOTYPE - { 94, SocketError.SocketNotSupported }, //ESOCKTNOSUPPORT - { 108, SocketError.Disconnecting }, //ESHUTDOWN - { 110, SocketError.TimedOut }, //ETIMEDOUT - { 0, SocketError.Success } - }; - - static NativeSocket() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - IsSupported = true; - UnixMode = true; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - IsSupported = true; - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int RecvFrom( - IntPtr socketHandle, - byte[] pinnedBuffer, - int len, - byte[] socketAddress, - ref int socketAddressSize) - { - return UnixMode - ? UnixSock.recvfrom(socketHandle, pinnedBuffer, len, 0, socketAddress, ref socketAddressSize) - : WinSock.recvfrom(socketHandle, pinnedBuffer, len, 0, socketAddress, ref socketAddressSize); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public -#if LITENETLIB_UNSAFE - unsafe -#endif - static int SendTo( - IntPtr socketHandle, -#if LITENETLIB_UNSAFE - byte* pinnedBuffer, -#else - byte[] pinnedBuffer, -#endif - int len, - byte[] socketAddress, - int socketAddressSize) - { - return UnixMode - ? UnixSock.sendto(socketHandle, pinnedBuffer, len, 0, socketAddress, socketAddressSize) - : WinSock.sendto(socketHandle, pinnedBuffer, len, 0, socketAddress, socketAddressSize); - } - - public static SocketError GetSocketError() - { - int error = Marshal.GetLastWin32Error(); - if (UnixMode) - return NativeErrorToSocketError.TryGetValue(error, out var err) - ? err - : SocketError.SocketError; - return (SocketError)error; - } - - public static SocketException GetSocketException() - { - int error = Marshal.GetLastWin32Error(); - if (UnixMode) - return NativeErrorToSocketError.TryGetValue(error, out var err) - ? new SocketException((int)err) - : new SocketException((int)SocketError.SocketError); - return new SocketException(error); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static short GetNativeAddressFamily(IPEndPoint remoteEndPoint) - { - return UnixMode - ? (short)(remoteEndPoint.AddressFamily == AddressFamily.InterNetwork ? AF_INET : AF_INET6) - : (short)remoteEndPoint.AddressFamily; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs.meta deleted file mode 100644 index 586308e..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NativeSocket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 29554417dec720ea5a66a5478451889c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs deleted file mode 100644 index ca7dfbc..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace LiteNetLib -{ - /// - /// Sending method type - /// - public enum DeliveryMethod : byte - { - /// - /// Unreliable. Packets can be dropped, can be duplicated, can arrive without order. - /// - Unreliable = 4, - - /// - /// Reliable. Packets won't be dropped, won't be duplicated, can arrive without order. - /// - ReliableUnordered = 0, - - /// - /// Unreliable. Packets can be dropped, won't be duplicated, will arrive in order. - /// - Sequenced = 1, - - /// - /// Reliable and ordered. Packets won't be dropped, won't be duplicated, will arrive in order. - /// - ReliableOrdered = 2, - - /// - /// Reliable only last packet. Packets can be dropped (except the last one), won't be duplicated, will arrive in order. - /// Cannot be fragmented - /// - ReliableSequenced = 3 - } - - /// - /// Network constants. Can be tuned from sources for your purposes. - /// - public static class NetConstants - { - //can be tuned - public const int DefaultWindowSize = 64; - public const int SocketBufferSize = 1024 * 1024; //1mb - public const int SocketTTL = 255; - - public const int HeaderSize = 1; - public const int ChanneledHeaderSize = 4; - public const int FragmentHeaderSize = 6; - public const int FragmentedHeaderTotalSize = ChanneledHeaderSize + FragmentHeaderSize; - public const ushort MaxSequence = 32768; - public const ushort HalfMaxSequence = MaxSequence / 2; - - //protocol - internal const int ProtocolId = 13; - internal const int MaxUdpHeaderSize = 68; - internal const int ChannelTypeCount = 4; - - internal static readonly int[] PossibleMtu = - { - 576 - MaxUdpHeaderSize, //minimal (RFC 1191) - 1024, //most games standard - 1232 - MaxUdpHeaderSize, - 1460 - MaxUdpHeaderSize, //google cloud - 1472 - MaxUdpHeaderSize, //VPN - 1492 - MaxUdpHeaderSize, //Ethernet with LLC and SNAP, PPPoE (RFC 1042) - 1500 - MaxUdpHeaderSize //Ethernet II (RFC 1191) - }; - - //Max possible single packet size - public static readonly int MaxPacketSize = PossibleMtu[PossibleMtu.Length - 1]; - public static readonly int MaxUnreliableDataSize = MaxPacketSize - HeaderSize; - - //peer specific - public const byte MaxConnectionNumber = 4; - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs.meta deleted file mode 100644 index 26bc130..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetConstants.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 00de73a0c90d93374b1213cdb597871a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs deleted file mode 100644 index 44cb6f3..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Diagnostics; - -namespace LiteNetLib -{ - public class InvalidPacketException : ArgumentException - { - public InvalidPacketException(string message) : base(message) - { - } - } - - public class TooBigPacketException : InvalidPacketException - { - public TooBigPacketException(string message) : base(message) - { - } - } - - public enum NetLogLevel - { - Warning, - Error, - Trace, - Info - } - - /// - /// Interface to implement for your own logger - /// - public interface INetLogger - { - void WriteNet(NetLogLevel level, string str, params object[] args); - } - - /// - /// Static class for defining your own LiteNetLib logger instead of Console.WriteLine - /// or Debug.Log if compiled with UNITY flag - /// - public static class NetDebug - { - public static INetLogger Logger = null; - private static readonly object DebugLogLock = new object(); - private static void WriteLogic(NetLogLevel logLevel, string str, params object[] args) - { - lock (DebugLogLock) - { - if (Logger == null) - { -#if UNITY_5_3_OR_NEWER - UnityEngine.Debug.Log(string.Format(str, args)); -#else - Console.WriteLine(str, args); -#endif - } - else - { - Logger.WriteNet(logLevel, str, args); - } - } - } - - [Conditional("DEBUG_MESSAGES")] - internal static void Write(string str) - { - WriteLogic(NetLogLevel.Trace, str); - } - - [Conditional("DEBUG_MESSAGES")] - internal static void Write(NetLogLevel level, string str) - { - WriteLogic(level, str); - } - - [Conditional("DEBUG_MESSAGES"), Conditional("DEBUG")] - internal static void WriteForce(string str) - { - WriteLogic(NetLogLevel.Trace, str); - } - - [Conditional("DEBUG_MESSAGES"), Conditional("DEBUG")] - internal static void WriteForce(NetLogLevel level, string str) - { - WriteLogic(level, str); - } - - internal static void WriteError(string str) - { - WriteLogic(NetLogLevel.Error, str); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs.meta deleted file mode 100644 index e74c9ba..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetDebug.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 99fd29417a05299a0aaf9fcf586f92e9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs deleted file mode 100644 index 0831220..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; - -namespace LiteNetLib -{ - public partial class NetManager - { - private NetPacket _poolHead; - private int _poolCount; - private readonly object _poolLock = new object(); - - /// - /// Maximum packet pool size (increase if you have tons of packets sending) - /// - public int PacketPoolSize = 1000; - - public int PoolCount => _poolCount; - - private NetPacket PoolGetWithData(PacketProperty property, byte[] data, int start, int length) - { - int headerSize = NetPacket.GetHeaderSize(property); - NetPacket packet = PoolGetPacket(length + headerSize); - packet.Property = property; - Buffer.BlockCopy(data, start, packet.RawData, headerSize, length); - return packet; - } - - //Get packet with size - private NetPacket PoolGetWithProperty(PacketProperty property, int size) - { - NetPacket packet = PoolGetPacket(size + NetPacket.GetHeaderSize(property)); - packet.Property = property; - return packet; - } - - private NetPacket PoolGetWithProperty(PacketProperty property) - { - NetPacket packet = PoolGetPacket(NetPacket.GetHeaderSize(property)); - packet.Property = property; - return packet; - } - - internal NetPacket PoolGetPacket(int size) - { - if (size > NetConstants.MaxPacketSize) - return new NetPacket(size); - - NetPacket packet; - lock (_poolLock) - { - packet = _poolHead; - if (packet == null) - return new NetPacket(size); - - _poolHead = _poolHead.Next; - _poolCount--; - } - - packet.Size = size; - if (packet.RawData.Length < size) - packet.RawData = new byte[size]; - return packet; - } - - internal void PoolRecycle(NetPacket packet) - { - if (packet.RawData.Length > NetConstants.MaxPacketSize || _poolCount >= PacketPoolSize) - { - //Don't pool big packets. Save memory - return; - } - - //Clean fragmented flag - packet.RawData[0] = 0; - lock (_poolLock) - { - packet.Next = _poolHead; - _poolHead = packet; - _poolCount++; - } - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs.meta deleted file mode 100644 index d5998d1..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.PacketPool.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e26aba5fa2d7e2d91b9dce82e304fde2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs deleted file mode 100644 index aabeaa3..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs +++ /dev/null @@ -1,732 +0,0 @@ -using System.Runtime.InteropServices; -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - public partial class NetManager - { - private const int ReceivePollingTime = 500000; //0.5 second - - private Socket _udpSocketv4; - private Socket _udpSocketv6; - private Thread _receiveThread; - private IPEndPoint _bufferEndPointv4; - private IPEndPoint _bufferEndPointv6; -#if UNITY_2018_3_OR_NEWER - private PausedSocketFix _pausedSocketFix; -#endif - -#if !LITENETLIB_UNSAFE - [ThreadStatic] private static byte[] _sendToBuffer; -#endif - [ThreadStatic] private static byte[] _endPointBuffer; - - private readonly Dictionary _nativeAddrMap = new Dictionary(); - - private const int SioUdpConnreset = -1744830452; //SIO_UDP_CONNRESET = IOC_IN | IOC_VENDOR | 12 - private static readonly IPAddress MulticastAddressV6 = IPAddress.Parse("ff02::1"); - public static readonly bool IPv6Support; - - /// - /// Maximum packets count that will be processed in Manual PollEvents - /// - public int MaxPacketsReceivePerUpdate = 0; - - // special case in iOS (and possibly android that should be resolved in unity) - internal bool NotConnected; - - public short Ttl - { - get - { -#if UNITY_SWITCH - return 0; -#else - return _udpSocketv4.Ttl; -#endif - } - internal set - { -#if !UNITY_SWITCH - _udpSocketv4.Ttl = value; -#endif - } - } - - static NetManager() - { -#if DISABLE_IPV6 - IPv6Support = false; -#elif !UNITY_2019_1_OR_NEWER && !UNITY_2018_4_OR_NEWER && (!UNITY_EDITOR && ENABLE_IL2CPP) - string version = UnityEngine.Application.unityVersion; - IPv6Support = Socket.OSSupportsIPv6 && int.Parse(version.Remove(version.IndexOf('f')).Split('.')[2]) >= 6; -#else - IPv6Support = Socket.OSSupportsIPv6; -#endif - } - - private void RegisterEndPoint(IPEndPoint ep) - { - if (UseNativeSockets && ep is NativeEndPoint nep) - { - _nativeAddrMap.Add(new NativeAddr(nep.NativeAddress, nep.NativeAddress.Length), nep); - } - } - - private void UnregisterEndPoint(IPEndPoint ep) - { - if (UseNativeSockets && ep is NativeEndPoint nep) - { - var nativeAddr = new NativeAddr(nep.NativeAddress, nep.NativeAddress.Length); - _nativeAddrMap.Remove(nativeAddr); - } - } - - private bool ProcessError(SocketException ex) - { - switch (ex.SocketErrorCode) - { - case SocketError.NotConnected: - NotConnected = true; - return true; - case SocketError.Interrupted: - case SocketError.NotSocket: - case SocketError.OperationAborted: - return true; - case SocketError.ConnectionReset: - case SocketError.MessageSize: - case SocketError.TimedOut: - case SocketError.NetworkReset: - //NetDebug.Write($"[R]Ignored error: {(int)ex.SocketErrorCode} - {ex}"); - break; - default: - NetDebug.WriteError($"[R]Error code: {(int)ex.SocketErrorCode} - {ex}"); - CreateEvent(NetEvent.EType.Error, errorCode: ex.SocketErrorCode); - break; - } - return false; - } - - private void ManualReceive(Socket socket, EndPoint bufferEndPoint) - { - //Reading data - try - { - int packetsReceived = 0; - while (socket.Available > 0) - { - ReceiveFrom(socket, ref bufferEndPoint); - packetsReceived++; - if (packetsReceived == MaxPacketsReceivePerUpdate) - break; - } - } - catch (SocketException ex) - { - ProcessError(ex); - } - catch (ObjectDisposedException) - { - - } - catch (Exception e) - { - //protects socket receive thread - NetDebug.WriteError("[NM] SocketReceiveThread error: " + e ); - } - } - - private bool NativeReceiveFrom(ref NetPacket packet, IntPtr s, byte[] addrBuffer, int addrSize) - { - //Reading data - packet.Size = NativeSocket.RecvFrom(s, packet.RawData, NetConstants.MaxPacketSize, addrBuffer, ref addrSize); - if (packet.Size == 0) - return false; //socket closed - if (packet.Size == -1) - { - var errorCode = NativeSocket.GetSocketError(); - //Linux timeout EAGAIN - return errorCode == SocketError.WouldBlock || errorCode == SocketError.TimedOut || ProcessError(new SocketException((int)errorCode)) == false; - } - - var nativeAddr = new NativeAddr(addrBuffer, addrSize); - if (!_nativeAddrMap.TryGetValue(nativeAddr, out var endPoint)) - endPoint = new NativeEndPoint(addrBuffer); - - //All ok! - //NetDebug.WriteForce($"[R]Received data from {endPoint}, result: {packet.Size}"); - OnMessageReceived(packet, endPoint); - packet = PoolGetPacket(NetConstants.MaxPacketSize); - return true; - } - - private void NativeReceiveLogic() - { - IntPtr socketHandle4 = _udpSocketv4.Handle; - IntPtr socketHandle6 = _udpSocketv6?.Handle ?? IntPtr.Zero; - byte[] addrBuffer4 = new byte[NativeSocket.IPv4AddrSize]; - byte[] addrBuffer6 = new byte[NativeSocket.IPv6AddrSize]; - int addrSize4 = addrBuffer4.Length; - int addrSize6 = addrBuffer6.Length; - var selectReadList = new List(2); - var socketv4 = _udpSocketv4; - var socketV6 = _udpSocketv6; - var packet = PoolGetPacket(NetConstants.MaxPacketSize); - - while (IsRunning) - { - if (socketV6 == null) - { - if (NativeReceiveFrom(ref packet, socketHandle4, addrBuffer4, addrSize4) == false) - return; - continue; - } - bool messageReceived = false; - if (socketv4.Available != 0) - { - if (NativeReceiveFrom(ref packet, socketHandle4, addrBuffer4, addrSize4) == false) - return; - messageReceived = true; - } - if (socketV6.Available != 0) - { - if (NativeReceiveFrom(ref packet, socketHandle6, addrBuffer6, addrSize6) == false) - return; - messageReceived = true; - } - if (messageReceived) - continue; - selectReadList.Clear(); - selectReadList.Add(socketv4); - selectReadList.Add(socketV6); - try - { - Socket.Select(selectReadList, null, null, ReceivePollingTime); - } - catch (SocketException ex) - { - if (ProcessError(ex)) - return; - } - catch (ObjectDisposedException) - { - //socket closed - return; - } - catch (ThreadAbortException) - { - //thread closed - return; - } - catch (Exception e) - { - //protects socket receive thread - NetDebug.WriteError("[NM] SocketReceiveThread error: " + e ); - } - } - } - - private void ReceiveFrom(Socket s, ref EndPoint bufferEndPoint) - { - var packet = PoolGetPacket(NetConstants.MaxPacketSize); - packet.Size = s.ReceiveFrom(packet.RawData, 0, NetConstants.MaxPacketSize, SocketFlags.None, ref bufferEndPoint); - OnMessageReceived(packet, (IPEndPoint)bufferEndPoint); - } - - private void ReceiveLogic() - { - EndPoint bufferEndPoint4 = new IPEndPoint(IPAddress.Any, 0); - EndPoint bufferEndPoint6 = new IPEndPoint(IPAddress.IPv6Any, 0); - var selectReadList = new List(2); - var socketv4 = _udpSocketv4; - var socketV6 = _udpSocketv6; - - while (IsRunning) - { - //Reading data - try - { - if (socketV6 == null) - { - if (socketv4.Available == 0 && !socketv4.Poll(ReceivePollingTime, SelectMode.SelectRead)) - continue; - ReceiveFrom(socketv4, ref bufferEndPoint4); - } - else - { - bool messageReceived = false; - if (socketv4.Available != 0) - { - ReceiveFrom(socketv4, ref bufferEndPoint4); - messageReceived = true; - } - if (socketV6.Available != 0) - { - ReceiveFrom(socketV6, ref bufferEndPoint6); - messageReceived = true; - } - if (messageReceived) - continue; - - selectReadList.Clear(); - selectReadList.Add(socketv4); - selectReadList.Add(socketV6); - Socket.Select(selectReadList, null, null, ReceivePollingTime); - } - //NetDebug.Write(NetLogLevel.Trace, $"[R]Received data from {bufferEndPoint}, result: {packet.Size}"); - } - catch (SocketException ex) - { - if (ProcessError(ex)) - return; - } - catch (ObjectDisposedException) - { - //socket closed - return; - } - catch (ThreadAbortException) - { - //thread closed - return; - } - catch (Exception e) - { - //protects socket receive thread - NetDebug.WriteError("[NM] SocketReceiveThread error: " + e ); - } - } - } - - /// - /// Start logic thread and listening on selected port - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - /// mode of library - public bool Start(IPAddress addressIPv4, IPAddress addressIPv6, int port, bool manualMode) - { - if (IsRunning && NotConnected == false) - return false; - - NotConnected = false; - _manualMode = manualMode; - UseNativeSockets = UseNativeSockets && NativeSocket.IsSupported; - _udpSocketv4 = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - if (!BindSocket(_udpSocketv4, new IPEndPoint(addressIPv4, port))) - return false; - - LocalPort = ((IPEndPoint) _udpSocketv4.LocalEndPoint).Port; - -#if UNITY_2018_3_OR_NEWER - if (_pausedSocketFix == null) - _pausedSocketFix = new PausedSocketFix(this, addressIPv4, addressIPv6, port, manualMode); -#endif - - IsRunning = true; - if (_manualMode) - { - _bufferEndPointv4 = new IPEndPoint(IPAddress.Any, 0); - } - - //Check IPv6 support - if (IPv6Support && IPv6Enabled) - { - _udpSocketv6 = new Socket(AddressFamily.InterNetworkV6, SocketType.Dgram, ProtocolType.Udp); - //Use one port for two sockets - if (BindSocket(_udpSocketv6, new IPEndPoint(addressIPv6, LocalPort))) - { - if (_manualMode) - { - _bufferEndPointv6 = new IPEndPoint(IPAddress.IPv6Any, 0); - } - } - else - { - _udpSocketv6 = null; - } - } - - if (!manualMode) - { - ThreadStart ts = ReceiveLogic; - if (UseNativeSockets) - ts = NativeReceiveLogic; - _receiveThread = new Thread(ts) - { - Name = $"ReceiveThread({LocalPort})", - IsBackground = true - }; - _receiveThread.Start(); - if (_logicThread == null) - { - _logicThread = new Thread(UpdateLogic) { Name = "LogicThread", IsBackground = true }; - _logicThread.Start(); - } - } - - return true; - } - - private bool BindSocket(Socket socket, IPEndPoint ep) - { - //Setup socket - socket.ReceiveTimeout = 500; - socket.SendTimeout = 500; - socket.ReceiveBufferSize = NetConstants.SocketBufferSize; - socket.SendBufferSize = NetConstants.SocketBufferSize; - socket.Blocking = true; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - try - { - socket.IOControl(SioUdpConnreset, new byte[] {0}, null); - } - catch - { - //ignored - } - } - - try - { - socket.ExclusiveAddressUse = !ReuseAddress; - socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, ReuseAddress); - } - catch - { - //Unity with IL2CPP throws an exception here, it doesn't matter in most cases so just ignore it - } - if (ep.AddressFamily == AddressFamily.InterNetwork) - { - Ttl = NetConstants.SocketTTL; - - try { socket.EnableBroadcast = true; } - catch (SocketException e) - { - NetDebug.WriteError($"[B]Broadcast error: {e.SocketErrorCode}"); - } - - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - try { socket.DontFragment = true; } - catch (SocketException e) - { - NetDebug.WriteError($"[B]DontFragment error: {e.SocketErrorCode}"); - } - } - } - //Bind - try - { - socket.Bind(ep); - NetDebug.Write(NetLogLevel.Trace, $"[B]Successfully binded to port: {((IPEndPoint)socket.LocalEndPoint).Port}, AF: {socket.AddressFamily}"); - - //join multicast - if (ep.AddressFamily == AddressFamily.InterNetworkV6) - { - try - { -#if !UNITY_2018_3_OR_NEWER - socket.SetSocketOption( - SocketOptionLevel.IPv6, - SocketOptionName.AddMembership, - new IPv6MulticastOption(MulticastAddressV6)); -#endif - } - catch (Exception) - { - // Unity3d throws exception - ignored - } - } - } - catch (SocketException bindException) - { - switch (bindException.SocketErrorCode) - { - //IPv6 bind fix - case SocketError.AddressAlreadyInUse: - if (socket.AddressFamily == AddressFamily.InterNetworkV6) - { - try - { - //Set IPv6Only - socket.DualMode = false; - socket.Bind(ep); - } - catch (SocketException ex) - { - //because its fixed in 2018_3 - NetDebug.WriteError($"[B]Bind exception: {ex}, errorCode: {ex.SocketErrorCode}"); - return false; - } - return true; - } - break; - //hack for iOS (Unity3D) - case SocketError.AddressFamilyNotSupported: - return true; - } - NetDebug.WriteError($"[B]Bind exception: {bindException}, errorCode: {bindException.SocketErrorCode}"); - return false; - } - return true; - } - - internal int SendRawAndRecycle(NetPacket packet, IPEndPoint remoteEndPoint) - { - int result = SendRaw(packet.RawData, 0, packet.Size, remoteEndPoint); - PoolRecycle(packet); - return result; - } - - internal int SendRaw(NetPacket packet, IPEndPoint remoteEndPoint) - { - return SendRaw(packet.RawData, 0, packet.Size, remoteEndPoint); - } - - internal int SendRaw(byte[] message, int start, int length, IPEndPoint remoteEndPoint) - { - if (!IsRunning) - return 0; - - NetPacket expandedPacket = null; - if (_extraPacketLayer != null) - { - expandedPacket = PoolGetPacket(length + _extraPacketLayer.ExtraPacketSizeForLayer); - Buffer.BlockCopy(message, start, expandedPacket.RawData, 0, length); - start = 0; - _extraPacketLayer.ProcessOutBoundPacket(ref remoteEndPoint, ref expandedPacket.RawData, ref start, ref length); - message = expandedPacket.RawData; - } - - var socket = _udpSocketv4; - if (remoteEndPoint.AddressFamily == AddressFamily.InterNetworkV6 && IPv6Support) - { - socket = _udpSocketv6; - if (socket == null) - return 0; - } - - int result; - try - { - if (UseNativeSockets) - { - byte[] socketAddress; - - if (remoteEndPoint is NativeEndPoint nep) - { - socketAddress = nep.NativeAddress; - } - else //Convert endpoint to raw - { - if (_endPointBuffer == null) - _endPointBuffer = new byte[NativeSocket.IPv6AddrSize]; - socketAddress = _endPointBuffer; - - bool ipv4 = remoteEndPoint.AddressFamily == AddressFamily.InterNetwork; - short addressFamily = NativeSocket.GetNativeAddressFamily(remoteEndPoint); - - socketAddress[0] = (byte) (addressFamily); - socketAddress[1] = (byte) (addressFamily >> 8); - socketAddress[2] = (byte) (remoteEndPoint.Port >> 8); - socketAddress[3] = (byte) (remoteEndPoint.Port); - - if (ipv4) - { -#pragma warning disable 618 - long addr = remoteEndPoint.Address.Address; -#pragma warning restore 618 - socketAddress[4] = (byte) (addr); - socketAddress[5] = (byte) (addr >> 8); - socketAddress[6] = (byte) (addr >> 16); - socketAddress[7] = (byte) (addr >> 24); - } - else - { -#if NETCOREAPP || NETSTANDARD2_1 || NETSTANDARD2_1_OR_GREATER - remoteEndPoint.Address.TryWriteBytes(new Span(socketAddress, 8, 16), out _); -#else - byte[] addrBytes = remoteEndPoint.Address.GetAddressBytes(); - Buffer.BlockCopy(addrBytes, 0, socketAddress, 8, 16); -#endif - } - } - -#if LITENETLIB_UNSAFE - unsafe - { - fixed (byte* dataWithOffset = &message[start]) - { - result = - NativeSocket.SendTo(socket.Handle, dataWithOffset, length, socketAddress, socketAddress.Length); - } - } -#else - if (start > 0) - { - if (_sendToBuffer == null) - _sendToBuffer = new byte[NetConstants.MaxPacketSize]; - Buffer.BlockCopy(message, start, _sendToBuffer, 0, length); - message = _sendToBuffer; - } - - result = NativeSocket.SendTo(socket.Handle, message, length, socketAddress, socketAddress.Length); -#endif - if (result == -1) - throw NativeSocket.GetSocketException(); - } - else - { - result = socket.SendTo(message, start, length, SocketFlags.None, remoteEndPoint); - } - //NetDebug.WriteForce("[S]Send packet to {0}, result: {1}", remoteEndPoint, result); - } - catch (SocketException ex) - { - switch (ex.SocketErrorCode) - { - case SocketError.NoBufferSpaceAvailable: - case SocketError.Interrupted: - return 0; - case SocketError.MessageSize: - NetDebug.Write(NetLogLevel.Trace, $"[SRD] 10040, datalen: {length}"); - return 0; - - case SocketError.HostUnreachable: - case SocketError.NetworkUnreachable: - if (DisconnectOnUnreachable && TryGetPeer(remoteEndPoint, out var fromPeer)) - { - DisconnectPeerForce( - fromPeer, - ex.SocketErrorCode == SocketError.HostUnreachable - ? DisconnectReason.HostUnreachable - : DisconnectReason.NetworkUnreachable, - ex.SocketErrorCode, - null); - } - - CreateEvent(NetEvent.EType.Error, remoteEndPoint: remoteEndPoint, errorCode: ex.SocketErrorCode); - return -1; - - case SocketError.Shutdown: - CreateEvent(NetEvent.EType.Error, remoteEndPoint: remoteEndPoint, errorCode: ex.SocketErrorCode); - return -1; - - default: - NetDebug.WriteError($"[S] {ex}"); - return -1; - } - } - catch (Exception ex) - { - NetDebug.WriteError($"[S] {ex}"); - return 0; - } - finally - { - if (expandedPacket != null) - { - PoolRecycle(expandedPacket); - } - } - - if (result <= 0) - return 0; - - if (EnableStatistics) - { - Statistics.IncrementPacketsSent(); - Statistics.AddBytesSent(length); - } - - return result; - } - - public bool SendBroadcast(NetDataWriter writer, int port) - { - return SendBroadcast(writer.Data, 0, writer.Length, port); - } - - public bool SendBroadcast(byte[] data, int port) - { - return SendBroadcast(data, 0, data.Length, port); - } - - public bool SendBroadcast(byte[] data, int start, int length, int port) - { - if (!IsRunning) - return false; - - NetPacket packet; - if (_extraPacketLayer != null) - { - var headerSize = NetPacket.GetHeaderSize(PacketProperty.Broadcast); - packet = PoolGetPacket(headerSize + length + _extraPacketLayer.ExtraPacketSizeForLayer); - packet.Property = PacketProperty.Broadcast; - Buffer.BlockCopy(data, start, packet.RawData, headerSize, length); - var checksumComputeStart = 0; - int preCrcLength = length + headerSize; - IPEndPoint emptyEp = null; - _extraPacketLayer.ProcessOutBoundPacket(ref emptyEp, ref packet.RawData, ref checksumComputeStart, ref preCrcLength); - } - else - { - packet = PoolGetWithData(PacketProperty.Broadcast, data, start, length); - } - - bool broadcastSuccess = false; - bool multicastSuccess = false; - try - { - broadcastSuccess = _udpSocketv4.SendTo( - packet.RawData, - 0, - packet.Size, - SocketFlags.None, - new IPEndPoint(IPAddress.Broadcast, port)) > 0; - - if (_udpSocketv6 != null) - { - multicastSuccess = _udpSocketv6.SendTo( - packet.RawData, - 0, - packet.Size, - SocketFlags.None, - new IPEndPoint(MulticastAddressV6, port)) > 0; - } - } - catch (Exception ex) - { - NetDebug.WriteError($"[S][MCAST] {ex}"); - return broadcastSuccess; - } - finally - { - PoolRecycle(packet); - } - - return broadcastSuccess || multicastSuccess; - } - - private void CloseSocket() - { - IsRunning = false; - _udpSocketv4?.Close(); - _udpSocketv6?.Close(); - _udpSocketv4 = null; - _udpSocketv6 = null; - if (_receiveThread != null && _receiveThread != Thread.CurrentThread) - _receiveThread.Join(); - _receiveThread = null; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs.meta deleted file mode 100644 index ee9309f..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.Socket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d190de687c3f4c86fba8fa624e0e5a2f -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs deleted file mode 100644 index f4599b4..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs +++ /dev/null @@ -1,1818 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using LiteNetLib.Layers; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - public sealed class NetPacketReader : NetDataReader - { - private NetPacket _packet; - private readonly NetManager _manager; - private readonly NetEvent _evt; - - internal NetPacketReader(NetManager manager, NetEvent evt) - { - _manager = manager; - _evt = evt; - } - - internal void SetSource(NetPacket packet, int headerSize) - { - if (packet == null) - return; - _packet = packet; - SetSource(packet.RawData, headerSize, packet.Size); - } - - internal void RecycleInternal() - { - Clear(); - if (_packet != null) - _manager.PoolRecycle(_packet); - _packet = null; - _manager.RecycleEvent(_evt); - } - - public void Recycle() - { - if (_manager.AutoRecycle) - return; - RecycleInternal(); - } - } - - internal sealed class NetEvent - { - public NetEvent Next; - - public enum EType - { - Connect, - Disconnect, - Receive, - ReceiveUnconnected, - Error, - ConnectionLatencyUpdated, - Broadcast, - ConnectionRequest, - MessageDelivered, - PeerAddressChanged - } - public EType Type; - - public NetPeer Peer; - public IPEndPoint RemoteEndPoint; - public object UserData; - public int Latency; - public SocketError ErrorCode; - public DisconnectReason DisconnectReason; - public ConnectionRequest ConnectionRequest; - public DeliveryMethod DeliveryMethod; - public byte ChannelNumber; - public readonly NetPacketReader DataReader; - - public NetEvent(NetManager manager) - { - DataReader = new NetPacketReader(manager, this); - } - } - - /// - /// Main class for all network operations. Can be used as client and/or server. - /// - public partial class NetManager : IEnumerable - { - private class IPEndPointComparer : IEqualityComparer - { - public bool Equals(IPEndPoint x, IPEndPoint y) - { - return x.Address.Equals(y.Address) && x.Port == y.Port; - } - - public int GetHashCode(IPEndPoint obj) - { - return obj.GetHashCode(); - } - } - - public struct NetPeerEnumerator : IEnumerator - { - private readonly NetPeer _initialPeer; - private NetPeer _p; - - public NetPeerEnumerator(NetPeer p) - { - _initialPeer = p; - _p = null; - } - - public void Dispose() - { - - } - - public bool MoveNext() - { - _p = _p == null ? _initialPeer : _p.NextPeer; - return _p != null; - } - - public void Reset() - { - throw new NotSupportedException(); - } - - public NetPeer Current => _p; - object IEnumerator.Current => _p; - } - -#if LITENETLIB_DEBUGGING - private struct IncomingData - { - public NetPacket Data; - public IPEndPoint EndPoint; - public DateTime TimeWhenGet; - } - private readonly List _pingSimulationList = new List(); - private readonly Random _randomGenerator = new Random(); - private const int MinLatencyThreshold = 5; -#endif - - private Thread _logicThread; - private bool _manualMode; - private readonly AutoResetEvent _updateTriggerEvent = new AutoResetEvent(true); - - private NetEvent _pendingEventHead; - private NetEvent _pendingEventTail; - - private NetEvent _netEventPoolHead; - private readonly INetEventListener _netEventListener; - private readonly IDeliveryEventListener _deliveryEventListener; - private readonly INtpEventListener _ntpEventListener; - private readonly IPeerAddressChangedListener _peerAddressChangedListener; - - private readonly Dictionary _peersDict = new Dictionary(new IPEndPointComparer()); - private readonly Dictionary _requestsDict = new Dictionary(new IPEndPointComparer()); - private readonly Dictionary _ntpRequests = new Dictionary(new IPEndPointComparer()); - private readonly ReaderWriterLockSlim _peersLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); - private volatile NetPeer _headPeer; - private int _connectedPeersCount; - private readonly List _connectedPeerListCache = new List(); - private NetPeer[] _peersArray = new NetPeer[32]; - private readonly PacketLayerBase _extraPacketLayer; - private int _lastPeerId; - private ConcurrentQueue _peerIds = new ConcurrentQueue(); - private byte _channelsCount = 1; - private readonly object _eventLock = new object(); - - //config section - /// - /// Enable messages receiving without connection. (with SendUnconnectedMessage method) - /// - public bool UnconnectedMessagesEnabled = false; - - /// - /// Enable nat punch messages - /// - public bool NatPunchEnabled = false; - - /// - /// Library logic update and send period in milliseconds - /// Lowest values in Windows doesn't change much because of Thread.Sleep precision - /// To more frequent sends (or sends tied to your game logic) use - /// - public int UpdateTime = 15; - - /// - /// Interval for latency detection and checking connection (in milliseconds) - /// - public int PingInterval = 1000; - - /// - /// If NetManager doesn't receive any packet from remote peer during this time (in milliseconds) then connection will be closed - /// (including library internal keepalive packets) - /// - public int DisconnectTimeout = 5000; - - /// - /// Simulate packet loss by dropping random amount of packets. (Works only in DEBUG mode) - /// - public bool SimulatePacketLoss = false; - - /// - /// Simulate latency by holding packets for random time. (Works only in DEBUG mode) - /// - public bool SimulateLatency = false; - - /// - /// Chance of packet loss when simulation enabled. value in percents (1 - 100). - /// - public int SimulationPacketLossChance = 10; - - /// - /// Minimum simulated latency (in milliseconds) - /// - public int SimulationMinLatency = 30; - - /// - /// Maximum simulated latency (in milliseconds) - /// - public int SimulationMaxLatency = 100; - - /// - /// Events automatically will be called without PollEvents method from another thread - /// - public bool UnsyncedEvents = false; - - /// - /// If true - receive event will be called from "receive" thread immediately otherwise on PollEvents call - /// - public bool UnsyncedReceiveEvent = false; - - /// - /// If true - delivery event will be called from "receive" thread immediately otherwise on PollEvents call - /// - public bool UnsyncedDeliveryEvent = false; - - /// - /// Allows receive broadcast packets - /// - public bool BroadcastReceiveEnabled = false; - - /// - /// Delay between initial connection attempts (in milliseconds) - /// - public int ReconnectDelay = 500; - - /// - /// Maximum connection attempts before client stops and call disconnect event. - /// - public int MaxConnectAttempts = 10; - - /// - /// Enables socket option "ReuseAddress" for specific purposes - /// - public bool ReuseAddress = false; - - /// - /// Statistics of all connections - /// - public readonly NetStatistics Statistics = new NetStatistics(); - - /// - /// Toggles the collection of network statistics for the instance and all known peers - /// - public bool EnableStatistics = false; - - /// - /// NatPunchModule for NAT hole punching operations - /// - public readonly NatPunchModule NatPunchModule; - - /// - /// Returns true if socket listening and update thread is running - /// - public bool IsRunning { get; private set; } - - /// - /// Local EndPoint (host and port) - /// - public int LocalPort { get; private set; } - - /// - /// Automatically recycle NetPacketReader after OnReceive event - /// - public bool AutoRecycle; - - /// - /// IPv6 support - /// - public bool IPv6Enabled = true; - - /// - /// Override MTU for all new peers registered in this NetManager, will ignores MTU Discovery! - /// - public int MtuOverride = 0; - - /// - /// Sets initial MTU to lowest possible value according to RFC1191 (576 bytes) - /// - public bool UseSafeMtu = false; - - /// - /// First peer. Useful for Client mode - /// - public NetPeer FirstPeer => _headPeer; - - /// - /// Experimental feature mostly for servers. Only for Windows/Linux - /// use direct socket calls for send/receive to drastically increase speed and reduce GC pressure - /// - public bool UseNativeSockets = false; - - /// - /// Disconnect peers if HostUnreachable or NetworkUnreachable spawned (old behaviour 0.9.x was true) - /// - public bool DisconnectOnUnreachable = false; - - /// - /// Allows peer change it's ip (lte to wifi, wifi to lte, etc). Use only on server - /// - public bool AllowPeerAddressChange = false; - - /// - /// QoS channel count per message type (value must be between 1 and 64 channels) - /// - public byte ChannelsCount - { - get => _channelsCount; - set - { - if (value < 1 || value > 64) - throw new ArgumentException("Channels count must be between 1 and 64"); - _channelsCount = value; - } - } - - /// - /// Returns connected peers list (with internal cached list) - /// - public List ConnectedPeerList - { - get - { - GetPeersNonAlloc(_connectedPeerListCache, ConnectionState.Connected); - return _connectedPeerListCache; - } - } - - /// - /// Gets peer by peer id - /// - /// id of peer - /// Peer if peer with id exist, otherwise null - public NetPeer GetPeerById(int id) - { - if (id >= 0 && id < _peersArray.Length) - { - return _peersArray[id]; - } - - return null; - } - - /// - /// Gets peer by peer id - /// - /// id of peer - /// resulting peer - /// True if peer with id exist, otherwise false - public bool TryGetPeerById(int id, out NetPeer peer) - { - peer = GetPeerById(id); - - return peer != null; - } - - /// - /// Returns connected peers count - /// - public int ConnectedPeersCount => Interlocked.CompareExchange(ref _connectedPeersCount,0,0); - - public int ExtraPacketSizeForLayer => _extraPacketLayer?.ExtraPacketSizeForLayer ?? 0; - - private bool TryGetPeer(IPEndPoint endPoint, out NetPeer peer) - { - _peersLock.EnterReadLock(); - bool result = _peersDict.TryGetValue(endPoint, out peer); - _peersLock.ExitReadLock(); - return result; - } - - private void AddPeer(NetPeer peer) - { - _peersLock.EnterWriteLock(); - if (_headPeer != null) - { - peer.NextPeer = _headPeer; - _headPeer.PrevPeer = peer; - } - _headPeer = peer; - _peersDict.Add(peer.EndPoint, peer); - if (peer.Id >= _peersArray.Length) - { - int newSize = _peersArray.Length * 2; - while (peer.Id >= newSize) - newSize *= 2; - Array.Resize(ref _peersArray, newSize); - } - _peersArray[peer.Id] = peer; - RegisterEndPoint(peer.EndPoint); - _peersLock.ExitWriteLock(); - } - - private void RemovePeer(NetPeer peer) - { - _peersLock.EnterWriteLock(); - RemovePeerInternal(peer); - _peersLock.ExitWriteLock(); - } - - private void RemovePeerInternal(NetPeer peer) - { - if (!_peersDict.Remove(peer.EndPoint)) - return; - if (peer == _headPeer) - _headPeer = peer.NextPeer; - - if (peer.PrevPeer != null) - peer.PrevPeer.NextPeer = peer.NextPeer; - if (peer.NextPeer != null) - peer.NextPeer.PrevPeer = peer.PrevPeer; - peer.PrevPeer = null; - - _peersArray[peer.Id] = null; - _peerIds.Enqueue(peer.Id); - UnregisterEndPoint(peer.EndPoint); - } - - /// - /// NetManager constructor - /// - /// Network events listener (also can implement IDeliveryEventListener) - /// Extra processing of packages, like CRC checksum or encryption. All connected NetManagers must have same layer. - public NetManager(INetEventListener listener, PacketLayerBase extraPacketLayer = null) - { - _netEventListener = listener; - _deliveryEventListener = listener as IDeliveryEventListener; - _ntpEventListener = listener as INtpEventListener; - _peerAddressChangedListener = listener as IPeerAddressChangedListener; - NatPunchModule = new NatPunchModule(this); - _extraPacketLayer = extraPacketLayer; - } - - internal void ConnectionLatencyUpdated(NetPeer fromPeer, int latency) - { - CreateEvent(NetEvent.EType.ConnectionLatencyUpdated, fromPeer, latency: latency); - } - - internal void MessageDelivered(NetPeer fromPeer, object userData) - { - if(_deliveryEventListener != null) - CreateEvent(NetEvent.EType.MessageDelivered, fromPeer, userData: userData); - } - - internal void DisconnectPeerForce(NetPeer peer, - DisconnectReason reason, - SocketError socketErrorCode, - NetPacket eventData) - { - DisconnectPeer(peer, reason, socketErrorCode, true, null, 0, 0, eventData); - } - - private void DisconnectPeer( - NetPeer peer, - DisconnectReason reason, - SocketError socketErrorCode, - bool force, - byte[] data, - int start, - int count, - NetPacket eventData) - { - var shutdownResult = peer.Shutdown(data, start, count, force); - if (shutdownResult == ShutdownResult.None) - return; - if(shutdownResult == ShutdownResult.WasConnected) - Interlocked.Decrement(ref _connectedPeersCount); - CreateEvent( - NetEvent.EType.Disconnect, - peer, - errorCode: socketErrorCode, - disconnectReason: reason, - readerSource: eventData); - } - - private void CreateEvent( - NetEvent.EType type, - NetPeer peer = null, - IPEndPoint remoteEndPoint = null, - SocketError errorCode = 0, - int latency = 0, - DisconnectReason disconnectReason = DisconnectReason.ConnectionFailed, - ConnectionRequest connectionRequest = null, - DeliveryMethod deliveryMethod = DeliveryMethod.Unreliable, - byte channelNumber = 0, - NetPacket readerSource = null, - object userData = null) - { - NetEvent evt; - bool unsyncEvent = UnsyncedEvents; - - if (type == NetEvent.EType.Connect) - Interlocked.Increment(ref _connectedPeersCount); - else if (type == NetEvent.EType.MessageDelivered) - unsyncEvent = UnsyncedDeliveryEvent; - - lock(_eventLock) - { - evt = _netEventPoolHead; - if (evt == null) - evt = new NetEvent(this); - else - _netEventPoolHead = evt.Next; - } - - evt.Next = null; - evt.Type = type; - evt.DataReader.SetSource(readerSource, readerSource?.GetHeaderSize() ?? 0); - evt.Peer = peer; - evt.RemoteEndPoint = remoteEndPoint; - evt.Latency = latency; - evt.ErrorCode = errorCode; - evt.DisconnectReason = disconnectReason; - evt.ConnectionRequest = connectionRequest; - evt.DeliveryMethod = deliveryMethod; - evt.ChannelNumber = channelNumber; - evt.UserData = userData; - - if (unsyncEvent || _manualMode) - { - ProcessEvent(evt); - } - else - { - lock (_eventLock) - { - if (_pendingEventTail == null) - _pendingEventHead = evt; - else - _pendingEventTail.Next = evt; - _pendingEventTail = evt; - } - } - } - - private void ProcessEvent(NetEvent evt) - { - NetDebug.Write("[NM] Processing event: " + evt.Type); - bool emptyData = evt.DataReader.IsNull; - switch (evt.Type) - { - case NetEvent.EType.Connect: - _netEventListener.OnPeerConnected(evt.Peer); - break; - case NetEvent.EType.Disconnect: - var info = new DisconnectInfo - { - Reason = evt.DisconnectReason, - AdditionalData = evt.DataReader, - SocketErrorCode = evt.ErrorCode - }; - _netEventListener.OnPeerDisconnected(evt.Peer, info); - break; - case NetEvent.EType.Receive: - _netEventListener.OnNetworkReceive(evt.Peer, evt.DataReader, evt.ChannelNumber, evt.DeliveryMethod); - break; - case NetEvent.EType.ReceiveUnconnected: - _netEventListener.OnNetworkReceiveUnconnected(evt.RemoteEndPoint, evt.DataReader, UnconnectedMessageType.BasicMessage); - break; - case NetEvent.EType.Broadcast: - _netEventListener.OnNetworkReceiveUnconnected(evt.RemoteEndPoint, evt.DataReader, UnconnectedMessageType.Broadcast); - break; - case NetEvent.EType.Error: - _netEventListener.OnNetworkError(evt.RemoteEndPoint, evt.ErrorCode); - break; - case NetEvent.EType.ConnectionLatencyUpdated: - _netEventListener.OnNetworkLatencyUpdate(evt.Peer, evt.Latency); - break; - case NetEvent.EType.ConnectionRequest: - _netEventListener.OnConnectionRequest(evt.ConnectionRequest); - break; - case NetEvent.EType.MessageDelivered: - _deliveryEventListener.OnMessageDelivered(evt.Peer, evt.UserData); - break; - case NetEvent.EType.PeerAddressChanged: - _peersLock.EnterUpgradeableReadLock(); - IPEndPoint previousAddress = null; - if (_peersDict.ContainsKey(evt.Peer.EndPoint)) - { - _peersLock.EnterWriteLock(); - _peersDict.Remove(evt.Peer.EndPoint); - previousAddress = evt.Peer.EndPoint; - evt.Peer.FinishEndPointChange(evt.RemoteEndPoint); - _peersDict.Add(evt.Peer.EndPoint, evt.Peer); - _peersLock.ExitWriteLock(); - } - _peersLock.ExitUpgradeableReadLock(); - if(previousAddress != null && _peerAddressChangedListener != null) - _peerAddressChangedListener.OnPeerAddressChanged(evt.Peer, previousAddress); - break; - } - //Recycle if not message - if (emptyData) - RecycleEvent(evt); - else if (AutoRecycle) - evt.DataReader.RecycleInternal(); - } - - internal void RecycleEvent(NetEvent evt) - { - evt.Peer = null; - evt.ErrorCode = 0; - evt.RemoteEndPoint = null; - evt.ConnectionRequest = null; - lock(_eventLock) - { - evt.Next = _netEventPoolHead; - _netEventPoolHead = evt; - } - } - - //Update function - private void UpdateLogic() - { - var peersToRemove = new List(); - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - while (IsRunning) - { - try - { - ProcessDelayedPackets(); - int elapsed = (int)stopwatch.ElapsedMilliseconds; - elapsed = elapsed <= 0 ? 1 : elapsed; - stopwatch.Restart(); - - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if (netPeer.ConnectionState == ConnectionState.Disconnected && - netPeer.TimeSinceLastPacket > DisconnectTimeout) - { - peersToRemove.Add(netPeer); - } - else - { - netPeer.Update(elapsed); - } - } - - if (peersToRemove.Count > 0) - { - _peersLock.EnterWriteLock(); - for (int i = 0; i < peersToRemove.Count; i++) - RemovePeerInternal(peersToRemove[i]); - _peersLock.ExitWriteLock(); - peersToRemove.Clear(); - } - - ProcessNtpRequests(elapsed); - - int sleepTime = UpdateTime - (int)stopwatch.ElapsedMilliseconds; - if (sleepTime > 0) - _updateTriggerEvent.WaitOne(sleepTime); - } - catch (ThreadAbortException) - { - return; - } - catch (Exception e) - { - NetDebug.WriteError("[NM] LogicThread error: " + e); - } - } - stopwatch.Stop(); - } - - [Conditional("LITENETLIB_DEBUGGING")] - private void ProcessDelayedPackets() - { -#if LITENETLIB_DEBUGGING - if (!SimulateLatency && _pingSimulationList.Count == 0) - return; - - var time = DateTime.UtcNow; - lock (_pingSimulationList) - { - for (int i = 0; i < _pingSimulationList.Count; i++) - { - var incomingData = _pingSimulationList[i]; - if (incomingData.TimeWhenGet <= time) - { - DebugMessageReceived(incomingData.Data, incomingData.EndPoint); - _pingSimulationList.RemoveAt(i); - i--; - } - } - } -#endif - } - - private void ProcessNtpRequests(int elapsedMilliseconds) - { - List requestsToRemove = null; - foreach (var ntpRequest in _ntpRequests) - { - ntpRequest.Value.Send(_udpSocketv4, elapsedMilliseconds); - if(ntpRequest.Value.NeedToKill) - { - if (requestsToRemove == null) - requestsToRemove = new List(); - requestsToRemove.Add(ntpRequest.Key); - } - } - - if (requestsToRemove != null) - { - foreach (var ipEndPoint in requestsToRemove) - { - _ntpRequests.Remove(ipEndPoint); - } - } - } - - /// - /// Update and send logic. Use this only when NetManager started in manual mode - /// - /// elapsed milliseconds since last update call - public void ManualUpdate(int elapsedMilliseconds) - { - if (!_manualMode) - return; - - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if (netPeer.ConnectionState == ConnectionState.Disconnected && netPeer.TimeSinceLastPacket > DisconnectTimeout) - { - RemovePeerInternal(netPeer); - } - else - { - netPeer.Update(elapsedMilliseconds); - } - } - ProcessNtpRequests(elapsedMilliseconds); - } - - internal NetPeer OnConnectionSolved(ConnectionRequest request, byte[] rejectData, int start, int length) - { - NetPeer netPeer = null; - - if (request.Result == ConnectionRequestResult.RejectForce) - { - NetDebug.Write(NetLogLevel.Trace, "[NM] Peer connect reject force."); - if (rejectData != null && length > 0) - { - var shutdownPacket = PoolGetWithProperty(PacketProperty.Disconnect, length); - shutdownPacket.ConnectionNumber = request.InternalPacket.ConnectionNumber; - FastBitConverter.GetBytes(shutdownPacket.RawData, 1, request.InternalPacket.ConnectionTime); - if (shutdownPacket.Size >= NetConstants.PossibleMtu[0]) - NetDebug.WriteError("[Peer] Disconnect additional data size more than MTU!"); - else - Buffer.BlockCopy(rejectData, start, shutdownPacket.RawData, 9, length); - SendRawAndRecycle(shutdownPacket, request.RemoteEndPoint); - } - } - else - { - _peersLock.EnterUpgradeableReadLock(); - if (_peersDict.TryGetValue(request.RemoteEndPoint, out netPeer)) - { - //already have peer - _peersLock.ExitUpgradeableReadLock(); - } - else if (request.Result == ConnectionRequestResult.Reject) - { - netPeer = new NetPeer(this, request.RemoteEndPoint, GetNextPeerId()); - netPeer.Reject(request.InternalPacket, rejectData, start, length); - AddPeer(netPeer); - _peersLock.ExitUpgradeableReadLock(); - NetDebug.Write(NetLogLevel.Trace, "[NM] Peer connect reject."); - } - else //Accept - { - netPeer = new NetPeer(this, request, GetNextPeerId()); - AddPeer(netPeer); - _peersLock.ExitUpgradeableReadLock(); - CreateEvent(NetEvent.EType.Connect, netPeer); - NetDebug.Write(NetLogLevel.Trace, $"[NM] Received peer connection Id: {netPeer.ConnectTime}, EP: {netPeer.EndPoint}"); - } - } - - lock(_requestsDict) - _requestsDict.Remove(request.RemoteEndPoint); - - return netPeer; - } - - private int GetNextPeerId() - { - return _peerIds.TryDequeue(out int id) ? id : _lastPeerId++; - } - - private void ProcessConnectRequest( - IPEndPoint remoteEndPoint, - NetPeer netPeer, - NetConnectRequestPacket connRequest) - { - //if we have peer - if (netPeer != null) - { - var processResult = netPeer.ProcessConnectRequest(connRequest); - NetDebug.Write($"ConnectRequest LastId: {netPeer.ConnectTime}, NewId: {connRequest.ConnectionTime}, EP: {remoteEndPoint}, Result: {processResult}"); - - switch (processResult) - { - case ConnectRequestResult.Reconnection: - DisconnectPeerForce(netPeer, DisconnectReason.Reconnect, 0, null); - RemovePeer(netPeer); - //go to new connection - break; - case ConnectRequestResult.NewConnection: - RemovePeer(netPeer); - //go to new connection - break; - case ConnectRequestResult.P2PLose: - DisconnectPeerForce(netPeer, DisconnectReason.PeerToPeerConnection, 0, null); - RemovePeer(netPeer); - //go to new connection - break; - default: - //no operations needed - return; - } - //ConnectRequestResult.NewConnection - //Set next connection number - if(processResult != ConnectRequestResult.P2PLose) - connRequest.ConnectionNumber = (byte)((netPeer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); - //To reconnect peer - } - else - { - NetDebug.Write($"ConnectRequest Id: {connRequest.ConnectionTime}, EP: {remoteEndPoint}"); - } - - ConnectionRequest req; - lock (_requestsDict) - { - if (_requestsDict.TryGetValue(remoteEndPoint, out req)) - { - req.UpdateRequest(connRequest); - return; - } - req = new ConnectionRequest(remoteEndPoint, connRequest, this); - _requestsDict.Add(remoteEndPoint, req); - } - NetDebug.Write($"[NM] Creating request event: {connRequest.ConnectionTime}"); - CreateEvent(NetEvent.EType.ConnectionRequest, connectionRequest: req); - } - - private void OnMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) - { -#if LITENETLIB_DEBUGGING - if (SimulatePacketLoss && _randomGenerator.NextDouble() * 100 < SimulationPacketLossChance) - { - //drop packet - return; - } - if (SimulateLatency) - { - int latency = _randomGenerator.Next(SimulationMinLatency, SimulationMaxLatency); - if (latency > MinLatencyThreshold) - { - lock (_pingSimulationList) - { - _pingSimulationList.Add(new IncomingData - { - Data = packet, - EndPoint = remoteEndPoint, - TimeWhenGet = DateTime.UtcNow.AddMilliseconds(latency) - }); - } - //hold packet - return; - } - } - - //ProcessEvents - DebugMessageReceived(packet, remoteEndPoint); - } - - private void DebugMessageReceived(NetPacket packet, IPEndPoint remoteEndPoint) - { -#endif - var originalPacketSize = packet.Size; - if (EnableStatistics) - { - Statistics.IncrementPacketsReceived(); - Statistics.AddBytesReceived(originalPacketSize); - } - - if (_ntpRequests.Count > 0) - { - if (_ntpRequests.TryGetValue(remoteEndPoint, out var request)) - { - if (packet.Size < 48) - { - NetDebug.Write(NetLogLevel.Trace, $"NTP response too short: {packet.Size}"); - return; - } - - byte[] copiedData = new byte[packet.Size]; - Buffer.BlockCopy(packet.RawData, 0, copiedData, 0, packet.Size); - NtpPacket ntpPacket = NtpPacket.FromServerResponse(copiedData, DateTime.UtcNow); - try - { - ntpPacket.ValidateReply(); - } - catch (InvalidOperationException ex) - { - NetDebug.Write(NetLogLevel.Trace, $"NTP response error: {ex.Message}"); - ntpPacket = null; - } - - if (ntpPacket != null) - { - _ntpRequests.Remove(remoteEndPoint); - _ntpEventListener?.OnNtpResponse(ntpPacket); - } - return; - } - } - - if (_extraPacketLayer != null) - { - int start = 0; - _extraPacketLayer.ProcessInboundPacket(ref remoteEndPoint, ref packet.RawData, ref start, ref packet.Size); - if (packet.Size == 0) - return; - } - - if (!packet.Verify()) - { - NetDebug.WriteError("[NM] DataReceived: bad!"); - PoolRecycle(packet); - return; - } - - switch (packet.Property) - { - //special case connect request - case PacketProperty.ConnectRequest: - if (NetConnectRequestPacket.GetProtocolId(packet) != NetConstants.ProtocolId) - { - SendRawAndRecycle(PoolGetWithProperty(PacketProperty.InvalidProtocol), remoteEndPoint); - return; - } - break; - //unconnected messages - case PacketProperty.Broadcast: - if (!BroadcastReceiveEnabled) - return; - CreateEvent(NetEvent.EType.Broadcast, remoteEndPoint: remoteEndPoint, readerSource: packet); - return; - case PacketProperty.UnconnectedMessage: - if (!UnconnectedMessagesEnabled) - return; - CreateEvent(NetEvent.EType.ReceiveUnconnected, remoteEndPoint: remoteEndPoint, readerSource: packet); - return; - case PacketProperty.NatMessage: - if (NatPunchEnabled) - NatPunchModule.ProcessMessage(remoteEndPoint, packet); - return; - } - - //Check normal packets - _peersLock.EnterReadLock(); - bool peerFound = _peersDict.TryGetValue(remoteEndPoint, out var netPeer); - _peersLock.ExitReadLock(); - - if (peerFound && EnableStatistics) - { - netPeer.Statistics.IncrementPacketsReceived(); - netPeer.Statistics.AddBytesReceived(originalPacketSize); - } - - switch (packet.Property) - { - case PacketProperty.ConnectRequest: - var connRequest = NetConnectRequestPacket.FromData(packet); - if (connRequest != null) - ProcessConnectRequest(remoteEndPoint, netPeer, connRequest); - break; - case PacketProperty.PeerNotFound: - if (peerFound) //local - { - if (netPeer.ConnectionState != ConnectionState.Connected) - return; - if (packet.Size == 1) - { - //first reply - //send NetworkChanged packet - netPeer.ResetMtu(); - SendRaw(NetConnectAcceptPacket.MakeNetworkChanged(netPeer), remoteEndPoint); - NetDebug.Write($"PeerNotFound sending connection info: {remoteEndPoint}"); - } - else if (packet.Size == 2 && packet.RawData[1] == 1) - { - //second reply - DisconnectPeerForce(netPeer, DisconnectReason.PeerNotFound, 0, null); - } - } - else if (packet.Size > 1) //remote - { - //check if this is old peer - bool isOldPeer = false; - - if (AllowPeerAddressChange) - { - NetDebug.Write($"[NM] Looks like address change: {packet.Size}"); - var remoteData = NetConnectAcceptPacket.FromData(packet); - if (remoteData != null && - remoteData.PeerNetworkChanged && - remoteData.PeerId < _peersArray.Length) - { - _peersLock.EnterUpgradeableReadLock(); - var peer = _peersArray[remoteData.PeerId]; - if (peer != null && - peer.ConnectTime == remoteData.ConnectionTime && - peer.ConnectionNum == remoteData.ConnectionNumber) - { - if (peer.ConnectionState == ConnectionState.Connected) - { - peer.InitiateEndPointChange(); - CreateEvent(NetEvent.EType.PeerAddressChanged, peer, remoteEndPoint); - NetDebug.Write("[NM] PeerNotFound change address of remote peer"); - } - isOldPeer = true; - } - _peersLock.ExitUpgradeableReadLock(); - } - } - - PoolRecycle(packet); - - //else peer really not found - if (!isOldPeer) - { - var secondResponse = PoolGetWithProperty(PacketProperty.PeerNotFound, 1); - secondResponse.RawData[1] = 1; - SendRawAndRecycle(secondResponse, remoteEndPoint); - } - } - break; - case PacketProperty.InvalidProtocol: - if (peerFound && netPeer.ConnectionState == ConnectionState.Outgoing) - DisconnectPeerForce(netPeer, DisconnectReason.InvalidProtocol, 0, null); - break; - case PacketProperty.Disconnect: - if (peerFound) - { - var disconnectResult = netPeer.ProcessDisconnect(packet); - if (disconnectResult == DisconnectResult.None) - { - PoolRecycle(packet); - return; - } - DisconnectPeerForce( - netPeer, - disconnectResult == DisconnectResult.Disconnect - ? DisconnectReason.RemoteConnectionClose - : DisconnectReason.ConnectionRejected, - 0, packet); - } - else - { - PoolRecycle(packet); - } - //Send shutdown - SendRawAndRecycle(PoolGetWithProperty(PacketProperty.ShutdownOk), remoteEndPoint); - break; - case PacketProperty.ConnectAccept: - if (!peerFound) - return; - var connAccept = NetConnectAcceptPacket.FromData(packet); - if (connAccept != null && netPeer.ProcessConnectAccept(connAccept)) - CreateEvent(NetEvent.EType.Connect, netPeer); - break; - default: - if(peerFound) - netPeer.ProcessPacket(packet); - else - SendRawAndRecycle(PoolGetWithProperty(PacketProperty.PeerNotFound), remoteEndPoint); - break; - } - } - - internal void CreateReceiveEvent(NetPacket packet, DeliveryMethod method, byte channelNumber, int headerSize, NetPeer fromPeer) - { - NetEvent evt; - - if (UnsyncedEvents || UnsyncedReceiveEvent || _manualMode) - { - lock (_eventLock) - { - evt = _netEventPoolHead; - if (evt == null) - evt = new NetEvent(this); - else - _netEventPoolHead = evt.Next; - } - evt.Next = null; - evt.Type = NetEvent.EType.Receive; - evt.DataReader.SetSource(packet, headerSize); - evt.Peer = fromPeer; - evt.DeliveryMethod = method; - evt.ChannelNumber = channelNumber; - ProcessEvent(evt); - } - else - { - lock (_eventLock) - { - evt = _netEventPoolHead; - if (evt == null) - evt = new NetEvent(this); - else - _netEventPoolHead = evt.Next; - - evt.Next = null; - evt.Type = NetEvent.EType.Receive; - evt.DataReader.SetSource(packet, headerSize); - evt.Peer = fromPeer; - evt.DeliveryMethod = method; - evt.ChannelNumber = channelNumber; - - if (_pendingEventTail == null) - _pendingEventHead = evt; - else - _pendingEventTail.Next = evt; - _pendingEventTail = evt; - } - } - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// DataWriter with data - /// Send options (reliable, unreliable, etc.) - public void SendToAll(NetDataWriter writer, DeliveryMethod options) - { - SendToAll(writer.Data, 0, writer.Length, options); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, DeliveryMethod options) - { - SendToAll(data, 0, data.Length, options); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Start of data - /// Length of data - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, int start, int length, DeliveryMethod options) - { - SendToAll(data, start, length, 0, options); - } - - /// - /// Send data to all connected peers - /// - /// DataWriter with data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options) - { - SendToAll(writer.Data, 0, writer.Length, channelNumber, options); - } - - /// - /// Send data to all connected peers - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options) - { - SendToAll(data, 0, data.Length, channelNumber, options); - } - - /// - /// Send data to all connected peers - /// - /// Data - /// Start of data - /// Length of data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options) - { - try - { - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - netPeer.Send(data, start, length, channelNumber, options); - } - finally - { - _peersLock.ExitReadLock(); - } - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// DataWriter with data - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(NetDataWriter writer, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(writer.Data, 0, writer.Length, 0, options, excludePeer); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(byte[] data, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(data, 0, data.Length, 0, options, excludePeer); - } - - /// - /// Send data to all connected peers (channel - 0) - /// - /// Data - /// Start of data - /// Length of data - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(byte[] data, int start, int length, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(data, start, length, 0, options, excludePeer); - } - - /// - /// Send data to all connected peers - /// - /// DataWriter with data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(NetDataWriter writer, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(writer.Data, 0, writer.Length, channelNumber, options, excludePeer); - } - - /// - /// Send data to all connected peers - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(byte[] data, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) - { - SendToAll(data, 0, data.Length, channelNumber, options, excludePeer); - } - - - /// - /// Send data to all connected peers - /// - /// Data - /// Start of data - /// Length of data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// Excluded peer - public void SendToAll(byte[] data, int start, int length, byte channelNumber, DeliveryMethod options, NetPeer excludePeer) - { - try - { - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if (netPeer != excludePeer) - netPeer.Send(data, start, length, channelNumber, options); - } - } - finally - { - _peersLock.ExitReadLock(); - } - } - - /// - /// Start logic thread and listening on available port - /// - public bool Start() - { - return Start(0); - } - - /// - /// Start logic thread and listening on selected port - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool Start(IPAddress addressIPv4, IPAddress addressIPv6, int port) - { - return Start(addressIPv4, addressIPv6, port, false); - } - - /// - /// Start logic thread and listening on selected port - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool Start(string addressIPv4, string addressIPv6, int port) - { - IPAddress ipv4 = NetUtils.ResolveAddress(addressIPv4); - IPAddress ipv6 = NetUtils.ResolveAddress(addressIPv6); - return Start(ipv4, ipv6, port); - } - - /// - /// Start logic thread and listening on selected port - /// - /// port to listen - public bool Start(int port) - { - return Start(IPAddress.Any, IPAddress.IPv6Any, port); - } - - /// - /// Start in manual mode and listening on selected port - /// In this mode you should use ManualReceive (without PollEvents) for receive packets - /// and ManualUpdate(...) for update and send packets - /// This mode useful mostly for single-threaded servers - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool StartInManualMode(IPAddress addressIPv4, IPAddress addressIPv6, int port) - { - return Start(addressIPv4, addressIPv6, port, true); - } - - /// - /// Start in manual mode and listening on selected port - /// In this mode you should use ManualReceive (without PollEvents) for receive packets - /// and ManualUpdate(...) for update and send packets - /// This mode useful mostly for single-threaded servers - /// - /// bind to specific ipv4 address - /// bind to specific ipv6 address - /// port to listen - public bool StartInManualMode(string addressIPv4, string addressIPv6, int port) - { - IPAddress ipv4 = NetUtils.ResolveAddress(addressIPv4); - IPAddress ipv6 = NetUtils.ResolveAddress(addressIPv6); - return StartInManualMode(ipv4, ipv6, port); - } - - /// - /// Start in manual mode and listening on selected port - /// In this mode you should use ManualReceive (without PollEvents) for receive packets - /// and ManualUpdate(...) for update and send packets - /// This mode useful mostly for single-threaded servers - /// - /// port to listen - public bool StartInManualMode(int port) - { - return StartInManualMode(IPAddress.Any, IPAddress.IPv6Any, port); - } - - /// - /// Send message without connection - /// - /// Raw data - /// Packet destination - /// Operation result - public bool SendUnconnectedMessage(byte[] message, IPEndPoint remoteEndPoint) - { - return SendUnconnectedMessage(message, 0, message.Length, remoteEndPoint); - } - - /// - /// Send message without connection. WARNING This method allocates a new IPEndPoint object and - /// synchronously makes a DNS request. If you're calling this method every frame it will be - /// much faster to just cache the IPEndPoint. - /// - /// Data serializer - /// Packet destination IP or hostname - /// Packet destination port - /// Operation result - public bool SendUnconnectedMessage(NetDataWriter writer, string address, int port) - { - IPEndPoint remoteEndPoint = NetUtils.MakeEndPoint(address, port); - - return SendUnconnectedMessage(writer.Data, 0, writer.Length, remoteEndPoint); - } - - /// - /// Send message without connection - /// - /// Data serializer - /// Packet destination - /// Operation result - public bool SendUnconnectedMessage(NetDataWriter writer, IPEndPoint remoteEndPoint) - { - return SendUnconnectedMessage(writer.Data, 0, writer.Length, remoteEndPoint); - } - - /// - /// Send message without connection - /// - /// Raw data - /// data start - /// data length - /// Packet destination - /// Operation result - public bool SendUnconnectedMessage(byte[] message, int start, int length, IPEndPoint remoteEndPoint) - { - //No need for CRC here, SendRaw does that - NetPacket packet = PoolGetWithData(PacketProperty.UnconnectedMessage, message, start, length); - return SendRawAndRecycle(packet, remoteEndPoint) > 0; - } - - /// - /// Triggers update and send logic immediately (works asynchronously) - /// - public void TriggerUpdate() - { - _updateTriggerEvent.Set(); - } - - /// - /// Receive all pending events. Call this in game update code - /// In Manual mode it will call also socket Receive (which can be slow) - /// - public void PollEvents() - { - if (_manualMode) - { - if (_udpSocketv4 != null) - ManualReceive(_udpSocketv4, _bufferEndPointv4); - if (_udpSocketv6 != null && _udpSocketv6 != _udpSocketv4) - ManualReceive(_udpSocketv6, _bufferEndPointv6); - ProcessDelayedPackets(); - return; - } - if (UnsyncedEvents) - return; - NetEvent pendingEvent; - lock (_eventLock) - { - pendingEvent = _pendingEventHead; - _pendingEventHead = null; - _pendingEventTail = null; - } - - while (pendingEvent != null) - { - var next = pendingEvent.Next; - ProcessEvent(pendingEvent); - pendingEvent = next; - } - } - - /// - /// Connect to remote host - /// - /// Server IP or hostname - /// Server Port - /// Connection key - /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting - /// Manager is not running. Call - public NetPeer Connect(string address, int port, string key) - { - return Connect(address, port, NetDataWriter.FromString(key)); - } - - /// - /// Connect to remote host - /// - /// Server IP or hostname - /// Server Port - /// Additional data for remote peer - /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting - /// Manager is not running. Call - public NetPeer Connect(string address, int port, NetDataWriter connectionData) - { - IPEndPoint ep; - try - { - ep = NetUtils.MakeEndPoint(address, port); - } - catch - { - CreateEvent(NetEvent.EType.Disconnect, disconnectReason: DisconnectReason.UnknownHost); - return null; - } - return Connect(ep, connectionData); - } - - /// - /// Connect to remote host - /// - /// Server end point (ip and port) - /// Connection key - /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting - /// Manager is not running. Call - public NetPeer Connect(IPEndPoint target, string key) - { - return Connect(target, NetDataWriter.FromString(key)); - } - - /// - /// Connect to remote host - /// - /// Server end point (ip and port) - /// Additional data for remote peer - /// New NetPeer if new connection, Old NetPeer if already connected, null peer if there is ConnectionRequest awaiting - /// Manager is not running. Call - public NetPeer Connect(IPEndPoint target, NetDataWriter connectionData) - { - if (!IsRunning) - throw new InvalidOperationException("Client is not running"); - - lock(_requestsDict) - { - if (_requestsDict.ContainsKey(target)) - return null; - } - - byte connectionNumber = 0; - _peersLock.EnterUpgradeableReadLock(); - if (_peersDict.TryGetValue(target, out var peer)) - { - switch (peer.ConnectionState) - { - //just return already connected peer - case ConnectionState.Connected: - case ConnectionState.Outgoing: - _peersLock.ExitUpgradeableReadLock(); - return peer; - } - //else reconnect - connectionNumber = (byte)((peer.ConnectionNum + 1) % NetConstants.MaxConnectionNumber); - RemovePeer(peer); - } - - //Create reliable connection - //And send connection request - peer = new NetPeer(this, target, GetNextPeerId(), connectionNumber, connectionData); - AddPeer(peer); - _peersLock.ExitUpgradeableReadLock(); - - return peer; - } - - /// - /// Force closes connection and stop all threads. - /// - public void Stop() - { - Stop(true); - } - - /// - /// Force closes connection and stop all threads. - /// - /// Send disconnect messages - public void Stop(bool sendDisconnectMessages) - { - if (!IsRunning) - return; - NetDebug.Write("[NM] Stop"); - -#if UNITY_2018_3_OR_NEWER - _pausedSocketFix.Deinitialize(); - _pausedSocketFix = null; -#endif - - //Send last disconnect - for(var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - netPeer.Shutdown(null, 0, 0, !sendDisconnectMessages); - - //Stop - CloseSocket(); - _updateTriggerEvent.Set(); - if (!_manualMode) - { - _logicThread.Join(); - _logicThread = null; - } - - //clear peers - _peersLock.EnterWriteLock(); - _headPeer = null; - _peersDict.Clear(); - _peersArray = new NetPeer[32]; - _peersLock.ExitWriteLock(); - _peerIds = new ConcurrentQueue(); - _lastPeerId = 0; -#if LITENETLIB_DEBUGGING - lock (_pingSimulationList) - _pingSimulationList.Clear(); -#endif - _connectedPeersCount = 0; - _pendingEventHead = null; - _pendingEventTail = null; - } - - /// - /// Return peers count with connection state - /// - /// peer connection state (you can use as bit flags) - /// peers count - public int GetPeersCount(ConnectionState peerState) - { - int count = 0; - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if ((netPeer.ConnectionState & peerState) != 0) - count++; - } - _peersLock.ExitReadLock(); - return count; - } - - /// - /// Get copy of peers (without allocations) - /// - /// List that will contain result - /// State of peers - public void GetPeersNonAlloc(List peers, ConnectionState peerState) - { - peers.Clear(); - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - if ((netPeer.ConnectionState & peerState) != 0) - peers.Add(netPeer); - } - _peersLock.ExitReadLock(); - } - - /// - /// Disconnect all peers without any additional data - /// - public void DisconnectAll() - { - DisconnectAll(null, 0, 0); - } - - /// - /// Disconnect all peers with shutdown message - /// - /// Data to send (must be less or equal MTU) - /// Data start - /// Data count - public void DisconnectAll(byte[] data, int start, int count) - { - //Send disconnect packets - _peersLock.EnterReadLock(); - for (var netPeer = _headPeer; netPeer != null; netPeer = netPeer.NextPeer) - { - DisconnectPeer( - netPeer, - DisconnectReason.DisconnectPeerCalled, - 0, - false, - data, - start, - count, - null); - } - _peersLock.ExitReadLock(); - } - - /// - /// Immediately disconnect peer from server without additional data - /// - /// peer to disconnect - public void DisconnectPeerForce(NetPeer peer) - { - DisconnectPeerForce(peer, DisconnectReason.DisconnectPeerCalled, 0, null); - } - - /// - /// Disconnect peer from server - /// - /// peer to disconnect - public void DisconnectPeer(NetPeer peer) - { - DisconnectPeer(peer, null, 0, 0); - } - - /// - /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - /// - /// peer to disconnect - /// additional data - public void DisconnectPeer(NetPeer peer, byte[] data) - { - DisconnectPeer(peer, data, 0, data.Length); - } - - /// - /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - /// - /// peer to disconnect - /// additional data - public void DisconnectPeer(NetPeer peer, NetDataWriter writer) - { - DisconnectPeer(peer, writer.Data, 0, writer.Length); - } - - /// - /// Disconnect peer from server and send additional data (Size must be less or equal MTU - 8) - /// - /// peer to disconnect - /// additional data - /// data start - /// data length - public void DisconnectPeer(NetPeer peer, byte[] data, int start, int count) - { - DisconnectPeer( - peer, - DisconnectReason.DisconnectPeerCalled, - 0, - false, - data, - start, - count, - null); - } - - /// - /// Create the requests for NTP server - /// - /// NTP Server address. - public void CreateNtpRequest(IPEndPoint endPoint) - { - _ntpRequests.Add(endPoint, new NtpRequest(endPoint)); - } - - /// - /// Create the requests for NTP server - /// - /// NTP Server address. - /// port - public void CreateNtpRequest(string ntpServerAddress, int port) - { - IPEndPoint endPoint = NetUtils.MakeEndPoint(ntpServerAddress, port); - _ntpRequests.Add(endPoint, new NtpRequest(endPoint)); - } - - /// - /// Create the requests for NTP server (default port) - /// - /// NTP Server address. - public void CreateNtpRequest(string ntpServerAddress) - { - IPEndPoint endPoint = NetUtils.MakeEndPoint(ntpServerAddress, NtpRequest.DefaultPort); - _ntpRequests.Add(endPoint, new NtpRequest(endPoint)); - } - - public NetPeerEnumerator GetEnumerator() - { - return new NetPeerEnumerator(_headPeer); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new NetPeerEnumerator(_headPeer); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return new NetPeerEnumerator(_headPeer); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs.meta deleted file mode 100644 index 921a22c..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ecc95cc1990e9e72eb034f41bb178b5c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs deleted file mode 100644 index 4b40308..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - internal enum PacketProperty : byte - { - Unreliable, - Channeled, - Ack, - Ping, - Pong, - ConnectRequest, - ConnectAccept, - Disconnect, - UnconnectedMessage, - MtuCheck, - MtuOk, - Broadcast, - Merged, - ShutdownOk, - PeerNotFound, - InvalidProtocol, - NatMessage, - Empty - } - - internal sealed class NetPacket - { - private static readonly int PropertiesCount = Enum.GetValues(typeof(PacketProperty)).Length; - private static readonly int[] HeaderSizes; - - static NetPacket() - { - HeaderSizes = NetUtils.AllocatePinnedUninitializedArray(PropertiesCount); - for (int i = 0; i < HeaderSizes.Length; i++) - { - switch ((PacketProperty)i) - { - case PacketProperty.Channeled: - case PacketProperty.Ack: - HeaderSizes[i] = NetConstants.ChanneledHeaderSize; - break; - case PacketProperty.Ping: - HeaderSizes[i] = NetConstants.HeaderSize + 2; - break; - case PacketProperty.ConnectRequest: - HeaderSizes[i] = NetConnectRequestPacket.HeaderSize; - break; - case PacketProperty.ConnectAccept: - HeaderSizes[i] = NetConnectAcceptPacket.Size; - break; - case PacketProperty.Disconnect: - HeaderSizes[i] = NetConstants.HeaderSize + 8; - break; - case PacketProperty.Pong: - HeaderSizes[i] = NetConstants.HeaderSize + 10; - break; - default: - HeaderSizes[i] = NetConstants.HeaderSize; - break; - } - } - } - - //Header - public PacketProperty Property - { - get => (PacketProperty)(RawData[0] & 0x1F); - set => RawData[0] = (byte)((RawData[0] & 0xE0) | (byte)value); - } - - public byte ConnectionNumber - { - get => (byte)((RawData[0] & 0x60) >> 5); - set => RawData[0] = (byte) ((RawData[0] & 0x9F) | (value << 5)); - } - - public ushort Sequence - { - get => BitConverter.ToUInt16(RawData, 1); - set => FastBitConverter.GetBytes(RawData, 1, value); - } - - public bool IsFragmented => (RawData[0] & 0x80) != 0; - - public void MarkFragmented() - { - RawData[0] |= 0x80; //set first bit - } - - public byte ChannelId - { - get => RawData[3]; - set => RawData[3] = value; - } - - public ushort FragmentId - { - get => BitConverter.ToUInt16(RawData, 4); - set => FastBitConverter.GetBytes(RawData, 4, value); - } - - public ushort FragmentPart - { - get => BitConverter.ToUInt16(RawData, 6); - set => FastBitConverter.GetBytes(RawData, 6, value); - } - - public ushort FragmentsTotal - { - get => BitConverter.ToUInt16(RawData, 8); - set => FastBitConverter.GetBytes(RawData, 8, value); - } - - //Data - public byte[] RawData; - public int Size; - - //Delivery - public object UserData; - - //Pool node - public NetPacket Next; - - public NetPacket(int size) - { - RawData = new byte[size]; - Size = size; - } - - public NetPacket(PacketProperty property, int size) - { - size += GetHeaderSize(property); - RawData = new byte[size]; - Property = property; - Size = size; - } - - public static int GetHeaderSize(PacketProperty property) - { - return HeaderSizes[(int)property]; - } - - public int GetHeaderSize() - { - return HeaderSizes[RawData[0] & 0x1F]; - } - - public bool Verify() - { - byte property = (byte)(RawData[0] & 0x1F); - if (property >= PropertiesCount) - return false; - int headerSize = HeaderSizes[property]; - bool fragmented = (RawData[0] & 0x80) != 0; - return Size >= headerSize && (!fragmented || Size >= headerSize + NetConstants.FragmentHeaderSize); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs.meta deleted file mode 100644 index 05991c1..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPacket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: c4abeec1dd82bf35faaab590069c424d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs deleted file mode 100644 index 9ae91ed..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs +++ /dev/null @@ -1,1395 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Net; -using System.Threading; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - /// - /// Peer connection state - /// - [Flags] - public enum ConnectionState : byte - { - Outgoing = 1 << 1, - Connected = 1 << 2, - ShutdownRequested = 1 << 3, - Disconnected = 1 << 4, - EndPointChange = 1 << 5, - Any = Outgoing | Connected | ShutdownRequested | EndPointChange - } - - internal enum ConnectRequestResult - { - None, - P2PLose, //when peer connecting - Reconnection, //when peer was connected - NewConnection //when peer was disconnected - } - - internal enum DisconnectResult - { - None, - Reject, - Disconnect - } - - internal enum ShutdownResult - { - None, - Success, - WasConnected - } - - /// - /// Network peer. Main purpose is sending messages to specific peer. - /// - public class NetPeer - { - //Ping and RTT - private int _rtt; - private int _avgRtt; - private int _rttCount; - private double _resendDelay = 27.0; - private int _pingSendTimer; - private int _rttResetTimer; - private readonly Stopwatch _pingTimer = new Stopwatch(); - private int _timeSinceLastPacket; - private long _remoteDelta; - - //Common - private readonly object _shutdownLock = new object(); - - internal volatile NetPeer NextPeer; - internal NetPeer PrevPeer; - - internal byte ConnectionNum - { - get => _connectNum; - private set - { - _connectNum = value; - _mergeData.ConnectionNumber = value; - _pingPacket.ConnectionNumber = value; - _pongPacket.ConnectionNumber = value; - } - } - - //Channels - private readonly Queue _unreliableChannel; - private readonly ConcurrentQueue _channelSendQueue; - private readonly BaseChannel[] _channels; - - //MTU - private int _mtu; - private int _mtuIdx; - private bool _finishMtu; - private int _mtuCheckTimer; - private int _mtuCheckAttempts; - private const int MtuCheckDelay = 1000; - private const int MaxMtuCheckAttempts = 4; - private readonly object _mtuMutex = new object(); - - //Fragment - private class IncomingFragments - { - public NetPacket[] Fragments; - public int ReceivedCount; - public int TotalSize; - public byte ChannelId; - } - private int _fragmentId; - private readonly Dictionary _holdedFragments; - private readonly Dictionary _deliveredFragments; - - //Merging - private readonly NetPacket _mergeData; - private int _mergePos; - private int _mergeCount; - - //Connection - private IPEndPoint _remoteEndPoint; - private int _connectAttempts; - private int _connectTimer; - private long _connectTime; - private byte _connectNum; - private ConnectionState _connectionState; - private NetPacket _shutdownPacket; - private const int ShutdownDelay = 300; - private int _shutdownTimer; - private readonly NetPacket _pingPacket; - private readonly NetPacket _pongPacket; - private readonly NetPacket _connectRequestPacket; - private readonly NetPacket _connectAcceptPacket; - - /// - /// Peer ip address and port - /// - public IPEndPoint EndPoint => _remoteEndPoint; - - /// - /// Peer parent NetManager - /// - public readonly NetManager NetManager; - - /// - /// Current connection state - /// - public ConnectionState ConnectionState => _connectionState; - - /// - /// Connection time for internal purposes - /// - internal long ConnectTime => _connectTime; - - /// - /// Peer id can be used as key in your dictionary of peers - /// - public readonly int Id; - - /// - /// Id assigned from server - /// - public int RemoteId { get; private set; } - - /// - /// Current one-way ping (RTT/2) in milliseconds - /// - public int Ping => _avgRtt/2; - - /// - /// Round trip time in milliseconds - /// - public int RoundTripTime => _avgRtt; - - /// - /// Current MTU - Maximum Transfer Unit ( maximum udp packet size without fragmentation ) - /// - public int Mtu => _mtu; - - /// - /// Delta with remote time in ticks (not accurate) - /// positive - remote time > our time - /// - public long RemoteTimeDelta => _remoteDelta; - - /// - /// Remote UTC time (not accurate) - /// - public DateTime RemoteUtcTime => new DateTime(DateTime.UtcNow.Ticks + _remoteDelta); - - /// - /// Time since last packet received (including internal library packets) - /// - public int TimeSinceLastPacket => _timeSinceLastPacket; - - internal double ResendDelay => _resendDelay; - - /// - /// Application defined object containing data about the connection - /// - public object Tag; - - /// - /// Statistics of peer connection - /// - public readonly NetStatistics Statistics; - - //incoming connection constructor - internal NetPeer(NetManager netManager, IPEndPoint remoteEndPoint, int id) - { - Id = id; - Statistics = new NetStatistics(); - NetManager = netManager; - ResetMtu(); - _remoteEndPoint = remoteEndPoint; - _connectionState = ConnectionState.Connected; - _mergeData = new NetPacket(PacketProperty.Merged, NetConstants.MaxPacketSize); - _pongPacket = new NetPacket(PacketProperty.Pong, 0); - _pingPacket = new NetPacket(PacketProperty.Ping, 0) {Sequence = 1}; - - _unreliableChannel = new Queue(); - _holdedFragments = new Dictionary(); - _deliveredFragments = new Dictionary(); - - _channels = new BaseChannel[netManager.ChannelsCount * NetConstants.ChannelTypeCount]; - _channelSendQueue = new ConcurrentQueue(); - } - - internal void InitiateEndPointChange() - { - ResetMtu(); - _connectionState = ConnectionState.EndPointChange; - } - - internal void FinishEndPointChange(IPEndPoint newEndPoint) - { - if (_connectionState != ConnectionState.EndPointChange) - return; - _connectionState = ConnectionState.Connected; - _remoteEndPoint = newEndPoint; - } - - internal void ResetMtu() - { - _finishMtu = false; - if (NetManager.MtuOverride > 0) - OverrideMtu(NetManager.MtuOverride); - else if (NetManager.UseSafeMtu) - SetMtu(0); - else - SetMtu(1); - } - - private void SetMtu(int mtuIdx) - { - _mtuIdx = mtuIdx; - _mtu = NetConstants.PossibleMtu[mtuIdx] - NetManager.ExtraPacketSizeForLayer; - } - - private void OverrideMtu(int mtuValue) - { - _mtu = mtuValue; - _finishMtu = true; - } - - /// - /// Returns packets count in queue for reliable channel - /// - /// number of channel 0-63 - /// type of channel ReliableOrdered or ReliableUnordered - /// packets count in channel queue - public int GetPacketsCountInReliableQueue(byte channelNumber, bool ordered) - { - int idx = channelNumber * NetConstants.ChannelTypeCount + - (byte) (ordered ? DeliveryMethod.ReliableOrdered : DeliveryMethod.ReliableUnordered); - var channel = _channels[idx]; - return channel != null ? ((ReliableChannel)channel).PacketsInQueue : 0; - } - - /// - /// Create temporary packet (maximum size MTU - headerSize) to send later without additional copies - /// - /// Delivery method (reliable, unreliable, etc.) - /// Number of channel (from 0 to channelsCount - 1) - /// PooledPacket that you can use to write data starting from UserDataOffset - public PooledPacket CreatePacketFromPool(DeliveryMethod deliveryMethod, byte channelNumber) - { - //multithreaded variable - int mtu = _mtu; - var packet = NetManager.PoolGetPacket(mtu); - if (deliveryMethod == DeliveryMethod.Unreliable) - { - packet.Property = PacketProperty.Unreliable; - return new PooledPacket(packet, mtu, 0); - } - else - { - packet.Property = PacketProperty.Channeled; - return new PooledPacket(packet, mtu, (byte)(channelNumber * NetConstants.ChannelTypeCount + (byte)deliveryMethod)); - } - } - - /// - /// Sends pooled packet without data copy - /// - /// packet to send - /// size of user data you want to send - public void SendPooledPacket(PooledPacket packet, int userDataSize) - { - if (_connectionState != ConnectionState.Connected) - return; - packet._packet.Size = packet.UserDataOffset + userDataSize; - if (packet._packet.Property == PacketProperty.Channeled) - { - CreateChannel(packet._channelNumber).AddToQueue(packet._packet); - } - else - { - lock(_unreliableChannel) - _unreliableChannel.Enqueue(packet._packet); - } - } - - private BaseChannel CreateChannel(byte idx) - { - BaseChannel newChannel = _channels[idx]; - if (newChannel != null) - return newChannel; - switch ((DeliveryMethod)(idx % NetConstants.ChannelTypeCount)) - { - case DeliveryMethod.ReliableUnordered: - newChannel = new ReliableChannel(this, false, idx); - break; - case DeliveryMethod.Sequenced: - newChannel = new SequencedChannel(this, false, idx); - break; - case DeliveryMethod.ReliableOrdered: - newChannel = new ReliableChannel(this, true, idx); - break; - case DeliveryMethod.ReliableSequenced: - newChannel = new SequencedChannel(this, true, idx); - break; - } - BaseChannel prevChannel = Interlocked.CompareExchange(ref _channels[idx], newChannel, null); - if (prevChannel != null) - return prevChannel; - - return newChannel; - } - - //"Connect to" constructor - internal NetPeer(NetManager netManager, IPEndPoint remoteEndPoint, int id, byte connectNum, NetDataWriter connectData) - : this(netManager, remoteEndPoint, id) - { - _connectTime = DateTime.UtcNow.Ticks; - _connectionState = ConnectionState.Outgoing; - ConnectionNum = connectNum; - - //Make initial packet - _connectRequestPacket = NetConnectRequestPacket.Make(connectData, remoteEndPoint.Serialize(), _connectTime, id); - _connectRequestPacket.ConnectionNumber = connectNum; - - //Send request - NetManager.SendRaw(_connectRequestPacket, _remoteEndPoint); - - NetDebug.Write(NetLogLevel.Trace, $"[CC] ConnectId: {_connectTime}, ConnectNum: {connectNum}"); - } - - //"Accept" incoming constructor - internal NetPeer(NetManager netManager, ConnectionRequest request, int id) - : this(netManager, request.RemoteEndPoint, id) - { - _connectTime = request.InternalPacket.ConnectionTime; - ConnectionNum = request.InternalPacket.ConnectionNumber; - RemoteId = request.InternalPacket.PeerId; - - //Make initial packet - _connectAcceptPacket = NetConnectAcceptPacket.Make(_connectTime, ConnectionNum, id); - - //Make Connected - _connectionState = ConnectionState.Connected; - - //Send - NetManager.SendRaw(_connectAcceptPacket, _remoteEndPoint); - - NetDebug.Write(NetLogLevel.Trace, $"[CC] ConnectId: {_connectTime}"); - } - - //Reject - internal void Reject(NetConnectRequestPacket requestData, byte[] data, int start, int length) - { - _connectTime = requestData.ConnectionTime; - _connectNum = requestData.ConnectionNumber; - Shutdown(data, start, length, false); - } - - internal bool ProcessConnectAccept(NetConnectAcceptPacket packet) - { - if (_connectionState != ConnectionState.Outgoing) - return false; - - //check connection id - if (packet.ConnectionTime != _connectTime) - { - NetDebug.Write(NetLogLevel.Trace, $"[NC] Invalid connectId: {packet.ConnectionTime} != our({_connectTime})"); - return false; - } - //check connect num - ConnectionNum = packet.ConnectionNumber; - RemoteId = packet.PeerId; - - NetDebug.Write(NetLogLevel.Trace, "[NC] Received connection accept"); - Interlocked.Exchange(ref _timeSinceLastPacket, 0); - _connectionState = ConnectionState.Connected; - return true; - } - - /// - /// Gets maximum size of packet that will be not fragmented. - /// - /// Type of packet that you want send - /// size in bytes - public int GetMaxSinglePacketSize(DeliveryMethod options) - { - return _mtu - NetPacket.GetHeaderSize(options == DeliveryMethod.Unreliable ? PacketProperty.Unreliable : PacketProperty.Channeled); - } - - /// - /// Send data to peer with delivery event called - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// User data that will be received in DeliveryEvent - /// - /// If you trying to send unreliable packet type - /// - public void SendWithDeliveryEvent(byte[] data, byte channelNumber, DeliveryMethod deliveryMethod, object userData) - { - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); - SendInternal(data, 0, data.Length, channelNumber, deliveryMethod, userData); - } - - /// - /// Send data to peer with delivery event called - /// - /// Data - /// Start of data - /// Length of data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// User data that will be received in DeliveryEvent - /// - /// If you trying to send unreliable packet type - /// - public void SendWithDeliveryEvent(byte[] data, int start, int length, byte channelNumber, DeliveryMethod deliveryMethod, object userData) - { - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); - SendInternal(data, start, length, channelNumber, deliveryMethod, userData); - } - - /// - /// Send data to peer with delivery event called - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// User data that will be received in DeliveryEvent - /// - /// If you trying to send unreliable packet type - /// - public void SendWithDeliveryEvent(NetDataWriter dataWriter, byte channelNumber, DeliveryMethod deliveryMethod, object userData) - { - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); - SendInternal(dataWriter.Data, 0, dataWriter.Length, channelNumber, deliveryMethod, userData); - } - - /// - /// Send data to peer (channel - 0) - /// - /// Data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(byte[] data, DeliveryMethod deliveryMethod) - { - SendInternal(data, 0, data.Length, 0, deliveryMethod, null); - } - - /// - /// Send data to peer (channel - 0) - /// - /// DataWriter with data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(NetDataWriter dataWriter, DeliveryMethod deliveryMethod) - { - SendInternal(dataWriter.Data, 0, dataWriter.Length, 0, deliveryMethod, null); - } - - /// - /// Send data to peer (channel - 0) - /// - /// Data - /// Start of data - /// Length of data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(byte[] data, int start, int length, DeliveryMethod options) - { - SendInternal(data, start, length, 0, options, null); - } - - /// - /// Send data to peer - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(byte[] data, byte channelNumber, DeliveryMethod deliveryMethod) - { - SendInternal(data, 0, data.Length, channelNumber, deliveryMethod, null); - } - - /// - /// Send data to peer - /// - /// DataWriter with data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(NetDataWriter dataWriter, byte channelNumber, DeliveryMethod deliveryMethod) - { - SendInternal(dataWriter.Data, 0, dataWriter.Length, channelNumber, deliveryMethod, null); - } - - /// - /// Send data to peer - /// - /// Data - /// Start of data - /// Length of data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(byte[] data, int start, int length, byte channelNumber, DeliveryMethod deliveryMethod) - { - SendInternal(data, start, length, channelNumber, deliveryMethod, null); - } - - private void SendInternal( - byte[] data, - int start, - int length, - byte channelNumber, - DeliveryMethod deliveryMethod, - object userData) - { - if (_connectionState != ConnectionState.Connected || channelNumber >= _channels.Length) - return; - - //Select channel - PacketProperty property; - BaseChannel channel = null; - - if (deliveryMethod == DeliveryMethod.Unreliable) - { - property = PacketProperty.Unreliable; - } - else - { - property = PacketProperty.Channeled; - channel = CreateChannel((byte)(channelNumber * NetConstants.ChannelTypeCount + (byte)deliveryMethod)); - } - - //Prepare - NetDebug.Write("[RS]Packet: " + property); - - //Check fragmentation - int headerSize = NetPacket.GetHeaderSize(property); - //Save mtu for multithread - int mtu = _mtu; - if (length + headerSize > mtu) - { - //if cannot be fragmented - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new TooBigPacketException("Unreliable or ReliableSequenced packet size exceeded maximum of " + (mtu - headerSize) + " bytes, Check allowed size by GetMaxSinglePacketSize()"); - - int packetFullSize = mtu - headerSize; - int packetDataSize = packetFullSize - NetConstants.FragmentHeaderSize; - int totalPackets = length / packetDataSize + (length % packetDataSize == 0 ? 0 : 1); - - NetDebug.Write($@"FragmentSend: - MTU: {mtu} - headerSize: {headerSize} - packetFullSize: {packetFullSize} - packetDataSize: {packetDataSize} - totalPackets: {totalPackets}"); - - if (totalPackets > ushort.MaxValue) - throw new TooBigPacketException("Data was split in " + totalPackets + " fragments, which exceeds " + ushort.MaxValue); - - ushort currentFragmentId = (ushort)Interlocked.Increment(ref _fragmentId); - - for(ushort partIdx = 0; partIdx < totalPackets; partIdx++) - { - int sendLength = length > packetDataSize ? packetDataSize : length; - - NetPacket p = NetManager.PoolGetPacket(headerSize + sendLength + NetConstants.FragmentHeaderSize); - p.Property = property; - p.UserData = userData; - p.FragmentId = currentFragmentId; - p.FragmentPart = partIdx; - p.FragmentsTotal = (ushort)totalPackets; - p.MarkFragmented(); - - Buffer.BlockCopy(data, start + partIdx * packetDataSize, p.RawData, NetConstants.FragmentedHeaderTotalSize, sendLength); - channel.AddToQueue(p); - - length -= sendLength; - } - return; - } - - //Else just send - NetPacket packet = NetManager.PoolGetPacket(headerSize + length); - packet.Property = property; - Buffer.BlockCopy(data, start, packet.RawData, headerSize, length); - packet.UserData = userData; - - if (channel == null) //unreliable - { - lock(_unreliableChannel) - _unreliableChannel.Enqueue(packet); - } - else - { - channel.AddToQueue(packet); - } - } - -#if LITENETLIB_SPANS || NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1 || NETCOREAPP3_1 || NET5_0 || NETSTANDARD2_1 - /// - /// Send data to peer with delivery event called - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Delivery method (reliable, unreliable, etc.) - /// User data that will be received in DeliveryEvent - /// - /// If you trying to send unreliable packet type - /// - public void SendWithDeliveryEvent(ReadOnlySpan data, byte channelNumber, DeliveryMethod deliveryMethod, object userData) - { - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new ArgumentException("Delivery event will work only for ReliableOrdered/Unordered packets"); - SendInternal(data, channelNumber, deliveryMethod, userData); - } - - /// - /// Send data to peer (channel - 0) - /// - /// Data - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(ReadOnlySpan data, DeliveryMethod deliveryMethod) - { - SendInternal(data, 0, deliveryMethod, null); - } - - /// - /// Send data to peer - /// - /// Data - /// Number of channel (from 0 to channelsCount - 1) - /// Send options (reliable, unreliable, etc.) - /// - /// If size exceeds maximum limit: - /// MTU - headerSize bytes for Unreliable - /// Fragment count exceeded ushort.MaxValue - /// - public void Send(ReadOnlySpan data, byte channelNumber, DeliveryMethod deliveryMethod) - { - SendInternal(data, channelNumber, deliveryMethod, null); - } - - private void SendInternal( - ReadOnlySpan data, - byte channelNumber, - DeliveryMethod deliveryMethod, - object userData) - { - if (_connectionState != ConnectionState.Connected || channelNumber >= _channels.Length) - return; - - //Select channel - PacketProperty property; - BaseChannel channel = null; - - if (deliveryMethod == DeliveryMethod.Unreliable) - { - property = PacketProperty.Unreliable; - } - else - { - property = PacketProperty.Channeled; - channel = CreateChannel((byte)(channelNumber * NetConstants.ChannelTypeCount + (byte)deliveryMethod)); - } - - //Prepare - NetDebug.Write("[RS]Packet: " + property); - - //Check fragmentation - int headerSize = NetPacket.GetHeaderSize(property); - //Save mtu for multithread - int mtu = _mtu; - int length = data.Length; - if (length + headerSize > mtu) - { - //if cannot be fragmented - if (deliveryMethod != DeliveryMethod.ReliableOrdered && deliveryMethod != DeliveryMethod.ReliableUnordered) - throw new TooBigPacketException("Unreliable or ReliableSequenced packet size exceeded maximum of " + (mtu - headerSize) + " bytes, Check allowed size by GetMaxSinglePacketSize()"); - - int packetFullSize = mtu - headerSize; - int packetDataSize = packetFullSize - NetConstants.FragmentHeaderSize; - int totalPackets = length / packetDataSize + (length % packetDataSize == 0 ? 0 : 1); - - if (totalPackets > ushort.MaxValue) - throw new TooBigPacketException("Data was split in " + totalPackets + " fragments, which exceeds " + ushort.MaxValue); - - ushort currentFragmentId = (ushort)Interlocked.Increment(ref _fragmentId); - - for (ushort partIdx = 0; partIdx < totalPackets; partIdx++) - { - int sendLength = length > packetDataSize ? packetDataSize : length; - - NetPacket p = NetManager.PoolGetPacket(headerSize + sendLength + NetConstants.FragmentHeaderSize); - p.Property = property; - p.UserData = userData; - p.FragmentId = currentFragmentId; - p.FragmentPart = partIdx; - p.FragmentsTotal = (ushort)totalPackets; - p.MarkFragmented(); - - data.Slice(partIdx * packetDataSize, sendLength).CopyTo(new Span(p.RawData, NetConstants.FragmentedHeaderTotalSize, sendLength)); - channel.AddToQueue(p); - - length -= sendLength; - } - return; - } - - //Else just send - NetPacket packet = NetManager.PoolGetPacket(headerSize + length); - packet.Property = property; - data.CopyTo(new Span(packet.RawData, headerSize, length)); - packet.UserData = userData; - - if (channel == null) //unreliable - { - lock(_unreliableChannel) - _unreliableChannel.Enqueue(packet); - } - else - { - channel.AddToQueue(packet); - } - } -#endif - - public void Disconnect(byte[] data) - { - NetManager.DisconnectPeer(this, data); - } - - public void Disconnect(NetDataWriter writer) - { - NetManager.DisconnectPeer(this, writer); - } - - public void Disconnect(byte[] data, int start, int count) - { - NetManager.DisconnectPeer(this, data, start, count); - } - - public void Disconnect() - { - NetManager.DisconnectPeer(this); - } - - internal DisconnectResult ProcessDisconnect(NetPacket packet) - { - if ((_connectionState == ConnectionState.Connected || _connectionState == ConnectionState.Outgoing) && - packet.Size >= 9 && - BitConverter.ToInt64(packet.RawData, 1) == _connectTime && - packet.ConnectionNumber == _connectNum) - { - return _connectionState == ConnectionState.Connected - ? DisconnectResult.Disconnect - : DisconnectResult.Reject; - } - return DisconnectResult.None; - } - - internal void AddToReliableChannelSendQueue(BaseChannel channel) - { - _channelSendQueue.Enqueue(channel); - } - - internal ShutdownResult Shutdown(byte[] data, int start, int length, bool force) - { - lock (_shutdownLock) - { - //trying to shutdown already disconnected - if (_connectionState == ConnectionState.Disconnected || - _connectionState == ConnectionState.ShutdownRequested) - { - return ShutdownResult.None; - } - - var result = _connectionState == ConnectionState.Connected - ? ShutdownResult.WasConnected - : ShutdownResult.Success; - - //don't send anything - if (force) - { - _connectionState = ConnectionState.Disconnected; - return result; - } - - //reset time for reconnect protection - Interlocked.Exchange(ref _timeSinceLastPacket, 0); - - //send shutdown packet - _shutdownPacket = new NetPacket(PacketProperty.Disconnect, length) {ConnectionNumber = _connectNum}; - FastBitConverter.GetBytes(_shutdownPacket.RawData, 1, _connectTime); - if (_shutdownPacket.Size >= _mtu) - { - //Drop additional data - NetDebug.WriteError("[Peer] Disconnect additional data size more than MTU - 8!"); - } - else if (data != null && length > 0) - { - Buffer.BlockCopy(data, start, _shutdownPacket.RawData, 9, length); - } - _connectionState = ConnectionState.ShutdownRequested; - NetDebug.Write("[Peer] Send disconnect"); - NetManager.SendRaw(_shutdownPacket, _remoteEndPoint); - return result; - } - } - - private void UpdateRoundTripTime(int roundTripTime) - { - _rtt += roundTripTime; - _rttCount++; - _avgRtt = _rtt/_rttCount; - _resendDelay = 25.0 + _avgRtt * 2.1; // 25 ms + double rtt - } - - internal void AddReliablePacket(DeliveryMethod method, NetPacket p) - { - if (p.IsFragmented) - { - NetDebug.Write($"Fragment. Id: {p.FragmentId}, Part: {p.FragmentPart}, Total: {p.FragmentsTotal}"); - //Get needed array from dictionary - ushort packetFragId = p.FragmentId; - byte packetChannelId = p.ChannelId; - if (!_holdedFragments.TryGetValue(packetFragId, out var incomingFragments)) - { - incomingFragments = new IncomingFragments - { - Fragments = new NetPacket[p.FragmentsTotal], - ChannelId = p.ChannelId - }; - _holdedFragments.Add(packetFragId, incomingFragments); - } - - //Cache - var fragments = incomingFragments.Fragments; - - //Error check - if (p.FragmentPart >= fragments.Length || - fragments[p.FragmentPart] != null || - p.ChannelId != incomingFragments.ChannelId) - { - NetManager.PoolRecycle(p); - NetDebug.WriteError("Invalid fragment packet"); - return; - } - //Fill array - fragments[p.FragmentPart] = p; - - //Increase received fragments count - incomingFragments.ReceivedCount++; - - //Increase total size - incomingFragments.TotalSize += p.Size - NetConstants.FragmentedHeaderTotalSize; - - //Check for finish - if (incomingFragments.ReceivedCount != fragments.Length) - return; - - //just simple packet - NetPacket resultingPacket = NetManager.PoolGetPacket(incomingFragments.TotalSize); - - int pos = 0; - for (int i = 0; i < incomingFragments.ReceivedCount; i++) - { - var fragment = fragments[i]; - int writtenSize = fragment.Size - NetConstants.FragmentedHeaderTotalSize; - - if (pos+writtenSize > resultingPacket.RawData.Length) - { - _holdedFragments.Remove(packetFragId); - NetDebug.WriteError($"Fragment error pos: {pos + writtenSize} >= resultPacketSize: {resultingPacket.RawData.Length} , totalSize: {incomingFragments.TotalSize}"); - return; - } - if (fragment.Size > fragment.RawData.Length) - { - _holdedFragments.Remove(packetFragId); - NetDebug.WriteError($"Fragment error size: {fragment.Size} > fragment.RawData.Length: {fragment.RawData.Length}"); - return; - } - - //Create resulting big packet - Buffer.BlockCopy( - fragment.RawData, - NetConstants.FragmentedHeaderTotalSize, - resultingPacket.RawData, - pos, - writtenSize); - pos += writtenSize; - - //Free memory - NetManager.PoolRecycle(fragment); - fragments[i] = null; - } - - //Clear memory - _holdedFragments.Remove(packetFragId); - - //Send to process - NetManager.CreateReceiveEvent(resultingPacket, method, (byte)(packetChannelId / NetConstants.ChannelTypeCount), 0, this); - } - else //Just simple packet - { - NetManager.CreateReceiveEvent(p, method, (byte)(p.ChannelId / NetConstants.ChannelTypeCount), NetConstants.ChanneledHeaderSize, this); - } - } - - private void ProcessMtuPacket(NetPacket packet) - { - //header + int - if (packet.Size < NetConstants.PossibleMtu[0]) - return; - - //first stage check (mtu check and mtu ok) - int receivedMtu = BitConverter.ToInt32(packet.RawData, 1); - int endMtuCheck = BitConverter.ToInt32(packet.RawData, packet.Size - 4); - if (receivedMtu != packet.Size || receivedMtu != endMtuCheck || receivedMtu > NetConstants.MaxPacketSize) - { - NetDebug.WriteError($"[MTU] Broken packet. RMTU {receivedMtu}, EMTU {endMtuCheck}, PSIZE {packet.Size}"); - return; - } - - if (packet.Property == PacketProperty.MtuCheck) - { - _mtuCheckAttempts = 0; - NetDebug.Write("[MTU] check. send back: " + receivedMtu); - packet.Property = PacketProperty.MtuOk; - NetManager.SendRawAndRecycle(packet, _remoteEndPoint); - } - else if(receivedMtu > _mtu && !_finishMtu) //MtuOk - { - //invalid packet - if (receivedMtu != NetConstants.PossibleMtu[_mtuIdx + 1] - NetManager.ExtraPacketSizeForLayer) - return; - - lock (_mtuMutex) - { - SetMtu(_mtuIdx+1); - } - //if maxed - finish. - if (_mtuIdx == NetConstants.PossibleMtu.Length - 1) - _finishMtu = true; - NetManager.PoolRecycle(packet); - NetDebug.Write("[MTU] ok. Increase to: " + _mtu); - } - } - - private void UpdateMtuLogic(int deltaTime) - { - if (_finishMtu) - return; - - _mtuCheckTimer += deltaTime; - if (_mtuCheckTimer < MtuCheckDelay) - return; - - _mtuCheckTimer = 0; - _mtuCheckAttempts++; - if (_mtuCheckAttempts >= MaxMtuCheckAttempts) - { - _finishMtu = true; - return; - } - - lock (_mtuMutex) - { - if (_mtuIdx >= NetConstants.PossibleMtu.Length - 1) - return; - - //Send increased packet - int newMtu = NetConstants.PossibleMtu[_mtuIdx + 1] - NetManager.ExtraPacketSizeForLayer; - var p = NetManager.PoolGetPacket(newMtu); - p.Property = PacketProperty.MtuCheck; - FastBitConverter.GetBytes(p.RawData, 1, newMtu); //place into start - FastBitConverter.GetBytes(p.RawData, p.Size - 4, newMtu);//and end of packet - - //Must check result for MTU fix - if (NetManager.SendRawAndRecycle(p, _remoteEndPoint) <= 0) - _finishMtu = true; - } - } - - internal ConnectRequestResult ProcessConnectRequest(NetConnectRequestPacket connRequest) - { - //current or new request - switch (_connectionState) - { - //P2P case - case ConnectionState.Outgoing: - //fast check - if (connRequest.ConnectionTime < _connectTime) - { - return ConnectRequestResult.P2PLose; - } - //slow rare case check - if (connRequest.ConnectionTime == _connectTime) - { - var remoteBytes = _remoteEndPoint.Serialize(); - var localBytes = connRequest.TargetAddress; - for (int i = remoteBytes.Size-1; i >= 0; i--) - { - byte rb = remoteBytes[i]; - if (rb == localBytes[i]) - continue; - if (rb < localBytes[i]) - return ConnectRequestResult.P2PLose; - } - } - break; - - case ConnectionState.Connected: - //Old connect request - if (connRequest.ConnectionTime == _connectTime) - { - //just reply accept - NetManager.SendRaw(_connectAcceptPacket, _remoteEndPoint); - } - //New connect request - else if (connRequest.ConnectionTime > _connectTime) - { - return ConnectRequestResult.Reconnection; - } - break; - - case ConnectionState.Disconnected: - case ConnectionState.ShutdownRequested: - if (connRequest.ConnectionTime >= _connectTime) - return ConnectRequestResult.NewConnection; - break; - } - return ConnectRequestResult.None; - } - - //Process incoming packet - internal void ProcessPacket(NetPacket packet) - { - //not initialized - if (_connectionState == ConnectionState.Outgoing || _connectionState == ConnectionState.Disconnected) - { - NetManager.PoolRecycle(packet); - return; - } - if (packet.Property == PacketProperty.ShutdownOk) - { - if (_connectionState == ConnectionState.ShutdownRequested) - _connectionState = ConnectionState.Disconnected; - NetManager.PoolRecycle(packet); - return; - } - if (packet.ConnectionNumber != _connectNum) - { - NetDebug.Write(NetLogLevel.Trace, "[RR]Old packet"); - NetManager.PoolRecycle(packet); - return; - } - Interlocked.Exchange(ref _timeSinceLastPacket, 0); - - NetDebug.Write($"[RR]PacketProperty: {packet.Property}"); - switch (packet.Property) - { - case PacketProperty.Merged: - int pos = NetConstants.HeaderSize; - while (pos < packet.Size) - { - ushort size = BitConverter.ToUInt16(packet.RawData, pos); - pos += 2; - if (packet.RawData.Length - pos < size) - break; - - NetPacket mergedPacket = NetManager.PoolGetPacket(size); - Buffer.BlockCopy(packet.RawData, pos, mergedPacket.RawData, 0, size); - mergedPacket.Size = size; - - if (!mergedPacket.Verify()) - break; - - pos += size; - ProcessPacket(mergedPacket); - } - NetManager.PoolRecycle(packet); - break; - //If we get ping, send pong - case PacketProperty.Ping: - if (NetUtils.RelativeSequenceNumber(packet.Sequence, _pongPacket.Sequence) > 0) - { - NetDebug.Write("[PP]Ping receive, send pong"); - FastBitConverter.GetBytes(_pongPacket.RawData, 3, DateTime.UtcNow.Ticks); - _pongPacket.Sequence = packet.Sequence; - NetManager.SendRaw(_pongPacket, _remoteEndPoint); - } - NetManager.PoolRecycle(packet); - break; - - //If we get pong, calculate ping time and rtt - case PacketProperty.Pong: - if (packet.Sequence == _pingPacket.Sequence) - { - _pingTimer.Stop(); - int elapsedMs = (int)_pingTimer.ElapsedMilliseconds; - _remoteDelta = BitConverter.ToInt64(packet.RawData, 3) + (elapsedMs * TimeSpan.TicksPerMillisecond ) / 2 - DateTime.UtcNow.Ticks; - UpdateRoundTripTime(elapsedMs); - NetManager.ConnectionLatencyUpdated(this, elapsedMs / 2); - NetDebug.Write($"[PP]Ping: {packet.Sequence} - {elapsedMs} - {_remoteDelta}"); - } - NetManager.PoolRecycle(packet); - break; - - case PacketProperty.Ack: - case PacketProperty.Channeled: - if (packet.ChannelId > _channels.Length) - { - NetManager.PoolRecycle(packet); - break; - } - var channel = _channels[packet.ChannelId] ?? (packet.Property == PacketProperty.Ack ? null : CreateChannel(packet.ChannelId)); - if (channel != null) - { - if (!channel.ProcessPacket(packet)) - NetManager.PoolRecycle(packet); - } - break; - - //Simple packet without acks - case PacketProperty.Unreliable: - NetManager.CreateReceiveEvent(packet, DeliveryMethod.Unreliable, 0, NetConstants.HeaderSize, this); - return; - - case PacketProperty.MtuCheck: - case PacketProperty.MtuOk: - ProcessMtuPacket(packet); - break; - - default: - NetDebug.WriteError("Error! Unexpected packet type: " + packet.Property); - break; - } - } - - private void SendMerged() - { - if (_mergeCount == 0) - return; - int bytesSent; - if (_mergeCount > 1) - { - NetDebug.Write("[P]Send merged: " + _mergePos + ", count: " + _mergeCount); - bytesSent = NetManager.SendRaw(_mergeData.RawData, 0, NetConstants.HeaderSize + _mergePos, _remoteEndPoint); - } - else - { - //Send without length information and merging - bytesSent = NetManager.SendRaw(_mergeData.RawData, NetConstants.HeaderSize + 2, _mergePos - 2, _remoteEndPoint); - } - - if (NetManager.EnableStatistics) - { - Statistics.IncrementPacketsSent(); - Statistics.AddBytesSent(bytesSent); - } - - _mergePos = 0; - _mergeCount = 0; - } - - internal void SendUserData(NetPacket packet) - { - packet.ConnectionNumber = _connectNum; - int mergedPacketSize = NetConstants.HeaderSize + packet.Size + 2; - const int sizeTreshold = 20; - if (mergedPacketSize + sizeTreshold >= _mtu) - { - NetDebug.Write(NetLogLevel.Trace, "[P]SendingPacket: " + packet.Property); - int bytesSent = NetManager.SendRaw(packet, _remoteEndPoint); - - if (NetManager.EnableStatistics) - { - Statistics.IncrementPacketsSent(); - Statistics.AddBytesSent(bytesSent); - } - - return; - } - if (_mergePos + mergedPacketSize > _mtu) - SendMerged(); - - FastBitConverter.GetBytes(_mergeData.RawData, _mergePos + NetConstants.HeaderSize, (ushort)packet.Size); - Buffer.BlockCopy(packet.RawData, 0, _mergeData.RawData, _mergePos + NetConstants.HeaderSize + 2, packet.Size); - _mergePos += packet.Size + 2; - _mergeCount++; - //DebugWriteForce("Merged: " + _mergePos + "/" + (_mtu - 2) + ", count: " + _mergeCount); - } - - internal void Update(int deltaTime) - { - Interlocked.Add(ref _timeSinceLastPacket, deltaTime); - switch (_connectionState) - { - case ConnectionState.Connected: - if (_timeSinceLastPacket > NetManager.DisconnectTimeout) - { - NetDebug.Write($"[UPDATE] Disconnect by timeout: {_timeSinceLastPacket} > {NetManager.DisconnectTimeout}"); - NetManager.DisconnectPeerForce(this, DisconnectReason.Timeout, 0, null); - return; - } - break; - - case ConnectionState.ShutdownRequested: - if (_timeSinceLastPacket > NetManager.DisconnectTimeout) - { - _connectionState = ConnectionState.Disconnected; - } - else - { - _shutdownTimer += deltaTime; - if (_shutdownTimer >= ShutdownDelay) - { - _shutdownTimer = 0; - NetManager.SendRaw(_shutdownPacket, _remoteEndPoint); - } - } - return; - - case ConnectionState.Outgoing: - _connectTimer += deltaTime; - if (_connectTimer > NetManager.ReconnectDelay) - { - _connectTimer = 0; - _connectAttempts++; - if (_connectAttempts > NetManager.MaxConnectAttempts) - { - NetManager.DisconnectPeerForce(this, DisconnectReason.ConnectionFailed, 0, null); - return; - } - - //else send connect again - NetManager.SendRaw(_connectRequestPacket, _remoteEndPoint); - } - return; - - case ConnectionState.Disconnected: - return; - } - - //Send ping - _pingSendTimer += deltaTime; - if (_pingSendTimer >= NetManager.PingInterval) - { - NetDebug.Write("[PP] Send ping..."); - //reset timer - _pingSendTimer = 0; - //send ping - _pingPacket.Sequence++; - //ping timeout - if (_pingTimer.IsRunning) - UpdateRoundTripTime((int)_pingTimer.ElapsedMilliseconds); - _pingTimer.Restart(); - NetManager.SendRaw(_pingPacket, _remoteEndPoint); - } - - //RTT - round trip time - _rttResetTimer += deltaTime; - if (_rttResetTimer >= NetManager.PingInterval * 3) - { - _rttResetTimer = 0; - _rtt = _avgRtt; - _rttCount = 1; - } - - UpdateMtuLogic(deltaTime); - - //Pending send - int count = _channelSendQueue.Count; - while (count-- > 0) - { - if (!_channelSendQueue.TryDequeue(out var channel)) - break; - if (channel.SendAndCheckQueue()) - { - // still has something to send, re-add it to the send queue - _channelSendQueue.Enqueue(channel); - } - } - - lock (_unreliableChannel) - { - int unreliableCount = _unreliableChannel.Count; - for (int i = 0; i < unreliableCount; i++) - { - var packet = _unreliableChannel.Dequeue(); - SendUserData(packet); - NetManager.PoolRecycle(packet); - } - } - - SendMerged(); - } - - //For reliable channel - internal void RecycleAndDeliver(NetPacket packet) - { - if (packet.UserData != null) - { - if (packet.IsFragmented) - { - _deliveredFragments.TryGetValue(packet.FragmentId, out ushort fragCount); - fragCount++; - if (fragCount == packet.FragmentsTotal) - { - NetManager.MessageDelivered(this, packet.UserData); - _deliveredFragments.Remove(packet.FragmentId); - } - else - { - _deliveredFragments[packet.FragmentId] = fragCount; - } - } - else - { - NetManager.MessageDelivered(this, packet.UserData); - } - packet.UserData = null; - } - NetManager.PoolRecycle(packet); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs.meta deleted file mode 100644 index eee1406..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetPeer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 275317db1cf564734938a93ee4937011 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs deleted file mode 100644 index c369a0f..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using LiteNetLib.Utils; - -namespace LiteNetLib -{ - public sealed class NetStatistics - { - private long _packetsSent; - private long _packetsReceived; - private long _bytesSent; - private long _bytesReceived; - private long _packetLoss; - private readonly Dictionary _packetsWrittenByType = new Dictionary(NetPacketProcessor.PacketCount); - private readonly Dictionary _bytesWrittenByType = new Dictionary(NetPacketProcessor.PacketCount); - - public long PacketsSent => Interlocked.Read(ref _packetsSent); - public long PacketsReceived => Interlocked.Read(ref _packetsReceived); - public long BytesSent => Interlocked.Read(ref _bytesSent); - public long BytesReceived => Interlocked.Read(ref _bytesReceived); - public long PacketLoss => Interlocked.Read(ref _packetLoss); - public Dictionary PacketsWrittenByType => new Dictionary(_packetsWrittenByType); - public Dictionary BytesWrittenByType => new Dictionary(_bytesWrittenByType); - - public long PacketLossPercent { - get { - long sent = PacketsSent, loss = PacketLoss; - - return sent == 0 ? 0 : loss * 100 / sent; - } - } - - public void Reset() - { - Interlocked.Exchange(ref _packetsSent, 0); - Interlocked.Exchange(ref _packetsReceived, 0); - Interlocked.Exchange(ref _bytesSent, 0); - Interlocked.Exchange(ref _bytesReceived, 0); - Interlocked.Exchange(ref _packetLoss, 0); - _packetsWrittenByType.Clear(); - _bytesWrittenByType.Clear(); - } - - public void IncrementPacketsSent() - { - Interlocked.Increment(ref _packetsSent); - } - - public void IncrementPacketsReceived() - { - Interlocked.Increment(ref _packetsReceived); - } - - public void AddBytesSent(long bytesSent) - { - Interlocked.Add(ref _bytesSent, bytesSent); - } - - public void AddBytesReceived(long bytesReceived) - { - Interlocked.Add(ref _bytesReceived, bytesReceived); - } - - public void IncrementPacketLoss() - { - Interlocked.Increment(ref _packetLoss); - } - - public void AddPacketLoss(long packetLoss) - { - Interlocked.Add(ref _packetLoss, packetLoss); - } - - public void IncrementPacketsWritten(byte id) - { - if (_packetsWrittenByType.ContainsKey(id)) - _packetsWrittenByType[id]++; - else - _packetsWrittenByType[id] = 1; - } - - public void AddBytesWritten(byte id, int bytesWritten) - { - if (_bytesWrittenByType.ContainsKey(id)) - _bytesWrittenByType[id] += bytesWritten; - else - _bytesWrittenByType[id] = bytesWritten; - } - - public override string ToString() - { - return - string.Format( - "BytesReceived: {0}\nPacketsReceived: {1}\nBytesSent: {2}\nPacketsSent: {3}\nPacketLoss: {4}\nPacketLossPercent: {5}\n", - BytesReceived, - PacketsReceived, - BytesSent, - PacketsSent, - PacketLoss, - PacketLossPercent); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs.meta deleted file mode 100644 index 8407db6..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetStatistics.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9b449e5229fa4502fae05e714d8800f3 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs deleted file mode 100644 index f7b2bd8..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Net.Sockets; -using System.Net.NetworkInformation; - -namespace LiteNetLib -{ - /// - /// Address type that you want to receive from NetUtils.GetLocalIp method - /// - [Flags] - public enum LocalAddrType - { - IPv4 = 1, - IPv6 = 2, - All = IPv4 | IPv6 - } - - /// - /// Some specific network utilities - /// - public static class NetUtils - { - private static readonly NetworkSorter NetworkSorter = new NetworkSorter(); - - public static IPEndPoint MakeEndPoint(string hostStr, int port) - { - return new IPEndPoint(ResolveAddress(hostStr), port); - } - - public static IPAddress ResolveAddress(string hostStr) - { - if(hostStr == "localhost") - return IPAddress.Loopback; - - if (!IPAddress.TryParse(hostStr, out var ipAddress)) - { - if (NetManager.IPv6Support) - ipAddress = ResolveAddress(hostStr, AddressFamily.InterNetworkV6); - if (ipAddress == null) - ipAddress = ResolveAddress(hostStr, AddressFamily.InterNetwork); - } - if (ipAddress == null) - throw new ArgumentException("Invalid address: " + hostStr); - - return ipAddress; - } - - public static IPAddress ResolveAddress(string hostStr, AddressFamily addressFamily) - { - IPAddress[] addresses = Dns.GetHostEntry(hostStr).AddressList; - foreach (IPAddress ip in addresses) - { - if (ip.AddressFamily == addressFamily) - { - return ip; - } - } - return null; - } - - /// - /// Get all local ip addresses - /// - /// type of address (IPv4, IPv6 or both) - /// List with all local ip addresses - public static List GetLocalIpList(LocalAddrType addrType) - { - List targetList = new List(); - GetLocalIpList(targetList, addrType); - return targetList; - } - - /// - /// Get all local ip addresses (non alloc version) - /// - /// result list - /// type of address (IPv4, IPv6 or both) - public static void GetLocalIpList(IList targetList, LocalAddrType addrType) - { - bool ipv4 = (addrType & LocalAddrType.IPv4) == LocalAddrType.IPv4; - bool ipv6 = (addrType & LocalAddrType.IPv6) == LocalAddrType.IPv6; - try - { - // Sort networks interfaces so it prefer Wifi over Cellular networks - // Most cellulars networks seems to be incompatible with NAT Punch - var networks = NetworkInterface.GetAllNetworkInterfaces(); - Array.Sort(networks, NetworkSorter); - - foreach (NetworkInterface ni in networks) - { - //Skip loopback and disabled network interfaces - if (ni.NetworkInterfaceType == NetworkInterfaceType.Loopback || - ni.OperationalStatus != OperationalStatus.Up) - continue; - - var ipProps = ni.GetIPProperties(); - - //Skip address without gateway - if (ipProps.GatewayAddresses.Count == 0) - continue; - - foreach (UnicastIPAddressInformation ip in ipProps.UnicastAddresses) - { - var address = ip.Address; - if ((ipv4 && address.AddressFamily == AddressFamily.InterNetwork) || - (ipv6 && address.AddressFamily == AddressFamily.InterNetworkV6)) - targetList.Add(address.ToString()); - } - } - - //Fallback mode (unity android) - if (targetList.Count == 0) - { - IPAddress[] addresses = Dns.GetHostEntry(Dns.GetHostName()).AddressList; - foreach (IPAddress ip in addresses) - { - if((ipv4 && ip.AddressFamily == AddressFamily.InterNetwork) || - (ipv6 && ip.AddressFamily == AddressFamily.InterNetworkV6)) - targetList.Add(ip.ToString()); - } - } - } - catch - { - //ignored - } - - if (targetList.Count == 0) - { - if(ipv4) - targetList.Add("127.0.0.1"); - if(ipv6) - targetList.Add("::1"); - } - } - - private static readonly List IpList = new List(); - /// - /// Get first detected local ip address - /// - /// type of address (IPv4, IPv6 or both) - /// IP address if available. Else - string.Empty - public static string GetLocalIp(LocalAddrType addrType) - { - lock (IpList) - { - IpList.Clear(); - GetLocalIpList(IpList, addrType); - return IpList.Count == 0 ? string.Empty : IpList[0]; - } - } - - // =========================================== - // Internal and debug log related stuff - // =========================================== - internal static void PrintInterfaceInfos() - { - NetDebug.WriteForce(NetLogLevel.Info, $"IPv6Support: { NetManager.IPv6Support}"); - try - { - foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) - { - foreach (UnicastIPAddressInformation ip in ni.GetIPProperties().UnicastAddresses) - { - if (ip.Address.AddressFamily == AddressFamily.InterNetwork || - ip.Address.AddressFamily == AddressFamily.InterNetworkV6) - { - NetDebug.WriteForce( - NetLogLevel.Info, - $"Interface: {ni.Name}, Type: {ni.NetworkInterfaceType}, Ip: {ip.Address}, OpStatus: {ni.OperationalStatus}"); - } - } - } - } - catch (Exception e) - { - NetDebug.WriteForce(NetLogLevel.Info, $"Error while getting interface infos: {e}"); - } - } - - internal static int RelativeSequenceNumber(int number, int expected) - { - return (number - expected + NetConstants.MaxSequence + NetConstants.HalfMaxSequence) % NetConstants.MaxSequence - NetConstants.HalfMaxSequence; - } - - internal static T[] AllocatePinnedUninitializedArray(int count) where T : unmanaged - { -#if NET5_0_OR_GREATER || NET5_0 - return GC.AllocateUninitializedArray(count, true); -#else - return new T[count]; -#endif - } - } - - // Pick the most obvious choice for the local IP - // Ethernet > Wifi > Others > Cellular - internal class NetworkSorter : IComparer - { - [SuppressMessage("ReSharper", "PossibleNullReferenceException")] - public int Compare(NetworkInterface a, NetworkInterface b) - { - var isCellularA = a.NetworkInterfaceType == NetworkInterfaceType.Wman || - a.NetworkInterfaceType == NetworkInterfaceType.Wwanpp || - a.NetworkInterfaceType == NetworkInterfaceType.Wwanpp2; - - var isCellularB = b.NetworkInterfaceType == NetworkInterfaceType.Wman || - b.NetworkInterfaceType == NetworkInterfaceType.Wwanpp || - b.NetworkInterfaceType == NetworkInterfaceType.Wwanpp2; - - var isWifiA = a.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; - var isWifiB = b.NetworkInterfaceType == NetworkInterfaceType.Wireless80211; - - var isEthernetA = a.NetworkInterfaceType == NetworkInterfaceType.Ethernet || - a.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || - a.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet || - a.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || - a.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT; - - var isEthernetB = b.NetworkInterfaceType == NetworkInterfaceType.Ethernet || - b.NetworkInterfaceType == NetworkInterfaceType.Ethernet3Megabit || - b.NetworkInterfaceType == NetworkInterfaceType.GigabitEthernet || - b.NetworkInterfaceType == NetworkInterfaceType.FastEthernetFx || - b.NetworkInterfaceType == NetworkInterfaceType.FastEthernetT; - - var isOtherA = !isCellularA && !isWifiA && !isEthernetA; - var isOtherB = !isCellularB && !isWifiB && !isEthernetB; - - var priorityA = isEthernetA ? 3 : isWifiA ? 2 : isOtherA ? 1 : 0; - var priorityB = isEthernetB ? 3 : isWifiB ? 2 : isOtherB ? 1 : 0; - - return priorityA > priorityB ? -1 : priorityA < priorityB ? 1 : 0; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs.meta deleted file mode 100644 index a787891..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/NetUtils.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 10fc3a058b2de05d08a25aae8e958d1d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs deleted file mode 100644 index dc92122..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs +++ /dev/null @@ -1,57 +0,0 @@ -#if UNITY_2018_3_OR_NEWER -using System.Net; - -namespace LiteNetLib -{ - public class PausedSocketFix - { - private readonly NetManager _netManager; - private readonly IPAddress _ipv4; - private readonly IPAddress _ipv6; - private readonly int _port; - private readonly bool _manualMode; - private bool _initialized; - - public PausedSocketFix(NetManager netManager, IPAddress ipv4, IPAddress ipv6, int port, bool manualMode) - { - _netManager = netManager; - _ipv4 = ipv4; - _ipv6 = ipv6; - _port = port; - _manualMode = manualMode; - UnityEngine.Application.focusChanged += Application_focusChanged; - _initialized = true; - } - - public void Deinitialize() - { - if (_initialized) - UnityEngine.Application.focusChanged -= Application_focusChanged; - _initialized = false; - } - - private void Application_focusChanged(bool focused) - { - //If coming back into focus see if a reconnect is needed. - if (focused) - { - //try reconnect - if (!_initialized) - return; - //Was intentionally disconnected at some point. - if (!_netManager.IsRunning) - return; - //Socket is in working state. - if (_netManager.NotConnected == false) - return; - - //Socket isn't running but should be. Try to start again. - if (!_netManager.Start(_ipv4, _ipv6, _port, _manualMode)) - { - NetDebug.WriteError($"[S] Cannot restore connection. Ipv4 {_ipv4}, Ipv6 {_ipv6}, Port {_port}, ManualMode {_manualMode}"); - } - } - } - } -} -#endif diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs.meta deleted file mode 100644 index abdd2cf..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PausedSocketFix.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 1c1550912c878cb868a538c7be33778b -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs deleted file mode 100644 index 26ef7bd..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace LiteNetLib -{ - public readonly ref struct PooledPacket - { - internal readonly NetPacket _packet; - internal readonly byte _channelNumber; - - /// - /// Maximum data size that you can put into such packet - /// - public readonly int MaxUserDataSize; - - /// - /// Offset for user data when writing to Data array - /// - public readonly int UserDataOffset; - - /// - /// Raw packet data. Do not modify header! Use UserDataOffset as start point for your data - /// - public byte[] Data => _packet.RawData; - - internal PooledPacket(NetPacket packet, int maxDataSize, byte channelNumber) - { - _packet = packet; - UserDataOffset = _packet.GetHeaderSize(); - _packet.Size = UserDataOffset; - MaxUserDataSize = maxDataSize - UserDataOffset; - _channelNumber = channelNumber; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs.meta deleted file mode 100644 index a7adb24..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/PooledPacket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d76ffcb8ea1890343a4b835240331d42 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs deleted file mode 100644 index 4a10d17..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System; - -namespace LiteNetLib -{ - internal sealed class ReliableChannel : BaseChannel - { - private struct PendingPacket - { - private NetPacket _packet; - private long _timeStamp; - private bool _isSent; - - public override string ToString() - { - return _packet == null ? "Empty" : _packet.Sequence.ToString(); - } - - public void Init(NetPacket packet) - { - _packet = packet; - _isSent = false; - } - - //Returns true if there is a pending packet inside - public bool TrySend(long currentTime, NetPeer peer) - { - if (_packet == null) - return false; - - if (_isSent) //check send time - { - double resendDelay = peer.ResendDelay * TimeSpan.TicksPerMillisecond; - double packetHoldTime = currentTime - _timeStamp; - if (packetHoldTime < resendDelay) - return true; - NetDebug.Write($"[RC]Resend: {packetHoldTime} > {resendDelay}"); - } - _timeStamp = currentTime; - _isSent = true; - peer.SendUserData(_packet); - return true; - } - - public bool Clear(NetPeer peer) - { - if (_packet != null) - { - peer.RecycleAndDeliver(_packet); - _packet = null; - return true; - } - return false; - } - } - - private readonly NetPacket _outgoingAcks; //for send acks - private readonly PendingPacket[] _pendingPackets; //for unacked packets and duplicates - private readonly NetPacket[] _receivedPackets; //for order - private readonly bool[] _earlyReceived; //for unordered - - private int _localSeqence; - private int _remoteSequence; - private int _localWindowStart; - private int _remoteWindowStart; - - private bool _mustSendAcks; - - private readonly DeliveryMethod _deliveryMethod; - private readonly bool _ordered; - private readonly int _windowSize; - private const int BitsInByte = 8; - private readonly byte _id; - - public ReliableChannel(NetPeer peer, bool ordered, byte id) : base(peer) - { - _id = id; - _windowSize = NetConstants.DefaultWindowSize; - _ordered = ordered; - _pendingPackets = new PendingPacket[_windowSize]; - for (int i = 0; i < _pendingPackets.Length; i++) - _pendingPackets[i] = new PendingPacket(); - - if (_ordered) - { - _deliveryMethod = DeliveryMethod.ReliableOrdered; - _receivedPackets = new NetPacket[_windowSize]; - } - else - { - _deliveryMethod = DeliveryMethod.ReliableUnordered; - _earlyReceived = new bool[_windowSize]; - } - - _localWindowStart = 0; - _localSeqence = 0; - _remoteSequence = 0; - _remoteWindowStart = 0; - _outgoingAcks = new NetPacket(PacketProperty.Ack, (_windowSize - 1) / BitsInByte + 2) {ChannelId = id}; - } - - //ProcessAck in packet - private void ProcessAck(NetPacket packet) - { - if (packet.Size != _outgoingAcks.Size) - { - NetDebug.Write("[PA]Invalid acks packet size"); - return; - } - - ushort ackWindowStart = packet.Sequence; - int windowRel = NetUtils.RelativeSequenceNumber(_localWindowStart, ackWindowStart); - if (ackWindowStart >= NetConstants.MaxSequence || windowRel < 0) - { - NetDebug.Write("[PA]Bad window start"); - return; - } - - //check relevance - if (windowRel >= _windowSize) - { - NetDebug.Write("[PA]Old acks"); - return; - } - - byte[] acksData = packet.RawData; - lock (_pendingPackets) - { - for (int pendingSeq = _localWindowStart; - pendingSeq != _localSeqence; - pendingSeq = (pendingSeq + 1) % NetConstants.MaxSequence) - { - int rel = NetUtils.RelativeSequenceNumber(pendingSeq, ackWindowStart); - if (rel >= _windowSize) - { - NetDebug.Write("[PA]REL: " + rel); - break; - } - - int pendingIdx = pendingSeq % _windowSize; - int currentByte = NetConstants.ChanneledHeaderSize + pendingIdx / BitsInByte; - int currentBit = pendingIdx % BitsInByte; - if ((acksData[currentByte] & (1 << currentBit)) == 0) - { - if (Peer.NetManager.EnableStatistics) - { - Peer.Statistics.IncrementPacketLoss(); - Peer.NetManager.Statistics.IncrementPacketLoss(); - } - - //Skip false ack - NetDebug.Write($"[PA]False ack: {pendingSeq}"); - continue; - } - - if (pendingSeq == _localWindowStart) - { - //Move window - _localWindowStart = (_localWindowStart + 1) % NetConstants.MaxSequence; - } - - //clear packet - if (_pendingPackets[pendingIdx].Clear(Peer)) - NetDebug.Write($"[PA]Removing reliableInOrder ack: {pendingSeq} - true"); - } - } - } - - protected override bool SendNextPackets() - { - if (_mustSendAcks) - { - _mustSendAcks = false; - NetDebug.Write("[RR]SendAcks"); - lock(_outgoingAcks) - Peer.SendUserData(_outgoingAcks); - } - - long currentTime = DateTime.UtcNow.Ticks; - bool hasPendingPackets = false; - - lock (_pendingPackets) - { - //get packets from queue - lock (OutgoingQueue) - { - while (OutgoingQueue.Count > 0) - { - int relate = NetUtils.RelativeSequenceNumber(_localSeqence, _localWindowStart); - if (relate >= _windowSize) - break; - - var netPacket = OutgoingQueue.Dequeue(); - netPacket.Sequence = (ushort) _localSeqence; - netPacket.ChannelId = _id; - _pendingPackets[_localSeqence % _windowSize].Init(netPacket); - _localSeqence = (_localSeqence + 1) % NetConstants.MaxSequence; - } - } - - //send - for (int pendingSeq = _localWindowStart; pendingSeq != _localSeqence; pendingSeq = (pendingSeq + 1) % NetConstants.MaxSequence) - { - // Please note: TrySend is invoked on a mutable struct, it's important to not extract it into a variable here - if (_pendingPackets[pendingSeq % _windowSize].TrySend(currentTime, Peer)) - hasPendingPackets = true; - } - } - - return hasPendingPackets || _mustSendAcks || OutgoingQueue.Count > 0; - } - - //Process incoming packet - public override bool ProcessPacket(NetPacket packet) - { - if (packet.Property == PacketProperty.Ack) - { - ProcessAck(packet); - return false; - } - int seq = packet.Sequence; - if (seq >= NetConstants.MaxSequence) - { - NetDebug.Write("[RR]Bad sequence"); - return false; - } - - int relate = NetUtils.RelativeSequenceNumber(seq, _remoteWindowStart); - int relateSeq = NetUtils.RelativeSequenceNumber(seq, _remoteSequence); - - if (relateSeq > _windowSize) - { - NetDebug.Write("[RR]Bad sequence"); - return false; - } - - //Drop bad packets - if (relate < 0) - { - //Too old packet doesn't ack - NetDebug.Write("[RR]ReliableInOrder too old"); - return false; - } - if (relate >= _windowSize * 2) - { - //Some very new packet - NetDebug.Write("[RR]ReliableInOrder too new"); - return false; - } - - //If very new - move window - int ackIdx; - int ackByte; - int ackBit; - lock (_outgoingAcks) - { - if (relate >= _windowSize) - { - //New window position - int newWindowStart = (_remoteWindowStart + relate - _windowSize + 1) % NetConstants.MaxSequence; - _outgoingAcks.Sequence = (ushort) newWindowStart; - - //Clean old data - while (_remoteWindowStart != newWindowStart) - { - ackIdx = _remoteWindowStart % _windowSize; - ackByte = NetConstants.ChanneledHeaderSize + ackIdx / BitsInByte; - ackBit = ackIdx % BitsInByte; - _outgoingAcks.RawData[ackByte] &= (byte) ~(1 << ackBit); - _remoteWindowStart = (_remoteWindowStart + 1) % NetConstants.MaxSequence; - } - } - - //Final stage - process valid packet - //trigger acks send - _mustSendAcks = true; - - ackIdx = seq % _windowSize; - ackByte = NetConstants.ChanneledHeaderSize + ackIdx / BitsInByte; - ackBit = ackIdx % BitsInByte; - if ((_outgoingAcks.RawData[ackByte] & (1 << ackBit)) != 0) - { - NetDebug.Write("[RR]ReliableInOrder duplicate"); - //because _mustSendAcks == true - AddToPeerChannelSendQueue(); - return false; - } - - //save ack - _outgoingAcks.RawData[ackByte] |= (byte) (1 << ackBit); - } - - AddToPeerChannelSendQueue(); - - //detailed check - if (seq == _remoteSequence) - { - NetDebug.Write("[RR]ReliableInOrder packet succes"); - Peer.AddReliablePacket(_deliveryMethod, packet); - _remoteSequence = (_remoteSequence + 1) % NetConstants.MaxSequence; - - if (_ordered) - { - NetPacket p; - while ((p = _receivedPackets[_remoteSequence % _windowSize]) != null) - { - //process holden packet - _receivedPackets[_remoteSequence % _windowSize] = null; - Peer.AddReliablePacket(_deliveryMethod, p); - _remoteSequence = (_remoteSequence + 1) % NetConstants.MaxSequence; - } - } - else - { - while (_earlyReceived[_remoteSequence % _windowSize]) - { - //process early packet - _earlyReceived[_remoteSequence % _windowSize] = false; - _remoteSequence = (_remoteSequence + 1) % NetConstants.MaxSequence; - } - } - return true; - } - - //holden packet - if (_ordered) - { - _receivedPackets[ackIdx] = packet; - } - else - { - _earlyReceived[ackIdx] = true; - Peer.AddReliablePacket(_deliveryMethod, packet); - } - return true; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs.meta deleted file mode 100644 index 36ab889..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/ReliableChannel.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: df55950d9aab25fdc849b2f3e39cfc29 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs deleted file mode 100644 index bc47f86..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; - -namespace LiteNetLib -{ - internal sealed class SequencedChannel : BaseChannel - { - private int _localSequence; - private ushort _remoteSequence; - private readonly bool _reliable; - private NetPacket _lastPacket; - private readonly NetPacket _ackPacket; - private bool _mustSendAck; - private readonly byte _id; - private long _lastPacketSendTime; - - public SequencedChannel(NetPeer peer, bool reliable, byte id) : base(peer) - { - _id = id; - _reliable = reliable; - if (_reliable) - _ackPacket = new NetPacket(PacketProperty.Ack, 0) {ChannelId = id}; - } - - protected override bool SendNextPackets() - { - if (_reliable && OutgoingQueue.Count == 0) - { - long currentTime = DateTime.UtcNow.Ticks; - long packetHoldTime = currentTime - _lastPacketSendTime; - if (packetHoldTime >= Peer.ResendDelay * TimeSpan.TicksPerMillisecond) - { - var packet = _lastPacket; - if (packet != null) - { - _lastPacketSendTime = currentTime; - Peer.SendUserData(packet); - } - } - } - else - { - lock (OutgoingQueue) - { - while (OutgoingQueue.Count > 0) - { - NetPacket packet = OutgoingQueue.Dequeue(); - _localSequence = (_localSequence + 1) % NetConstants.MaxSequence; - packet.Sequence = (ushort)_localSequence; - packet.ChannelId = _id; - Peer.SendUserData(packet); - - if (_reliable && OutgoingQueue.Count == 0) - { - _lastPacketSendTime = DateTime.UtcNow.Ticks; - _lastPacket = packet; - } - else - { - Peer.NetManager.PoolRecycle(packet); - } - } - } - } - - if (_reliable && _mustSendAck) - { - _mustSendAck = false; - _ackPacket.Sequence = _remoteSequence; - Peer.SendUserData(_ackPacket); - } - - return _lastPacket != null; - } - - public override bool ProcessPacket(NetPacket packet) - { - if (packet.IsFragmented) - return false; - if (packet.Property == PacketProperty.Ack) - { - if (_reliable && _lastPacket != null && packet.Sequence == _lastPacket.Sequence) - _lastPacket = null; - return false; - } - int relative = NetUtils.RelativeSequenceNumber(packet.Sequence, _remoteSequence); - bool packetProcessed = false; - if (packet.Sequence < NetConstants.MaxSequence && relative > 0) - { - if (Peer.NetManager.EnableStatistics) - { - Peer.Statistics.AddPacketLoss(relative - 1); - Peer.NetManager.Statistics.AddPacketLoss(relative - 1); - } - - _remoteSequence = packet.Sequence; - Peer.NetManager.CreateReceiveEvent( - packet, - _reliable ? DeliveryMethod.ReliableSequenced : DeliveryMethod.Sequenced, - (byte)(packet.ChannelId / NetConstants.ChannelTypeCount), - NetConstants.ChanneledHeaderSize, - Peer); - packetProcessed = true; - } - - if (_reliable) - { - _mustSendAck = true; - AddToPeerChannelSendQueue(); - } - - return packetProcessed; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs.meta deleted file mode 100644 index 6516eaa..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/SequencedChannel.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 72093288874fee2cd959001aa3e3a6d4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils.meta deleted file mode 100644 index cbde705..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 81d3c59df7f54ac6b8789a9ca6bee25d -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs deleted file mode 100644 index 7e85680..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs +++ /dev/null @@ -1,150 +0,0 @@ -#if NETCOREAPP3_0_OR_GREATER || NETCOREAPP3_1 || NET5_0 -using System; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics.X86; -#endif -#if NET5_0_OR_GREATER || NET5_0 -using System.Runtime.Intrinsics.Arm; -#endif - -namespace LiteNetLib.Utils -{ - //Implementation from Crc32.NET - public static class CRC32C - { - public const int ChecksumSize = 4; - private const uint Poly = 0x82F63B78u; - private static readonly uint[] Table; - - static CRC32C() - { -#if NETCOREAPP3_0_OR_GREATER || NETCOREAPP3_1 || NET5_0 - if (Sse42.IsSupported) - return; -#endif -#if NET5_0_OR_GREATER || NET5_0 - if (Crc32.IsSupported) - return; -#endif - Table = NetUtils.AllocatePinnedUninitializedArray(16 * 256); - for (uint i = 0; i < 256; i++) - { - uint res = i; - for (int t = 0; t < 16; t++) - { - for (int k = 0; k < 8; k++) - res = (res & 1) == 1 ? Poly ^ (res >> 1) : (res >> 1); - Table[t * 256 + i] = res; - } - } - } - - /// - /// Compute CRC32C for data - /// - /// input data - /// offset - /// length - /// CRC32C checksum - public static uint Compute(byte[] input, int offset, int length) - { - uint crcLocal = uint.MaxValue; -#if NETCOREAPP3_0_OR_GREATER || NETCOREAPP3_1 || NET5_0 - if (Sse42.IsSupported) - { - var data = new ReadOnlySpan(input, offset, length); - int processed = 0; - if (Sse42.X64.IsSupported && data.Length > sizeof(ulong)) - { - processed = data.Length / sizeof(ulong) * sizeof(ulong); - var ulongs = MemoryMarshal.Cast(data.Slice(0, processed)); - ulong crclong = crcLocal; - for (int i = 0; i < ulongs.Length; i++) - { - crclong = Sse42.X64.Crc32(crclong, ulongs[i]); - } - - crcLocal = (uint)crclong; - } - else if (data.Length > sizeof(uint)) - { - processed = data.Length / sizeof(uint) * sizeof(uint); - var uints = MemoryMarshal.Cast(data.Slice(0, processed)); - for (int i = 0; i < uints.Length; i++) - { - crcLocal = Sse42.Crc32(crcLocal, uints[i]); - } - } - - for (int i = processed; i < data.Length; i++) - { - crcLocal = Sse42.Crc32(crcLocal, data[i]); - } - - return crcLocal ^ uint.MaxValue; - } -#endif -#if NET5_0_OR_GREATER || NET5_0 - if (Crc32.IsSupported) - { - var data = new ReadOnlySpan(input, offset, length); - int processed = 0; - if (Crc32.Arm64.IsSupported && data.Length > sizeof(ulong)) - { - processed = data.Length / sizeof(ulong) * sizeof(ulong); - var ulongs = MemoryMarshal.Cast(data.Slice(0, processed)); - for (int i = 0; i < ulongs.Length; i++) - { - crcLocal = Crc32.Arm64.ComputeCrc32C(crcLocal, ulongs[i]); - } - } - else if (data.Length > sizeof(uint)) - { - processed = data.Length / sizeof(uint) * sizeof(uint); - var uints = MemoryMarshal.Cast(data.Slice(0, processed)); - for (int i = 0; i < uints.Length; i++) - { - crcLocal = Crc32.ComputeCrc32C(crcLocal, uints[i]); - } - } - - for (int i = processed; i < data.Length; i++) - { - crcLocal = Crc32.ComputeCrc32C(crcLocal, data[i]); - } - - return crcLocal ^ uint.MaxValue; - } -#endif - while (length >= 16) - { - var a = Table[(3 * 256) + input[offset + 12]] - ^ Table[(2 * 256) + input[offset + 13]] - ^ Table[(1 * 256) + input[offset + 14]] - ^ Table[(0 * 256) + input[offset + 15]]; - - var b = Table[(7 * 256) + input[offset + 8]] - ^ Table[(6 * 256) + input[offset + 9]] - ^ Table[(5 * 256) + input[offset + 10]] - ^ Table[(4 * 256) + input[offset + 11]]; - - var c = Table[(11 * 256) + input[offset + 4]] - ^ Table[(10 * 256) + input[offset + 5]] - ^ Table[(9 * 256) + input[offset + 6]] - ^ Table[(8 * 256) + input[offset + 7]]; - - var d = Table[(15 * 256) + ((byte)crcLocal ^ input[offset])] - ^ Table[(14 * 256) + ((byte)(crcLocal >> 8) ^ input[offset + 1])] - ^ Table[(13 * 256) + ((byte)(crcLocal >> 16) ^ input[offset + 2])] - ^ Table[(12 * 256) + ((crcLocal >> 24) ^ input[offset + 3])]; - - crcLocal = d ^ c ^ b ^ a; - offset += 16; - length -= 16; - } - while (--length >= 0) - crcLocal = Table[(byte)(crcLocal ^ input[offset++])] ^ crcLocal >> 8; - return crcLocal ^ uint.MaxValue; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs.meta deleted file mode 100644 index aa0417e..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/CRC32C.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a28b2ade64d53b3a4a303a62169c4748 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs deleted file mode 100644 index 3ecd10c..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace LiteNetLib.Utils -{ - public static class FastBitConverter - { -#if (LITENETLIB_UNSAFE || LITENETLIB_UNSAFELIB || NETCOREAPP3_1 || NET5_0 || NETCOREAPP3_0_OR_GREATER) && !BIGENDIAN -#if LITENETLIB_UNSAFE - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static unsafe void GetBytes(byte[] bytes, int startIndex, T value) where T : unmanaged - { - int size = sizeof(T); - if (bytes.Length < startIndex + size) - ThrowIndexOutOfRangeException(); -#if LITENETLIB_UNSAFELIB || NETCOREAPP3_1 || NET5_0 || NETCOREAPP3_0_OR_GREATER - Unsafe.As(ref bytes[startIndex]) = value; -#else - fixed (byte* ptr = &bytes[startIndex]) - { -#if UNITY_ANDROID - // On some android systems, assigning *(T*)ptr throws a NRE if - // the ptr isn't aligned (i.e. if Position is 1,2,3,5, etc.). - // Here we have to use memcpy. - // - // => we can't get a pointer of a struct in C# without - // marshalling allocations - // => instead, we stack allocate an array of type T and use that - // => stackalloc avoids GC and is very fast. it only works for - // value types, but all blittable types are anyway. - T* valueBuffer = stackalloc T[1] { value }; - UnsafeUtility.MemCpy(ptr, valueBuffer, size); -#else - *(T*)ptr = value; -#endif - } -#endif - } -#else - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, T value) where T : unmanaged - { - if (bytes.Length < startIndex + Unsafe.SizeOf()) - ThrowIndexOutOfRangeException(); - Unsafe.As(ref bytes[startIndex]) = value; - } -#endif - - private static void ThrowIndexOutOfRangeException() => throw new IndexOutOfRangeException(); -#else - [StructLayout(LayoutKind.Explicit)] - private struct ConverterHelperDouble - { - [FieldOffset(0)] - public ulong Along; - - [FieldOffset(0)] - public double Adouble; - } - - [StructLayout(LayoutKind.Explicit)] - private struct ConverterHelperFloat - { - [FieldOffset(0)] - public int Aint; - - [FieldOffset(0)] - public float Afloat; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteLittleEndian(byte[] buffer, int offset, ulong data) - { -#if BIGENDIAN - buffer[offset + 7] = (byte)(data); - buffer[offset + 6] = (byte)(data >> 8); - buffer[offset + 5] = (byte)(data >> 16); - buffer[offset + 4] = (byte)(data >> 24); - buffer[offset + 3] = (byte)(data >> 32); - buffer[offset + 2] = (byte)(data >> 40); - buffer[offset + 1] = (byte)(data >> 48); - buffer[offset ] = (byte)(data >> 56); -#else - buffer[offset] = (byte)(data); - buffer[offset + 1] = (byte)(data >> 8); - buffer[offset + 2] = (byte)(data >> 16); - buffer[offset + 3] = (byte)(data >> 24); - buffer[offset + 4] = (byte)(data >> 32); - buffer[offset + 5] = (byte)(data >> 40); - buffer[offset + 6] = (byte)(data >> 48); - buffer[offset + 7] = (byte)(data >> 56); -#endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteLittleEndian(byte[] buffer, int offset, int data) - { -#if BIGENDIAN - buffer[offset + 3] = (byte)(data); - buffer[offset + 2] = (byte)(data >> 8); - buffer[offset + 1] = (byte)(data >> 16); - buffer[offset ] = (byte)(data >> 24); -#else - buffer[offset] = (byte)(data); - buffer[offset + 1] = (byte)(data >> 8); - buffer[offset + 2] = (byte)(data >> 16); - buffer[offset + 3] = (byte)(data >> 24); -#endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void WriteLittleEndian(byte[] buffer, int offset, short data) - { -#if BIGENDIAN - buffer[offset + 1] = (byte)(data); - buffer[offset ] = (byte)(data >> 8); -#else - buffer[offset] = (byte)(data); - buffer[offset + 1] = (byte)(data >> 8); -#endif - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, double value) - { - ConverterHelperDouble ch = new ConverterHelperDouble { Adouble = value }; - WriteLittleEndian(bytes, startIndex, ch.Along); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, float value) - { - ConverterHelperFloat ch = new ConverterHelperFloat { Afloat = value }; - WriteLittleEndian(bytes, startIndex, ch.Aint); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, short value) - { - WriteLittleEndian(bytes, startIndex, value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, ushort value) - { - WriteLittleEndian(bytes, startIndex, (short)value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, int value) - { - WriteLittleEndian(bytes, startIndex, value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, uint value) - { - WriteLittleEndian(bytes, startIndex, (int)value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, long value) - { - WriteLittleEndian(bytes, startIndex, (ulong)value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GetBytes(byte[] bytes, int startIndex, ulong value) - { - WriteLittleEndian(bytes, startIndex, value); - } -#endif - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs.meta deleted file mode 100644 index 71323b0..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/FastBitConverter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d225fec7b996f854daaf90b7cab04195 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs deleted file mode 100644 index 92f14be..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace LiteNetLib.Utils -{ - public interface INetSerializable - { - void Serialize(NetDataWriter writer); - void Deserialize(NetDataReader reader); - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs.meta deleted file mode 100644 index 52e88c1..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/INetSerializable.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9944c6f5e5d319163997f755bf15fcca -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs deleted file mode 100644 index 6ddab0e..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs +++ /dev/null @@ -1,673 +0,0 @@ -using System; -using System.Net; -using System.Runtime.CompilerServices; - -namespace LiteNetLib.Utils -{ - public class NetDataReader - { - protected byte[] _data; - protected int _position; - protected int _dataSize; - private int _offset; - - public byte[] RawData - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _data; - } - public int RawDataSize - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _dataSize; - } - public int UserDataOffset - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _offset; - } - public int UserDataSize - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _dataSize - _offset; - } - public bool IsNull - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _data == null; - } - public int Position - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _position; - } - public bool EndOfData - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _position == _dataSize; - } - public int AvailableBytes - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _dataSize - _position; - } - - public void SkipBytes(int count) - { - _position += count; - } - - public void SetPosition(int position) - { - _position = position; - } - - public void SetSource(NetDataWriter dataWriter) - { - _data = dataWriter.Data; - _position = 0; - _offset = 0; - _dataSize = dataWriter.Length; - } - - public void SetSource(byte[] source) - { - _data = source; - _position = 0; - _offset = 0; - _dataSize = source.Length; - } - - public void SetSource(byte[] source, int offset, int maxSize) - { - _data = source; - _position = offset; - _offset = offset; - _dataSize = maxSize; - } - - public NetDataReader() - { - - } - - public NetDataReader(NetDataWriter writer) - { - SetSource(writer); - } - - public NetDataReader(byte[] source) - { - SetSource(source); - } - - public NetDataReader(byte[] source, int offset, int maxSize) - { - SetSource(source, offset, maxSize); - } - - #region GetMethods - public IPEndPoint GetNetEndPoint() - { - string host = GetString(1000); - int port = GetInt(); - return NetUtils.MakeEndPoint(host, port); - } - - public byte GetByte() - { - byte res = _data[_position]; - _position++; - return res; - } - - public sbyte GetSByte() - { - return (sbyte)GetByte(); - } - - public T[] GetArray(ushort size) - { - ushort length = BitConverter.ToUInt16(_data, _position); - _position += 2; - T[] result = new T[length]; - length *= size; - Buffer.BlockCopy(_data, _position, result, 0, length); - _position += length; - return result; - } - - public bool[] GetBoolArray() - { - return GetArray(1); - } - - public ushort[] GetUShortArray() - { - return GetArray(2); - } - - public short[] GetShortArray() - { - return GetArray(2); - } - - public int[] GetIntArray() - { - return GetArray(4); - } - - public uint[] GetUIntArray() - { - return GetArray(4); - } - - public float[] GetFloatArray() - { - return GetArray(4); - } - - public double[] GetDoubleArray() - { - return GetArray(8); - } - - public long[] GetLongArray() - { - return GetArray(8); - } - - public ulong[] GetULongArray() - { - return GetArray(8); - } - - public string[] GetStringArray() - { - ushort length = GetUShort(); - string[] arr = new string[length]; - for (int i = 0; i < length; i++) - { - arr[i] = GetString(); - } - return arr; - } - - /// - /// Note that "maxStringLength" only limits the number of characters in a string, not its size in bytes. - /// Strings that exceed this parameter are returned as empty - /// - public string[] GetStringArray(int maxStringLength) - { - ushort length = GetUShort(); - string[] arr = new string[length]; - for (int i = 0; i < length; i++) - { - arr[i] = GetString(maxStringLength); - } - return arr; - } - - public bool GetBool() - { - return GetByte() == 1; - } - - public char GetChar() - { - return (char)GetUShort(); - } - - public ushort GetUShort() - { - ushort result = BitConverter.ToUInt16(_data, _position); - _position += 2; - return result; - } - - public short GetShort() - { - short result = BitConverter.ToInt16(_data, _position); - _position += 2; - return result; - } - - public long GetLong() - { - long result = BitConverter.ToInt64(_data, _position); - _position += 8; - return result; - } - - public ulong GetULong() - { - ulong result = BitConverter.ToUInt64(_data, _position); - _position += 8; - return result; - } - - public int GetInt() - { - int result = BitConverter.ToInt32(_data, _position); - _position += 4; - return result; - } - - public uint GetUInt() - { - uint result = BitConverter.ToUInt32(_data, _position); - _position += 4; - return result; - } - - public float GetFloat() - { - float result = BitConverter.ToSingle(_data, _position); - _position += 4; - return result; - } - - public double GetDouble() - { - double result = BitConverter.ToDouble(_data, _position); - _position += 8; - return result; - } - - /// - /// Note that "maxLength" only limits the number of characters in a string, not its size in bytes. - /// - /// "string.Empty" if value > "maxLength" - public string GetString(int maxLength) - { - ushort size = GetUShort(); - if (size == 0) - { - return string.Empty; - } - - int actualSize = size - 1; - if (actualSize >= NetDataWriter.StringBufferMaxLength) - { - return null; - } - - ArraySegment data = GetBytesSegment(actualSize); - - return (maxLength > 0 && NetDataWriter.uTF8Encoding.Value.GetCharCount(data.Array, data.Offset, data.Count) > maxLength) ? - string.Empty : - NetDataWriter.uTF8Encoding.Value.GetString(data.Array, data.Offset, data.Count); - } - - public string GetString() - { - ushort size = GetUShort(); - if (size == 0) - { - return string.Empty; - } - - int actualSize = size - 1; - if (actualSize >= NetDataWriter.StringBufferMaxLength) - { - return null; - } - - ArraySegment data = GetBytesSegment(actualSize); - - return NetDataWriter.uTF8Encoding.Value.GetString(data.Array, data.Offset, data.Count); - } - - public ArraySegment GetBytesSegment(int count) - { - ArraySegment segment = new ArraySegment(_data, _position, count); - _position += count; - return segment; - } - - public ArraySegment GetRemainingBytesSegment() - { - ArraySegment segment = new ArraySegment(_data, _position, AvailableBytes); - _position = _data.Length; - return segment; - } - - public T Get() where T : struct, INetSerializable - { - var obj = default(T); - obj.Deserialize(this); - return obj; - } - - public T Get(Func constructor) where T : class, INetSerializable - { - var obj = constructor(); - obj.Deserialize(this); - return obj; - } - - public byte[] GetRemainingBytes() - { - byte[] outgoingData = new byte[AvailableBytes]; - Buffer.BlockCopy(_data, _position, outgoingData, 0, AvailableBytes); - _position = _data.Length; - return outgoingData; - } - - public void GetBytes(byte[] destination, int start, int count) - { - Buffer.BlockCopy(_data, _position, destination, start, count); - _position += count; - } - - public void GetBytes(byte[] destination, int count) - { - Buffer.BlockCopy(_data, _position, destination, 0, count); - _position += count; - } - - public sbyte[] GetSBytesWithLength() - { - return GetArray(1); - } - - public byte[] GetBytesWithLength() - { - return GetArray(1); - } - #endregion - - #region PeekMethods - - public byte PeekByte() - { - return _data[_position]; - } - - public sbyte PeekSByte() - { - return (sbyte)_data[_position]; - } - - public bool PeekBool() - { - return _data[_position] == 1; - } - - public char PeekChar() - { - return (char)PeekUShort(); - } - - public ushort PeekUShort() - { - return BitConverter.ToUInt16(_data, _position); - } - - public short PeekShort() - { - return BitConverter.ToInt16(_data, _position); - } - - public long PeekLong() - { - return BitConverter.ToInt64(_data, _position); - } - - public ulong PeekULong() - { - return BitConverter.ToUInt64(_data, _position); - } - - public int PeekInt() - { - return BitConverter.ToInt32(_data, _position); - } - - public uint PeekUInt() - { - return BitConverter.ToUInt32(_data, _position); - } - - public float PeekFloat() - { - return BitConverter.ToSingle(_data, _position); - } - - public double PeekDouble() - { - return BitConverter.ToDouble(_data, _position); - } - - /// - /// Note that "maxLength" only limits the number of characters in a string, not its size in bytes. - /// - public string PeekString(int maxLength) - { - ushort size = PeekUShort(); - if (size == 0) - { - return string.Empty; - } - - int actualSize = size - 1; - if (actualSize >= NetDataWriter.StringBufferMaxLength) - { - return null; - } - - return (maxLength > 0 && NetDataWriter.uTF8Encoding.Value.GetCharCount(_data, _position + 2, actualSize) > maxLength) ? - string.Empty : - NetDataWriter.uTF8Encoding.Value.GetString(_data, _position + 2, actualSize); - } - - public string PeekString() - { - ushort size = PeekUShort(); - if (size == 0) - { - return string.Empty; - } - - int actualSize = size - 1; - if (actualSize >= NetDataWriter.StringBufferMaxLength) - { - return null; - } - - return NetDataWriter.uTF8Encoding.Value.GetString(_data, _position + 2, actualSize); - } - #endregion - - #region TryGetMethods - public bool TryGetByte(out byte result) - { - if (AvailableBytes >= 1) - { - result = GetByte(); - return true; - } - result = 0; - return false; - } - - public bool TryGetSByte(out sbyte result) - { - if (AvailableBytes >= 1) - { - result = GetSByte(); - return true; - } - result = 0; - return false; - } - - public bool TryGetBool(out bool result) - { - if (AvailableBytes >= 1) - { - result = GetBool(); - return true; - } - result = false; - return false; - } - - public bool TryGetChar(out char result) - { - if (!TryGetUShort(out ushort uShortValue)) - { - result = '\0'; - return false; - } - result = (char)uShortValue; - return true; - } - - public bool TryGetShort(out short result) - { - if (AvailableBytes >= 2) - { - result = GetShort(); - return true; - } - result = 0; - return false; - } - - public bool TryGetUShort(out ushort result) - { - if (AvailableBytes >= 2) - { - result = GetUShort(); - return true; - } - result = 0; - return false; - } - - public bool TryGetInt(out int result) - { - if (AvailableBytes >= 4) - { - result = GetInt(); - return true; - } - result = 0; - return false; - } - - public bool TryGetUInt(out uint result) - { - if (AvailableBytes >= 4) - { - result = GetUInt(); - return true; - } - result = 0; - return false; - } - - public bool TryGetLong(out long result) - { - if (AvailableBytes >= 8) - { - result = GetLong(); - return true; - } - result = 0; - return false; - } - - public bool TryGetULong(out ulong result) - { - if (AvailableBytes >= 8) - { - result = GetULong(); - return true; - } - result = 0; - return false; - } - - public bool TryGetFloat(out float result) - { - if (AvailableBytes >= 4) - { - result = GetFloat(); - return true; - } - result = 0; - return false; - } - - public bool TryGetDouble(out double result) - { - if (AvailableBytes >= 8) - { - result = GetDouble(); - return true; - } - result = 0; - return false; - } - - public bool TryGetString(out string result) - { - if (AvailableBytes >= 2) - { - ushort strSize = PeekUShort(); - if (AvailableBytes >= strSize + 1) - { - result = GetString(); - return true; - } - } - result = null; - return false; - } - - public bool TryGetStringArray(out string[] result) - { - if (!TryGetUShort(out ushort strArrayLength)) { - result = null; - return false; - } - - result = new string[strArrayLength]; - for (int i = 0; i < strArrayLength; i++) - { - if (!TryGetString(out result[i])) - { - result = null; - return false; - } - } - - return true; - } - - public bool TryGetBytesWithLength(out byte[] result) - { - if (AvailableBytes >= 2) - { - ushort length = PeekUShort(); - if (length >= 0 && AvailableBytes >= 2 + length) - { - result = GetBytesWithLength(); - return true; - } - } - result = null; - return false; - } - #endregion - - public void Clear() - { - _position = 0; - _dataSize = 0; - _data = null; - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs.meta deleted file mode 100644 index 8354523..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataReader.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 7708ab193d4292974897437d01aa2a10 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs deleted file mode 100644 index baa8351..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs +++ /dev/null @@ -1,381 +0,0 @@ -using System; -using System.Net; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; - -namespace LiteNetLib.Utils -{ - public class NetDataWriter - { - protected byte[] _data; - protected int _position; - private const int InitialSize = 64; - private readonly bool _autoResize; - - public int Capacity - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _data.Length; - } - public byte[] Data - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _data; - } - public int Length - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _position; - } - - public static readonly ThreadLocal uTF8Encoding = new ThreadLocal(() => new UTF8Encoding(false, true)); - public const int StringBufferMaxLength = 65535; - private readonly byte[] _stringBuffer = new byte[StringBufferMaxLength]; - - public NetDataWriter() : this(true, InitialSize) - { - } - - public NetDataWriter(bool autoResize) : this(autoResize, InitialSize) - { - } - - public NetDataWriter(bool autoResize, int initialSize) - { - _data = new byte[initialSize]; - _autoResize = autoResize; - } - - /// - /// Creates NetDataWriter from existing ByteArray - /// - /// Source byte array - /// Copy array to new location or use existing - public static NetDataWriter FromBytes(byte[] bytes, bool copy) - { - if (copy) - { - var netDataWriter = new NetDataWriter(true, bytes.Length); - netDataWriter.Put(bytes); - return netDataWriter; - } - return new NetDataWriter(true, 0) {_data = bytes, _position = bytes.Length}; - } - - /// - /// Creates NetDataWriter from existing ByteArray (always copied data) - /// - /// Source byte array - /// Offset of array - /// Length of array - public static NetDataWriter FromBytes(byte[] bytes, int offset, int length) - { - var netDataWriter = new NetDataWriter(true, bytes.Length); - netDataWriter.Put(bytes, offset, length); - return netDataWriter; - } - - public static NetDataWriter FromString(string value) - { - var netDataWriter = new NetDataWriter(); - netDataWriter.Put(value); - return netDataWriter; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ResizeIfNeed(int newSize) - { - if (_data.Length < newSize) - { - Array.Resize(ref _data, Math.Max(newSize, _data.Length * 2)); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void EnsureFit(int additionalSize) - { - if (_data.Length < _position + additionalSize) - { - Array.Resize(ref _data, Math.Max(_position + additionalSize, _data.Length * 2)); - } - } - - public void Reset(int size) - { - ResizeIfNeed(size); - _position = 0; - } - - public void Reset() - { - _position = 0; - } - - public byte[] CopyData() - { - byte[] resultData = new byte[_position]; - Buffer.BlockCopy(_data, 0, resultData, 0, _position); - return resultData; - } - - /// - /// Sets position of NetDataWriter to rewrite previous values - /// - /// new byte position - /// previous position of data writer - public int SetPosition(int position) - { - int prevPosition = _position; - _position = position; - return prevPosition; - } - - public void Put(float value) - { - if (_autoResize) - ResizeIfNeed(_position + 4); - FastBitConverter.GetBytes(_data, _position, value); - _position += 4; - } - - public void Put(double value) - { - if (_autoResize) - ResizeIfNeed(_position + 8); - FastBitConverter.GetBytes(_data, _position, value); - _position += 8; - } - - public void Put(long value) - { - if (_autoResize) - ResizeIfNeed(_position + 8); - FastBitConverter.GetBytes(_data, _position, value); - _position += 8; - } - - public void Put(ulong value) - { - if (_autoResize) - ResizeIfNeed(_position + 8); - FastBitConverter.GetBytes(_data, _position, value); - _position += 8; - } - - public void Put(int value) - { - if (_autoResize) - ResizeIfNeed(_position + 4); - FastBitConverter.GetBytes(_data, _position, value); - _position += 4; - } - - public void Put(uint value) - { - if (_autoResize) - ResizeIfNeed(_position + 4); - FastBitConverter.GetBytes(_data, _position, value); - _position += 4; - } - - public void Put(char value) - { - Put((ushort)value); - } - - public void Put(ushort value) - { - if (_autoResize) - ResizeIfNeed(_position + 2); - FastBitConverter.GetBytes(_data, _position, value); - _position += 2; - } - - public void Put(short value) - { - if (_autoResize) - ResizeIfNeed(_position + 2); - FastBitConverter.GetBytes(_data, _position, value); - _position += 2; - } - - public void Put(sbyte value) - { - if (_autoResize) - ResizeIfNeed(_position + 1); - _data[_position] = (byte)value; - _position++; - } - - public void Put(byte value) - { - if (_autoResize) - ResizeIfNeed(_position + 1); - _data[_position] = value; - _position++; - } - - public void Put(byte[] data, int offset, int length) - { - if (_autoResize) - ResizeIfNeed(_position + length); - Buffer.BlockCopy(data, offset, _data, _position, length); - _position += length; - } - - public void Put(byte[] data) - { - if (_autoResize) - ResizeIfNeed(_position + data.Length); - Buffer.BlockCopy(data, 0, _data, _position, data.Length); - _position += data.Length; - } - - public void PutSBytesWithLength(sbyte[] data, int offset, ushort length) - { - if (_autoResize) - ResizeIfNeed(_position + 2 + length); - FastBitConverter.GetBytes(_data, _position, length); - Buffer.BlockCopy(data, offset, _data, _position + 2, length); - _position += 2 + length; - } - - public void PutSBytesWithLength(sbyte[] data) - { - PutArray(data, 1); - } - - public void PutBytesWithLength(byte[] data, int offset, ushort length) - { - if (_autoResize) - ResizeIfNeed(_position + 2 + length); - FastBitConverter.GetBytes(_data, _position, length); - Buffer.BlockCopy(data, offset, _data, _position + 2, length); - _position += 2 + length; - } - - public void PutBytesWithLength(byte[] data) - { - PutArray(data, 1); - } - - public void Put(bool value) - { - Put((byte)(value ? 1 : 0)); - } - - public void PutArray(Array arr, int sz) - { - ushort length = arr == null ? (ushort) 0 : (ushort)arr.Length; - sz *= length; - if (_autoResize) - ResizeIfNeed(_position + sz + 2); - FastBitConverter.GetBytes(_data, _position, length); - if (arr != null) - Buffer.BlockCopy(arr, 0, _data, _position + 2, sz); - _position += sz + 2; - } - - public void PutArray(float[] value) - { - PutArray(value, 4); - } - - public void PutArray(double[] value) - { - PutArray(value, 8); - } - - public void PutArray(long[] value) - { - PutArray(value, 8); - } - - public void PutArray(ulong[] value) - { - PutArray(value, 8); - } - - public void PutArray(int[] value) - { - PutArray(value, 4); - } - - public void PutArray(uint[] value) - { - PutArray(value, 4); - } - - public void PutArray(ushort[] value) - { - PutArray(value, 2); - } - - public void PutArray(short[] value) - { - PutArray(value, 2); - } - - public void PutArray(bool[] value) - { - PutArray(value, 1); - } - - public void PutArray(string[] value) - { - ushort strArrayLength = value == null ? (ushort)0 : (ushort)value.Length; - Put(strArrayLength); - for (int i = 0; i < strArrayLength; i++) - Put(value[i]); - } - - public void PutArray(string[] value, int strMaxLength) - { - ushort strArrayLength = value == null ? (ushort)0 : (ushort)value.Length; - Put(strArrayLength); - for (int i = 0; i < strArrayLength; i++) - Put(value[i], strMaxLength); - } - - public void Put(IPEndPoint endPoint) - { - Put(endPoint.Address.ToString()); - Put(endPoint.Port); - } - - public void Put(string value) - { - Put(value, 0); - } - - /// - /// Note that "maxLength" only limits the number of characters in a string, not its size in bytes. - /// - public void Put(string value, int maxLength) - { - if (string.IsNullOrEmpty(value)) - { - Put((ushort)0); - return; - } - - int length = maxLength > 0 && value.Length > maxLength ? maxLength : value.Length; - int size = uTF8Encoding.Value.GetBytes(value, 0, length, _stringBuffer, 0); - - if (size == 0 || size >= StringBufferMaxLength) - { - Put((ushort)0); - return; - } - - Put(checked((ushort)(size + 1))); - Put(_stringBuffer, 0, size); - } - - public void Put(T obj) where T : INetSerializable - { - obj.Serialize(this); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs.meta deleted file mode 100644 index 212eeee..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetDataWriter.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b6b2e544f8091647e9f3de009f7eb041 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs deleted file mode 100644 index c8e29bc..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace LiteNetLib.Utils -{ - public class NetPacketProcessor - { - private static IReadOnlyDictionary PacketIdDict; - - public static byte PacketCount => (byte)(PacketIdDict?.Count ?? 0); - public static byte CurrentlyProcessingPacket { get; private set; } - - public static IReadOnlyDictionary RegisterPacketTypes() - { - Type[] packetTypes = AppDomain.CurrentDomain - .GetAssemblies() - .SelectMany(a => a.GetTypes()) - .Where(t => t.Namespace?.StartsWith("Multiplayer.Networking.Packets") == true || (t.Namespace == "LiteNetLib" && t.Name.EndsWith("Packet"))) - .OrderBy(t => t.FullName) - .ToArray(); - - if (packetTypes.Length > byte.MaxValue) - throw new OverflowException($"There's more than {byte.MaxValue} packet types!"); - - PacketIdDict = packetTypes - .Select((t, i) => new { Key = t, Value = (byte)i }) - .ToDictionary(x => x.Key, x => x.Value); - - return PacketIdDict; - } - - protected delegate void SubscribeDelegate(NetDataReader reader, object userData); - - private readonly NetManager _netManager; - private readonly NetSerializer _netSerializer; - private readonly Dictionary _callbacks = new Dictionary(); - - public NetPacketProcessor(NetManager netManager) - { - _netManager = netManager; - _netSerializer = new NetSerializer(); - } - - public NetPacketProcessor(NetManager netManager, int maxStringLength) - { - _netManager = netManager; - _netSerializer = new NetSerializer(maxStringLength); - } - - private static byte GetId() - { - if (PacketIdDict.TryGetValue(typeof(T), out byte id)) - return id; - throw new ArgumentException($"Failed to find packet ID for {typeof(T)}"); - } - - protected SubscribeDelegate GetCallbackFromData(NetDataReader reader) - { - byte id = reader.GetByte(); - CurrentlyProcessingPacket = id; - if (!_callbacks.TryGetValue(id, out var action)) - { - throw new ParseException($"Undefined packet {id} in NetDataReader"); - } - return action; - } - - private static void WriteId(NetDataWriter writer) - { - writer.Put(GetId()); - } - - /// - /// Register nested property type - /// - /// INetSerializable structure - public void RegisterNestedType() where T : struct, INetSerializable - { - _netSerializer.RegisterNestedType(); - } - - /// - /// Register nested property type - /// - /// - /// - public void RegisterNestedType(Action writeDelegate, Func readDelegate) - { - _netSerializer.RegisterNestedType(writeDelegate, readDelegate); - } - - /// - /// Register nested property type - /// - /// INetSerializable class - public void RegisterNestedType(Func constructor) where T : class, INetSerializable - { - _netSerializer.RegisterNestedType(constructor); - } - - /// - /// Reads all available data from NetDataReader and calls OnReceive delegates - /// - /// NetDataReader with packets data - public void ReadAllPackets(NetDataReader reader) - { - while (reader.AvailableBytes > 0) - ReadPacket(reader); - } - - /// - /// Reads all available data from NetDataReader and calls OnReceive delegates - /// - /// NetDataReader with packets data - /// Argument that passed to OnReceivedEvent - /// Malformed packet - public void ReadAllPackets(NetDataReader reader, object userData) - { - while (reader.AvailableBytes > 0) - ReadPacket(reader, userData); - } - - /// - /// Reads one packet from NetDataReader and calls OnReceive delegate - /// - /// NetDataReader with packet - /// Malformed packet - public void ReadPacket(NetDataReader reader) - { - ReadPacket(reader, null); - } - - public void Write(NetDataWriter writer, T packet) where T : class, new() - { - WriteId(writer); - _netSerializer.Serialize(writer, packet); - if (!_netManager.EnableStatistics) - return; - _netManager.Statistics.IncrementPacketsWritten(GetId()); - _netManager.Statistics.AddBytesWritten(GetId(), writer.Length); - } - - public void WriteNetSerializable(NetDataWriter writer, ref T packet) where T : INetSerializable - { - WriteId(writer); - packet.Serialize(writer); - if (!_netManager.EnableStatistics) - return; - _netManager.Statistics.IncrementPacketsWritten(GetId()); - _netManager.Statistics.AddBytesWritten(GetId(), writer.Length); - } - - /// - /// Reads one packet from NetDataReader and calls OnReceive delegate - /// - /// NetDataReader with packet - /// Argument that passed to OnReceivedEvent - /// Malformed packet - public void ReadPacket(NetDataReader reader, object userData) - { - GetCallbackFromData(reader)(reader, userData); - } - - /// - /// Register and subscribe to packet receive event - /// - /// event that will be called when packet deserialized with ReadPacket method - /// Method that constructs packet instead of slow Activator.CreateInstance - /// 's fields are not supported, or it has no fields - public void Subscribe(Action onReceive, Func packetConstructor) where T : class, new() - { - _netSerializer.Register(); - _callbacks[GetId()] = (reader, userData) => - { - var reference = packetConstructor(); - _netSerializer.Deserialize(reader, reference); - onReceive(reference); - }; - } - - /// - /// Register and subscribe to packet receive event (with userData) - /// - /// event that will be called when packet deserialized with ReadPacket method - /// Method that constructs packet instead of slow Activator.CreateInstance - /// 's fields are not supported, or it has no fields - public void Subscribe(Action onReceive, Func packetConstructor) where T : class, new() - { - _netSerializer.Register(); - _callbacks[GetId()] = (reader, userData) => - { - var reference = packetConstructor(); - _netSerializer.Deserialize(reader, reference); - onReceive(reference, (TUserData)userData); - }; - } - - /// - /// Register and subscribe to packet receive event - /// This method will overwrite last received packet class on receive (less garbage) - /// - /// event that will be called when packet deserialized with ReadPacket method - /// 's fields are not supported, or it has no fields - public void SubscribeReusable(Action onReceive) where T : class, new() - { - _netSerializer.Register(); - var reference = new T(); - _callbacks[GetId()] = (reader, userData) => - { - _netSerializer.Deserialize(reader, reference); - onReceive(reference); - }; - } - - /// - /// Register and subscribe to packet receive event - /// This method will overwrite last received packet class on receive (less garbage) - /// - /// event that will be called when packet deserialized with ReadPacket method - /// 's fields are not supported, or it has no fields - public void SubscribeReusable(Action onReceive) where T : class, new() - { - _netSerializer.Register(); - var reference = new T(); - _callbacks[GetId()] = (reader, userData) => - { - _netSerializer.Deserialize(reader, reference); - onReceive(reference, (TUserData)userData); - }; - } - - public void SubscribeNetSerializable( - Action onReceive, - Func packetConstructor) where T : INetSerializable - { - _callbacks[GetId()] = (reader, userData) => - { - var pkt = packetConstructor(); - pkt.Deserialize(reader); - onReceive(pkt, (TUserData)userData); - }; - } - - public void SubscribeNetSerializable( - Action onReceive, - Func packetConstructor) where T : INetSerializable - { - _callbacks[GetId()] = (reader, userData) => - { - var pkt = packetConstructor(); - pkt.Deserialize(reader); - onReceive(pkt); - }; - } - - public void SubscribeNetSerializable( - Action onReceive) where T : INetSerializable, new() - { - var reference = new T(); - _callbacks[GetId()] = (reader, userData) => - { - reference.Deserialize(reader); - onReceive(reference, (TUserData)userData); - }; - } - - public void SubscribeNetSerializable( - Action onReceive) where T : INetSerializable, new() - { - var reference = new T(); - _callbacks[GetId()] = (reader, userData) => - { - reference.Deserialize(reader); - onReceive(reference); - }; - } - - /// - /// Remove any subscriptions by type - /// - /// Packet type - /// true if remove is success - public bool RemoveSubscription() - { - return _callbacks.Remove(GetId()); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs.meta deleted file mode 100644 index 1a463ab..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetPacketProcessor.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 0639bb1a03a8629d58d4c50b8d0432bf -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs deleted file mode 100644 index 63f6cd6..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs +++ /dev/null @@ -1,738 +0,0 @@ -using System; -using System.Reflection; -using System.Collections.Generic; -using System.Net; -using System.Runtime.Serialization; - -namespace LiteNetLib.Utils -{ - public class InvalidTypeException : ArgumentException - { - public InvalidTypeException(string message) : base(message) { } - } - - public class ParseException : Exception - { - public ParseException(string message) : base(message) { } - } - - public class NetSerializer - { - private enum CallType - { - Basic, - Array, - List - } - - private abstract class FastCall - { - public CallType Type; - public virtual void Init(MethodInfo getMethod, MethodInfo setMethod, CallType type) { Type = type; } - public abstract void Read(T inf, NetDataReader r); - public abstract void Write(T inf, NetDataWriter w); - public abstract void ReadArray(T inf, NetDataReader r); - public abstract void WriteArray(T inf, NetDataWriter w); - public abstract void ReadList(T inf, NetDataReader r); - public abstract void WriteList(T inf, NetDataWriter w); - } - - private abstract class FastCallSpecific : FastCall - { - protected Func Getter; - protected Action Setter; - protected Func GetterArr; - protected Action SetterArr; - protected Func> GetterList; - protected Action> SetterList; - - public override void ReadArray(TClass inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: " + typeof(TProperty) + "[]"); } - public override void WriteArray(TClass inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: " + typeof(TProperty) + "[]"); } - public override void ReadList(TClass inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: List<" + typeof(TProperty) + ">"); } - public override void WriteList(TClass inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: List<" + typeof(TProperty) + ">"); } - - protected TProperty[] ReadArrayHelper(TClass inf, NetDataReader r) - { - ushort count = r.GetUShort(); - var arr = GetterArr(inf); - arr = arr == null || arr.Length != count ? new TProperty[count] : arr; - SetterArr(inf, arr); - return arr; - } - - protected TProperty[] WriteArrayHelper(TClass inf, NetDataWriter w) - { - var arr = GetterArr(inf); - w.Put((ushort)arr.Length); - return arr; - } - - protected List ReadListHelper(TClass inf, NetDataReader r, out int len) - { - len = r.GetUShort(); - var list = GetterList(inf); - if (list == null) - { - list = new List(len); - SetterList(inf, list); - } - return list; - } - - protected List WriteListHelper(TClass inf, NetDataWriter w, out int len) - { - var list = GetterList(inf); - if (list == null) - { - len = 0; - w.Put(0); - return null; - } - len = list.Count; - w.Put((ushort)len); - return list; - } - - public override void Init(MethodInfo getMethod, MethodInfo setMethod, CallType type) - { - base.Init(getMethod, setMethod, type); - switch (type) - { - case CallType.Array: - GetterArr = (Func)Delegate.CreateDelegate(typeof(Func), getMethod); - SetterArr = (Action)Delegate.CreateDelegate(typeof(Action), setMethod); - break; - case CallType.List: - GetterList = (Func>)Delegate.CreateDelegate(typeof(Func>), getMethod); - SetterList = (Action>)Delegate.CreateDelegate(typeof(Action>), setMethod); - break; - default: - Getter = (Func)Delegate.CreateDelegate(typeof(Func), getMethod); - Setter = (Action)Delegate.CreateDelegate(typeof(Action), setMethod); - break; - } - } - } - - private abstract class FastCallSpecificAuto : FastCallSpecific - { - protected abstract void ElementRead(NetDataReader r, out TProperty prop); - protected abstract void ElementWrite(NetDataWriter w, ref TProperty prop); - - public override void Read(TClass inf, NetDataReader r) - { - ElementRead(r, out var elem); - Setter(inf, elem); - } - - public override void Write(TClass inf, NetDataWriter w) - { - var elem = Getter(inf); - ElementWrite(w, ref elem); - } - - public override void ReadArray(TClass inf, NetDataReader r) - { - var arr = ReadArrayHelper(inf, r); - for (int i = 0; i < arr.Length; i++) - ElementRead(r, out arr[i]); - } - - public override void WriteArray(TClass inf, NetDataWriter w) - { - var arr = WriteArrayHelper(inf, w); - for (int i = 0; i < arr.Length; i++) - ElementWrite(w, ref arr[i]); - } - } - - private sealed class FastCallStatic : FastCallSpecific - { - private readonly Action _writer; - private readonly Func _reader; - - public FastCallStatic(Action write, Func read) - { - _writer = write; - _reader = read; - } - - public override void Read(TClass inf, NetDataReader r) { Setter(inf, _reader(r)); } - public override void Write(TClass inf, NetDataWriter w) { _writer(w, Getter(inf)); } - - public override void ReadList(TClass inf, NetDataReader r) - { - var list = ReadListHelper(inf, r, out int len); - int listCount = list.Count; - for (int i = 0; i < len; i++) - { - if (i < listCount) - list[i] = _reader(r); - else - list.Add(_reader(r)); - } - if (len < listCount) - list.RemoveRange(len, listCount - len); - } - - public override void WriteList(TClass inf, NetDataWriter w) - { - var list = WriteListHelper(inf, w, out int len); - for (int i = 0; i < len; i++) - _writer(w, list[i]); - } - - public override void ReadArray(TClass inf, NetDataReader r) - { - var arr = ReadArrayHelper(inf, r); - int len = arr.Length; - for (int i = 0; i < len; i++) - arr[i] = _reader(r); - } - - public override void WriteArray(TClass inf, NetDataWriter w) - { - var arr = WriteArrayHelper(inf, w); - int len = arr.Length; - for (int i = 0; i < len; i++) - _writer(w, arr[i]); - } - } - - private sealed class FastCallStruct : FastCallSpecific where TProperty : struct, INetSerializable - { - private TProperty _p; - - public override void Read(TClass inf, NetDataReader r) - { - _p.Deserialize(r); - Setter(inf, _p); - } - - public override void Write(TClass inf, NetDataWriter w) - { - _p = Getter(inf); - _p.Serialize(w); - } - - public override void ReadList(TClass inf, NetDataReader r) - { - var list = ReadListHelper(inf, r, out int len); - int listCount = list.Count; - for (int i = 0; i < len; i++) - { - var itm = default(TProperty); - itm.Deserialize(r); - if(i < listCount) - list[i] = itm; - else - list.Add(itm); - } - if (len < listCount) - list.RemoveRange(len, listCount - len); - } - - public override void WriteList(TClass inf, NetDataWriter w) - { - var list = WriteListHelper(inf, w, out int len); - for (int i = 0; i < len; i++) - list[i].Serialize(w); - } - - public override void ReadArray(TClass inf, NetDataReader r) - { - var arr = ReadArrayHelper(inf, r); - int len = arr.Length; - for (int i = 0; i < len; i++) - arr[i].Deserialize(r); - } - - public override void WriteArray(TClass inf, NetDataWriter w) - { - var arr = WriteArrayHelper(inf, w); - int len = arr.Length; - for (int i = 0; i < len; i++) - arr[i].Serialize(w); - } - } - - private sealed class FastCallClass : FastCallSpecific where TProperty : class, INetSerializable - { - private readonly Func _constructor; - public FastCallClass(Func constructor) { _constructor = constructor; } - - public override void Read(TClass inf, NetDataReader r) - { - var p = _constructor(); - p.Deserialize(r); - Setter(inf, p); - } - - public override void Write(TClass inf, NetDataWriter w) - { - var p = Getter(inf); - p?.Serialize(w); - } - - public override void ReadList(TClass inf, NetDataReader r) - { - var list = ReadListHelper(inf, r, out int len); - int listCount = list.Count; - for (int i = 0; i < len; i++) - { - if (i < listCount) - { - list[i].Deserialize(r); - } - else - { - var itm = _constructor(); - itm.Deserialize(r); - list.Add(itm); - } - } - if (len < listCount) - list.RemoveRange(len, listCount - len); - } - - public override void WriteList(TClass inf, NetDataWriter w) - { - var list = WriteListHelper(inf, w, out int len); - for (int i = 0; i < len; i++) - list[i].Serialize(w); - } - - public override void ReadArray(TClass inf, NetDataReader r) - { - var arr = ReadArrayHelper(inf, r); - int len = arr.Length; - for (int i = 0; i < len; i++) - { - arr[i] = _constructor(); - arr[i].Deserialize(r); - } - } - - public override void WriteArray(TClass inf, NetDataWriter w) - { - var arr = WriteArrayHelper(inf, w); - int len = arr.Length; - for (int i = 0; i < len; i++) - arr[i].Serialize(w); - } - } - - private class IntSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetInt()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetIntArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class UIntSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetUInt()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetUIntArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class ShortSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetShort()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetShortArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class UShortSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetUShort()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetUShortArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class LongSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetLong()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetLongArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class ULongSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetULong()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetULongArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class ByteSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetByte()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetBytesWithLength()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutBytesWithLength(GetterArr(inf)); } - } - - private class SByteSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetSByte()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetSBytesWithLength()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutSBytesWithLength(GetterArr(inf)); } - } - - private class FloatSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetFloat()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetFloatArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class DoubleSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetDouble()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetDoubleArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class BoolSerializer : FastCallSpecific - { - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetBool()); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf)); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetBoolArray()); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf)); } - } - - private class CharSerializer : FastCallSpecificAuto - { - protected override void ElementWrite(NetDataWriter w, ref char prop) { w.Put(prop); } - protected override void ElementRead(NetDataReader r, out char prop) { prop = r.GetChar(); } - } - - private class IPEndPointSerializer : FastCallSpecificAuto - { - protected override void ElementWrite(NetDataWriter w, ref IPEndPoint prop) { w.Put(prop); } - protected override void ElementRead(NetDataReader r, out IPEndPoint prop) { prop = r.GetNetEndPoint(); } - } - - private class StringSerializer : FastCallSpecific - { - private readonly int _maxLength; - public StringSerializer(int maxLength) { _maxLength = maxLength > 0 ? maxLength : short.MaxValue; } - public override void Read(T inf, NetDataReader r) { Setter(inf, r.GetString(_maxLength)); } - public override void Write(T inf, NetDataWriter w) { w.Put(Getter(inf), _maxLength); } - public override void ReadArray(T inf, NetDataReader r) { SetterArr(inf, r.GetStringArray(_maxLength)); } - public override void WriteArray(T inf, NetDataWriter w) { w.PutArray(GetterArr(inf), _maxLength); } - } - - private class EnumByteSerializer : FastCall - { - protected readonly PropertyInfo Property; - protected readonly Type PropertyType; - public EnumByteSerializer(PropertyInfo property, Type propertyType) - { - Property = property; - PropertyType = propertyType; - } - public override void Read(T inf, NetDataReader r) { Property.SetValue(inf, Enum.ToObject(PropertyType, r.GetByte()), null); } - public override void Write(T inf, NetDataWriter w) { w.Put((byte)Property.GetValue(inf, null)); } - public override void ReadArray(T inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: Enum[]"); } - public override void WriteArray(T inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: Enum[]"); } - public override void ReadList(T inf, NetDataReader r) { throw new InvalidTypeException("Unsupported type: List"); } - public override void WriteList(T inf, NetDataWriter w) { throw new InvalidTypeException("Unsupported type: List"); } - } - - private class EnumIntSerializer : EnumByteSerializer - { - public EnumIntSerializer(PropertyInfo property, Type propertyType) : base(property, propertyType) { } - public override void Read(T inf, NetDataReader r) { Property.SetValue(inf, Enum.ToObject(PropertyType, r.GetInt()), null); } - public override void Write(T inf, NetDataWriter w) { w.Put((int)Property.GetValue(inf, null)); } - } - - private sealed class ClassInfo - { - public static ClassInfo Instance; - private readonly FastCall[] _serializers; - private readonly int _membersCount; - - public ClassInfo(List> serializers) - { - _membersCount = serializers.Count; - _serializers = serializers.ToArray(); - } - - public void Write(T obj, NetDataWriter writer) - { - for (int i = 0; i < _membersCount; i++) - { - var s = _serializers[i]; - if (s.Type == CallType.Basic) - s.Write(obj, writer); - else if (s.Type == CallType.Array) - s.WriteArray(obj, writer); - else - s.WriteList(obj, writer); - } - } - - public void Read(T obj, NetDataReader reader) - { - for (int i = 0; i < _membersCount; i++) - { - var s = _serializers[i]; - if (s.Type == CallType.Basic) - s.Read(obj, reader); - else if(s.Type == CallType.Array) - s.ReadArray(obj, reader); - else - s.ReadList(obj, reader); - } - } - } - - private abstract class CustomType - { - public abstract FastCall Get(); - } - - private sealed class CustomTypeStruct : CustomType where TProperty : struct, INetSerializable - { - public override FastCall Get() { return new FastCallStruct(); } - } - - private sealed class CustomTypeClass : CustomType where TProperty : class, INetSerializable - { - private readonly Func _constructor; - public CustomTypeClass(Func constructor) { _constructor = constructor; } - public override FastCall Get() { return new FastCallClass(_constructor); } - } - - private sealed class CustomTypeStatic : CustomType - { - private readonly Action _writer; - private readonly Func _reader; - public CustomTypeStatic(Action writer, Func reader) - { - _writer = writer; - _reader = reader; - } - public override FastCall Get() { return new FastCallStatic(_writer, _reader); } - } - - /// - /// Register custom property type - /// - /// INetSerializable structure - public void RegisterNestedType() where T : struct, INetSerializable - { - _registeredTypes.Add(typeof(T), new CustomTypeStruct()); - } - - /// - /// Register custom property type - /// - /// INetSerializable class - public void RegisterNestedType(Func constructor) where T : class, INetSerializable - { - _registeredTypes.Add(typeof(T), new CustomTypeClass(constructor)); - } - - /// - /// Register custom property type - /// - /// Any packet - /// custom type writer - /// custom type reader - public void RegisterNestedType(Action writer, Func reader) - { - _registeredTypes.Add(typeof(T), new CustomTypeStatic(writer, reader)); - } - - private NetDataWriter _writer; - private readonly int _maxStringLength; - private readonly Dictionary _registeredTypes = new Dictionary(); - - public NetSerializer() : this(0) - { - } - - public NetSerializer(int maxStringLength) - { - _maxStringLength = maxStringLength; - } - - private ClassInfo RegisterInternal() - { - if (ClassInfo.Instance != null) - return ClassInfo.Instance; - - Type t = typeof(T); - var props = t.GetProperties( - BindingFlags.Instance | - BindingFlags.Public | - BindingFlags.GetProperty | - BindingFlags.SetProperty); - var serializers = new List>(); - for (int i = 0; i < props.Length; i++) - { - var property = props[i]; - var propertyType = property.PropertyType; - - var elementType = propertyType.IsArray ? propertyType.GetElementType() : propertyType; - var callType = propertyType.IsArray ? CallType.Array : CallType.Basic; - - if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>)) - { - elementType = propertyType.GetGenericArguments()[0]; - callType = CallType.List; - } - - if (Attribute.IsDefined(property, typeof(IgnoreDataMemberAttribute))) - continue; - - var getMethod = property.GetGetMethod(); - var setMethod = property.GetSetMethod(); - if (getMethod == null || setMethod == null) - continue; - - FastCall serialzer = null; - if (propertyType.IsEnum) - { - var underlyingType = Enum.GetUnderlyingType(propertyType); - if (underlyingType == typeof(byte)) - serialzer = new EnumByteSerializer(property, propertyType); - else if (underlyingType == typeof(int)) - serialzer = new EnumIntSerializer(property, propertyType); - else - throw new InvalidTypeException("Not supported enum underlying type: " + underlyingType.Name); - } - else if (elementType == typeof(string)) - serialzer = new StringSerializer(_maxStringLength); - else if (elementType == typeof(bool)) - serialzer = new BoolSerializer(); - else if (elementType == typeof(byte)) - serialzer = new ByteSerializer(); - else if (elementType == typeof(sbyte)) - serialzer = new SByteSerializer(); - else if (elementType == typeof(short)) - serialzer = new ShortSerializer(); - else if (elementType == typeof(ushort)) - serialzer = new UShortSerializer(); - else if (elementType == typeof(int)) - serialzer = new IntSerializer(); - else if (elementType == typeof(uint)) - serialzer = new UIntSerializer(); - else if (elementType == typeof(long)) - serialzer = new LongSerializer(); - else if (elementType == typeof(ulong)) - serialzer = new ULongSerializer(); - else if (elementType == typeof(float)) - serialzer = new FloatSerializer(); - else if (elementType == typeof(double)) - serialzer = new DoubleSerializer(); - else if (elementType == typeof(char)) - serialzer = new CharSerializer(); - else if (elementType == typeof(IPEndPoint)) - serialzer = new IPEndPointSerializer(); - else - { - _registeredTypes.TryGetValue(elementType, out var customType); - if (customType != null) - serialzer = customType.Get(); - } - - if (serialzer != null) - { - serialzer.Init(getMethod, setMethod, callType); - serializers.Add(serialzer); - } - else - { - throw new InvalidTypeException("Unknown property type: " + propertyType.FullName); - } - } - ClassInfo.Instance = new ClassInfo(serializers); - return ClassInfo.Instance; - } - - /// 's fields are not supported, or it has no fields - public void Register() - { - RegisterInternal(); - } - - /// - /// Reads packet with known type - /// - /// NetDataReader with packet - /// Returns packet if packet in reader is matched type - /// 's fields are not supported, or it has no fields - public T Deserialize(NetDataReader reader) where T : class, new() - { - var info = RegisterInternal(); - var result = new T(); - try - { - info.Read(result, reader); - } - catch - { - return null; - } - return result; - } - - /// - /// Reads packet with known type (non alloc variant) - /// - /// NetDataReader with packet - /// Deserialization target - /// Returns true if packet in reader is matched type - /// 's fields are not supported, or it has no fields - public bool Deserialize(NetDataReader reader, T target) where T : class, new() - { - var info = RegisterInternal(); - try - { - info.Read(target, reader); - } - catch - { - return false; - } - return true; - } - - /// - /// Serialize object to NetDataWriter (fast) - /// - /// Serialization target NetDataWriter - /// Object to serialize - /// 's fields are not supported, or it has no fields - public void Serialize(NetDataWriter writer, T obj) where T : class, new() - { - RegisterInternal().Write(obj, writer); - } - - /// - /// Serialize object to byte array - /// - /// Object to serialize - /// byte array with serialized data - public byte[] Serialize(T obj) where T : class, new() - { - if (_writer == null) - _writer = new NetDataWriter(); - _writer.Reset(); - Serialize(_writer, obj); - return _writer.CopyData(); - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs.meta deleted file mode 100644 index b75eed3..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NetSerializer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: f4cee768ab184eb66add2893ecf7648d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs deleted file mode 100644 index 1ba5210..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs +++ /dev/null @@ -1,423 +0,0 @@ -using System; - -namespace LiteNetLib.Utils -{ - /// - /// Represents RFC4330 SNTP packet used for communication to and from a network time server. - /// - /// - /// - /// Most applications should just use the property. - /// - /// - /// The same data structure represents both request and reply packets. - /// Request and reply differ in which properties are set and to what values. - /// - /// - /// The only real property is . - /// All other properties read from and write to the underlying byte array - /// with the exception of , - /// which is not part of the packet on network and it is instead set locally after receiving the packet. - /// - /// - /// Copied from GuerrillaNtp project - /// with permission from Robert Vazan (@robertvazan) under MIT license, see https://github.com/RevenantX/LiteNetLib/pull/236 - /// - /// - public class NtpPacket - { - private static readonly DateTime Epoch = new DateTime(1900, 1, 1); - - /// - /// Gets RFC4330-encoded SNTP packet. - /// - /// - /// Byte array containing RFC4330-encoded SNTP packet. It is at least 48 bytes long. - /// - /// - /// This is the only real property. All other properties except - /// read from or write to this byte array. - /// - public byte[] Bytes { get; } - - /// - /// Gets the leap second indicator. - /// - /// - /// Leap second warning, if any. Special value - /// indicates unsynchronized server clock. - /// Default is . - /// - /// - /// Only servers fill in this property. Clients can consult this property for possible leap second warning. - /// - public NtpLeapIndicator LeapIndicator => (NtpLeapIndicator)((Bytes[0] & 0xC0) >> 6); - - /// - /// Gets or sets protocol version number. - /// - /// - /// SNTP protocol version. Default is 4, which is the latest version at the time of this writing. - /// - /// - /// In request packets, clients should leave this property at default value 4. - /// Servers usually reply with the same protocol version. - /// - public int VersionNumber - { - get => (Bytes[0] & 0x38) >> 3; - private set => Bytes[0] = (byte)((Bytes[0] & ~0x38) | value << 3); - } - - /// - /// Gets or sets SNTP packet mode, i.e. whether this is client or server packet. - /// - /// - /// SNTP packet mode. Default is in newly created packets. - /// Server reply should have this property set to . - /// - public NtpMode Mode - { - get => (NtpMode)(Bytes[0] & 0x07); - private set => Bytes[0] = (byte)((Bytes[0] & ~0x07) | (int)value); - } - - /// - /// Gets server's distance from the reference clock. - /// - /// - /// - /// Distance from the reference clock. This property is set only in server reply packets. - /// Servers connected directly to reference clock hardware set this property to 1. - /// Statum number is incremented by 1 on every hop down the NTP server hierarchy. - /// - /// - /// Special value 0 indicates that this packet is a Kiss-o'-Death message - /// with kiss code stored in . - /// - /// - public int Stratum => Bytes[1]; - - /// - /// Gets server's preferred polling interval. - /// - /// - /// Polling interval in log2 seconds, e.g. 4 stands for 16s and 17 means 131,072s. - /// - public int Poll => Bytes[2]; - - /// - /// Gets the precision of server clock. - /// - /// - /// Clock precision in log2 seconds, e.g. -20 for microsecond precision. - /// - public int Precision => (sbyte)Bytes[3]; - - /// - /// Gets the total round-trip delay from the server to the reference clock. - /// - /// - /// Round-trip delay to the reference clock. Normally a positive value smaller than one second. - /// - public TimeSpan RootDelay => GetTimeSpan32(4); - - /// - /// Gets the estimated error in time reported by the server. - /// - /// - /// Estimated error in time reported by the server. Normally a positive value smaller than one second. - /// - public TimeSpan RootDispersion => GetTimeSpan32(8); - - /// - /// Gets the ID of the time source used by the server or Kiss-o'-Death code sent by the server. - /// - /// - /// - /// ID of server's time source or Kiss-o'-Death code. - /// Purpose of this property depends on value of property. - /// - /// - /// Stratum 1 servers write here one of several special values that describe the kind of hardware clock they use. - /// - /// - /// Stratum 2 and lower servers set this property to IPv4 address of their upstream server. - /// If upstream server has IPv6 address, the address is hashed, because it doesn't fit in this property. - /// - /// - /// When server sets to special value 0, - /// this property contains so called kiss code that instructs the client to stop querying the server. - /// - /// - public uint ReferenceId => GetUInt32BE(12); - - /// - /// Gets or sets the time when the server clock was last set or corrected. - /// - /// - /// Time when the server clock was last set or corrected or null when not specified. - /// - /// - /// This Property is usually set only by servers. It usually lags server's current time by several minutes, - /// so don't use this property for time synchronization. - /// - public DateTime? ReferenceTimestamp => GetDateTime64(16); - - /// - /// Gets or sets the time when the client sent its request. - /// - /// - /// This property is null in request packets. - /// In reply packets, it is the time when the client sent its request. - /// Servers copy this value from - /// that they find in received request packet. - /// - /// - /// - public DateTime? OriginTimestamp => GetDateTime64(24); - - /// - /// Gets or sets the time when the request was received by the server. - /// - /// - /// This property is null in request packets. - /// In reply packets, it is the time when the server received client request. - /// - /// - /// - public DateTime? ReceiveTimestamp => GetDateTime64(32); - - /// - /// Gets or sets the time when the packet was sent. - /// - /// - /// Time when the packet was sent. It should never be null. - /// Default value is . - /// - /// - /// This property must be set by both clients and servers. - /// - /// - /// - public DateTime? TransmitTimestamp { get { return GetDateTime64(40); } private set { SetDateTime64(40, value); } } - - /// - /// Gets or sets the time of reception of response SNTP packet on the client. - /// - /// - /// Time of reception of response SNTP packet on the client. It is null in request packets. - /// - /// - /// This property is not part of the protocol and has to be set when reply packet is received. - /// - /// - /// - public DateTime? DestinationTimestamp { get; private set; } - - /// - /// Gets the round-trip time to the server. - /// - /// - /// Time the request spent traveling to the server plus the time the reply spent traveling back. - /// This is calculated from timestamps in the packet as (t1 - t0) + (t3 - t2) - /// where t0 is , - /// t1 is , - /// t2 is , - /// and t3 is . - /// This property throws an exception in request packets. - /// - public TimeSpan RoundTripTime - { - get - { - CheckTimestamps(); - return (ReceiveTimestamp.Value - OriginTimestamp.Value) + (DestinationTimestamp.Value - TransmitTimestamp.Value); - } - } - - /// - /// Gets the offset that should be added to local time to synchronize it with server time. - /// - /// - /// Time difference between server and client. It should be added to local time to get server time. - /// It is calculated from timestamps in the packet as 0.5 * ((t1 - t0) - (t3 - t2)) - /// where t0 is , - /// t1 is , - /// t2 is , - /// and t3 is . - /// This property throws an exception in request packets. - /// - public TimeSpan CorrectionOffset - { - get - { - CheckTimestamps(); - return TimeSpan.FromTicks(((ReceiveTimestamp.Value - OriginTimestamp.Value) - (DestinationTimestamp.Value - TransmitTimestamp.Value)).Ticks / 2); - } - } - - /// - /// Initializes default request packet. - /// - /// - /// Properties and - /// are set appropriately for request packet. Property - /// is set to . - /// - public NtpPacket() : this(new byte[48]) - { - Mode = NtpMode.Client; - VersionNumber = 4; - TransmitTimestamp = DateTime.UtcNow; - } - - /// - /// Initializes packet from received data. - /// - internal NtpPacket(byte[] bytes) - { - if (bytes.Length < 48) - throw new ArgumentException("SNTP reply packet must be at least 48 bytes long.", "bytes"); - Bytes = bytes; - } - - /// - /// Initializes packet from data received from a server. - /// - /// Data received from the server. - /// Utc time of reception of response SNTP packet on the client. - /// - public static NtpPacket FromServerResponse(byte[] bytes, DateTime destinationTimestamp) - { - return new NtpPacket(bytes) { DestinationTimestamp = destinationTimestamp }; - } - - internal void ValidateRequest() - { - if (Mode != NtpMode.Client) - throw new InvalidOperationException("This is not a request SNTP packet."); - if (VersionNumber == 0) - throw new InvalidOperationException("Protocol version of the request is not specified."); - if (TransmitTimestamp == null) - throw new InvalidOperationException("TransmitTimestamp must be set in request packet."); - } - - internal void ValidateReply() - { - if (Mode != NtpMode.Server) - throw new InvalidOperationException("This is not a reply SNTP packet."); - if (VersionNumber == 0) - throw new InvalidOperationException("Protocol version of the reply is not specified."); - if (Stratum == 0) - throw new InvalidOperationException(string.Format("Received Kiss-o'-Death SNTP packet with code 0x{0:x}.", ReferenceId)); - if (LeapIndicator == NtpLeapIndicator.AlarmCondition) - throw new InvalidOperationException("SNTP server has unsynchronized clock."); - CheckTimestamps(); - } - - private void CheckTimestamps() - { - if (OriginTimestamp == null) - throw new InvalidOperationException("Origin timestamp is missing."); - if (ReceiveTimestamp == null) - throw new InvalidOperationException("Receive timestamp is missing."); - if (TransmitTimestamp == null) - throw new InvalidOperationException("Transmit timestamp is missing."); - if (DestinationTimestamp == null) - throw new InvalidOperationException("Destination timestamp is missing."); - } - - private DateTime? GetDateTime64(int offset) - { - var field = GetUInt64BE(offset); - if (field == 0) - return null; - return new DateTime(Epoch.Ticks + Convert.ToInt64(field * (1.0 / (1L << 32) * 10000000.0))); - } - - private void SetDateTime64(int offset, DateTime? value) - { - SetUInt64BE(offset, value == null ? 0 : Convert.ToUInt64((value.Value.Ticks - Epoch.Ticks) * (0.0000001 * (1L << 32)))); - } - - private TimeSpan GetTimeSpan32(int offset) - { - return TimeSpan.FromSeconds(GetInt32BE(offset) / (double)(1 << 16)); - } - - private ulong GetUInt64BE(int offset) - { - return SwapEndianness(BitConverter.ToUInt64(Bytes, offset)); - } - - private void SetUInt64BE(int offset, ulong value) - { - FastBitConverter.GetBytes(Bytes, offset, SwapEndianness(value)); - } - - private int GetInt32BE(int offset) - { - return (int)GetUInt32BE(offset); - } - - private uint GetUInt32BE(int offset) - { - return SwapEndianness(BitConverter.ToUInt32(Bytes, offset)); - } - - private static uint SwapEndianness(uint x) - { - return ((x & 0xff) << 24) | ((x & 0xff00) << 8) | ((x & 0xff0000) >> 8) | ((x & 0xff000000) >> 24); - } - - private static ulong SwapEndianness(ulong x) - { - return ((ulong)SwapEndianness((uint)x) << 32) | SwapEndianness((uint)(x >> 32)); - } - } - - /// - /// Represents leap second warning from the server that instructs the client to add or remove leap second. - /// - /// - public enum NtpLeapIndicator - { - /// - /// No leap second warning. No action required. - /// - NoWarning, - - /// - /// Warns the client that the last minute of the current day has 61 seconds. - /// - LastMinuteHas61Seconds, - - /// - /// Warns the client that the last minute of the current day has 59 seconds. - /// - LastMinuteHas59Seconds, - - /// - /// Special value indicating that the server clock is unsynchronized and the returned time is unreliable. - /// - AlarmCondition - } - - /// - /// Describes SNTP packet mode, i.e. client or server. - /// - /// - public enum NtpMode - { - /// - /// Identifies client-to-server SNTP packet. - /// - Client = 3, - - /// - /// Identifies server-to-client SNTP packet. - /// - Server = 4, - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs.meta deleted file mode 100644 index a47fed2..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpPacket.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: d1ceb29a3d14d86e98fddc9c1b17e922 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs deleted file mode 100644 index bd7f74f..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Net; -using System.Net.Sockets; - -namespace LiteNetLib.Utils -{ - internal sealed class NtpRequest - { - private const int ResendTimer = 1000; - private const int KillTimer = 10000; - public const int DefaultPort = 123; - private readonly IPEndPoint _ntpEndPoint; - private int _resendTime = ResendTimer; - private int _killTime = 0; - - public NtpRequest(IPEndPoint endPoint) - { - _ntpEndPoint = endPoint; - } - - public bool NeedToKill => _killTime >= KillTimer; - - public bool Send(Socket socket, int time) - { - _resendTime += time; - _killTime += time; - if (_resendTime < ResendTimer) - { - return false; - } - var packet = new NtpPacket(); - try - { - int sendCount = socket.SendTo(packet.Bytes, 0, packet.Bytes.Length, SocketFlags.None, _ntpEndPoint); - return sendCount == packet.Bytes.Length; - } - catch - { - return false; - } - } - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs.meta deleted file mode 100644 index 6f3e611..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/NtpRequest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: bfa3324d57a7c285ea1dcbff1142e7e2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs deleted file mode 100644 index b73e1b9..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace LiteNetLib.Utils -{ - /// - /// PreserveAttribute prevents byte code stripping from removing a class, method, field, or property. - /// - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Interface | AttributeTargets.Delegate, Inherited = false)] - public class PreserveAttribute : Attribute - { - } -} diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs.meta deleted file mode 100644 index b2075b6..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/Utils/Preserve.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 7c73743e8060388d79d8ba1cf976a2e1 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json deleted file mode 100644 index e501145..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "com.revenantx.litenetlib", - "version": "1.0.1-1", - "displayName": "LiteNetLib", - "description": "Lite reliable UDP library for .NET Standard 2.0 (Mono, .NET Core, .NET Framework)", - "unity": "2018.3", - "author": { - "name": "RevenantX", - "url": "https://github.com/RevenantX" - } -} \ No newline at end of file diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json.meta deleted file mode 100644 index fc07e68..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/LiteNetLib/package.json.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 51f5d4f9655c57c32bf114ffbae054ce -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md b/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md deleted file mode 100644 index dc0d8aa..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# LiteNetLib - -Lite reliable UDP library for .NET Standard 2.0 (Mono, .NET Core, .NET Framework) - -[![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://stand-with-ukraine.pp.ua) - -**HighLevel API Part**: [LiteEntitySystem](https://github.com/RevenantX/LiteEntitySystem) - -**Discord chat**: [![Discord](https://img.shields.io/discord/501682175930925058.svg)](https://discord.gg/FATFPdy) - -[OLD BRANCH (and examples) for 0.9.x](https://github.com/RevenantX/LiteNetLib/tree/0.9) - -[Little Game Example on Unity](https://github.com/RevenantX/NetGameExample) - -[Documentation](https://revenantx.github.io/LiteNetLib/index.html) - -## Build - -### [NuGet](https://www.nuget.org/packages/LiteNetLib/) [![NuGet](https://img.shields.io/nuget/v/LiteNetLib?color=blue)](https://www.nuget.org/packages/LiteNetLib/) [![NuGet](https://img.shields.io/nuget/vpre/LiteNetLib)](https://www.nuget.org/packages/LiteNetLib/#versions-body-tab) [![NuGet](https://img.shields.io/nuget/dt/LiteNetLib)](https://www.nuget.org/packages/LiteNetLib/) - -### [Release builds](https://github.com/RevenantX/LiteNetLib/releases) [![GitHub (pre-)release](https://img.shields.io/github/release/RevenantX/LiteNetLib/all.svg)](https://github.com/RevenantX/LiteNetLib/releases) - -### [DLL build from master](https://ci.appveyor.com/project/RevenantX/litenetlib/branch/master/artifacts) [![](https://ci.appveyor.com/api/projects/status/354501wnvxs8kuh3/branch/master?svg=true)](https://ci.appveyor.com/project/RevenantX/litenetlib/branch/master) -( Warning! Master branch can be unstable! ) - -## Features - -* Lightweight - * Small CPU and RAM usage - * Small packet size overhead ( 1 byte for unreliable, 4 bytes for reliable packets ) -* Simple connection handling -* Peer to peer connections -* Helper classes for sending and reading messages -* Multiple data channels -* Different send mechanics - * Reliable with order - * Reliable without order - * Reliable sequenced (realiable only last packet) - * Ordered but unreliable with duplication prevention - * Simple UDP packets without order and reliability -* Fast packet serializer [(Usage manual)](https://revenantx.github.io/LiteNetLib/articles/netserializerusage.html) -* Automatic small packets merging -* Automatic fragmentation of reliable packets -* Automatic MTU detection -* Optional CRC32C checksums -* UDP NAT hole punching -* NTP time requests -* Packet loss and latency simulation -* IPv6 support (using separate socket for performance) -* Connection statisitcs -* Multicasting (for discovering hosts in local network) -* Unity support -* Supported platforms: - * Windows/Mac/Linux (.NET Framework, Mono, .NET Core, .NET Standard) - * Lumin OS (Magic Leap) - * Monogame - * Godot - * Unity 2018.3 (Desktop platforms, Android, iOS, Switch) - -## Support developer -[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/revx) - -## Unity notes!!! -* Minimal supported Unity is 2018.3. For older Unity versions use [0.9.x library](https://github.com/RevenantX/LiteNetLib/tree/0.9) versions -* Always use library sources instead of precompiled DLL files ( because there are platform specific #ifdefs and workarounds for unity bugs ) - -## Usage samples - -### Client -```csharp -EventBasedNetListener listener = new EventBasedNetListener(); -NetManager client = new NetManager(listener); -client.Start(); -client.Connect("localhost" /* host ip or name */, 9050 /* port */, "SomeConnectionKey" /* text key or NetDataWriter */); -listener.NetworkReceiveEvent += (fromPeer, dataReader, deliveryMethod, channel) => -{ - Console.WriteLine("We got: {0}", dataReader.GetString(100 /* max length of string */)); - dataReader.Recycle(); -}; - -while (!Console.KeyAvailable) -{ - client.PollEvents(); - Thread.Sleep(15); -} - -client.Stop(); -``` -### Server -```csharp -EventBasedNetListener listener = new EventBasedNetListener(); -NetManager server = new NetManager(listener); -server.Start(9050 /* port */); - -listener.ConnectionRequestEvent += request => -{ - if(server.ConnectedPeersCount < 10 /* max connections */) - request.AcceptIfKey("SomeConnectionKey"); - else - request.Reject(); -}; - -listener.PeerConnectedEvent += peer => -{ - Console.WriteLine("We got connection: {0}", peer.EndPoint); // Show peer ip - NetDataWriter writer = new NetDataWriter(); // Create writer class - writer.Put("Hello client!"); // Put some string - peer.Send(writer, DeliveryMethod.ReliableOrdered); // Send with reliability -}; - -while (!Console.KeyAvailable) -{ - server.PollEvents(); - Thread.Sleep(15); -} -server.Stop(); -``` diff --git a/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md.meta b/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md.meta deleted file mode 100644 index 72d771a..0000000 --- a/MultiplayerAssets/Assets/Scripts/LiteNetLib/README.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 2929904cd5f7a7400b9842994d31f74d -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs index b0a87a0..7f74bf2 100644 --- a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs +++ b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs @@ -10,5 +10,9 @@ public class AssetIndex : ScriptableObject [Header("Textures")] public Sprite multiplayerIcon; + public Sprite lockIcon; + public Sprite refreshIcon; + public Sprite connectIcon; + public Sprite lanIcon; } } diff --git a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs.meta b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs.meta index f58bced..7e50507 100644 --- a/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs.meta +++ b/MultiplayerAssets/Assets/Scripts/Multiplayer/AssetIndex.cs.meta @@ -1,3 +1,17 @@ fileFormatVersion: 2 guid: 6ab658f490174d2e96148e7e6e27ad3a -timeCreated: 1689643659 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: + - playerPrefab: {instanceID: 0} + - multiplayerIcon: {fileID: 21300000, guid: 981b3e40e34126c43a32b7a54238d2d6, type: 3} + - lockIcon: {fileID: 21300000, guid: b8a707a2b12db584fad32aed46912dd0, type: 3} + - refreshIcon: {fileID: 21300000, guid: 7c3f2166549e6e144ae26c8d527d59b0, type: 3} + - connectIcon: {fileID: 21300000, guid: dad0fda7f8df3cd41a278a839fe12d23, type: 3} + - lanIcon: {fileID: 21300000, guid: 8386cff9a47c8a2409ad12ae6ae2233e, type: 3} + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Textures/Connect.png b/MultiplayerAssets/Assets/Textures/Connect.png new file mode 100644 index 0000000..6b22b32 Binary files /dev/null and b/MultiplayerAssets/Assets/Textures/Connect.png differ diff --git a/MultiplayerAssets/Assets/Textures/Connect.png.meta b/MultiplayerAssets/Assets/Textures/Connect.png.meta new file mode 100644 index 0000000..30a876c --- /dev/null +++ b/MultiplayerAssets/Assets/Textures/Connect.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: dad0fda7f8df3cd41a278a839fe12d23 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Textures/LAN_icon.png b/MultiplayerAssets/Assets/Textures/LAN_icon.png new file mode 100644 index 0000000..142abee Binary files /dev/null and b/MultiplayerAssets/Assets/Textures/LAN_icon.png differ diff --git a/MultiplayerAssets/Assets/Textures/LAN_icon.png.meta b/MultiplayerAssets/Assets/Textures/LAN_icon.png.meta new file mode 100644 index 0000000..2b294c1 --- /dev/null +++ b/MultiplayerAssets/Assets/Textures/LAN_icon.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 8386cff9a47c8a2409ad12ae6ae2233e +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 1 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 1 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Textures/Refresh.png b/MultiplayerAssets/Assets/Textures/Refresh.png new file mode 100644 index 0000000..9f9062d Binary files /dev/null and b/MultiplayerAssets/Assets/Textures/Refresh.png differ diff --git a/MultiplayerAssets/Assets/Textures/Refresh.png.meta b/MultiplayerAssets/Assets/Textures/Refresh.png.meta new file mode 100644 index 0000000..570a634 --- /dev/null +++ b/MultiplayerAssets/Assets/Textures/Refresh.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: 7c3f2166549e6e144ae26c8d527d59b0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 1 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 1 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Textures/lock_icon.png b/MultiplayerAssets/Assets/Textures/lock_icon.png new file mode 100644 index 0000000..dcb097e Binary files /dev/null and b/MultiplayerAssets/Assets/Textures/lock_icon.png differ diff --git a/MultiplayerAssets/Assets/Textures/lock_icon.png.meta b/MultiplayerAssets/Assets/Textures/lock_icon.png.meta new file mode 100644 index 0000000..40ef252 --- /dev/null +++ b/MultiplayerAssets/Assets/Textures/lock_icon.png.meta @@ -0,0 +1,104 @@ +fileFormatVersion: 2 +guid: b8a707a2b12db584fad32aed46912dd0 +TextureImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 11 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 1 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + streamingMipmaps: 0 + streamingMipmapsPriority: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: 1 + aniso: 1 + mipBias: 0 + wrapU: 1 + wrapV: 1 + wrapW: 0 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 1 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spritePixelsToUnits: 100 + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spriteGenerateFallbackPhysicsShape: 0 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 8 + textureShape: 1 + singleChannelComponent: 0 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + applyGammaDecoding: 0 + platformSettings: + - serializedVersion: 3 + buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 1 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + - serializedVersion: 3 + buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 1 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + forceMaximumCompressionQuality_BC6H_BC7: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + bones: [] + spriteID: 5e97eb03825dee720800000000000000 + internalID: 0 + vertices: [] + indices: + edges: [] + weights: [] + secondaryTextures: [] + spritePackingTag: + pSDRemoveMatte: 0 + pSDShowRemoveMatteOption: 0 + userData: + assetBundleName: + assetBundleVariant: diff --git a/MultiplayerAssets/Assets/Textures/multiplayer_icon.png.meta b/MultiplayerAssets/Assets/Textures/multiplayer_icon.png.meta index b6bf436..ed2953e 100644 --- a/MultiplayerAssets/Assets/Textures/multiplayer_icon.png.meta +++ b/MultiplayerAssets/Assets/Textures/multiplayer_icon.png.meta @@ -62,7 +62,7 @@ TextureImporter: - serializedVersion: 3 buildTarget: DefaultTexturePlatform maxTextureSize: 2048 - resizeAlgorithm: 0 + resizeAlgorithm: 1 textureFormat: -1 textureCompression: 1 compressionQuality: 50 @@ -74,7 +74,7 @@ TextureImporter: - serializedVersion: 3 buildTarget: Standalone maxTextureSize: 2048 - resizeAlgorithm: 0 + resizeAlgorithm: 1 textureFormat: -1 textureCompression: 1 compressionQuality: 50 diff --git a/MultiplayerAssets/Packages/manifest.json b/MultiplayerAssets/Packages/manifest.json index b4953ac..d948b24 100644 --- a/MultiplayerAssets/Packages/manifest.json +++ b/MultiplayerAssets/Packages/manifest.json @@ -1,5 +1,6 @@ { "dependencies": { + "com.unity.assetbundlebrowser": "1.7.0", "com.unity.ide.rider": "1.2.1", "com.unity.ide.visualstudio": "2.0.18", "com.unity.ide.vscode": "1.2.5", diff --git a/MultiplayerAssets/Packages/packages-lock.json b/MultiplayerAssets/Packages/packages-lock.json index 38fde5f..d638f04 100644 --- a/MultiplayerAssets/Packages/packages-lock.json +++ b/MultiplayerAssets/Packages/packages-lock.json @@ -1,5 +1,12 @@ { "dependencies": { + "com.unity.assetbundlebrowser": { + "version": "1.7.0", + "depth": 0, + "source": "registry", + "dependencies": {}, + "url": "https://packages.unity.com" + }, "com.unity.ext.nunit": { "version": "1.0.6", "depth": 2, diff --git a/README.md b/README.md index 457cd00..efb6ac7 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,11 @@ A Derail Valley mod that adds multiplayer.

- Report Bug + Report Bug · - Request Feature + Request Feature + · + Discord

@@ -31,11 +33,16 @@
  • Roadmap
  • Building
  • Contributing
  • +
  • Translations
  • License
  • +# Important! +At present, assume all other mods are incompatible! +Some mods may work, but many do cause issues and break multiplayer capabilities. +Our primary focus is to have the vanilla game working in multiplayer; once this is achieved we will then work on compatibility with other mods. @@ -46,6 +53,7 @@ Multiplayer is a Derail Valley mod that adds multiplayer to the game, allowing y It works by having one player host a game, and then other players can join that game. +This fork is a continuation of [Insprill's](https://github.com/Insprill/dv-multiplayer) amazing efforts. @@ -82,6 +90,21 @@ If you're new to contributing to open-source projects, you can follow [this][con + + +## Translations + +Special thanks to those who have assisted with translations - Apologies if I've missed you, drop me a line and I'll update this section. +If you'd like to help with translations, please create a pull request or send a message on our [Discord channel](https://discord.com/channels/332511223536943105/1234574186161377363). +| **Translator** | **Language** | +| :------------ | :------------ +| Ádi | Hungarian | +| My Name Is BorING | Chinese (Simplified) | +| Harfeur | French | + + + + ## License @@ -95,18 +118,18 @@ See [LICENSE][license-url] for more information. -[contributors-shield]: https://img.shields.io/github/contributors/Insprill/dv-multiplayer.svg?style=for-the-badge -[contributors-url]: https://github.com/Insprill/dv-multiplayer/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/Insprill/dv-multiplayer.svg?style=for-the-badge -[forks-url]: https://github.com/Insprill/dv-multiplayer/network/members -[stars-shield]: https://img.shields.io/github/stars/Insprill/dv-multiplayer.svg?style=for-the-badge -[stars-url]: https://github.com/Insprill/dv-multiplayer/stargazers -[issues-shield]: https://img.shields.io/github/issues/Insprill/dv-multiplayer.svg?style=for-the-badge -[issues-url]: https://github.com/Insprill/dv-multiplayer/issues -[license-shield]: https://img.shields.io/github/license/Insprill/dv-multiplayer.svg?style=for-the-badge -[license-url]: https://github.com/Insprill/dv-multiplayer/blob/master/LICENSE +[contributors-shield]: https://img.shields.io/github/contributors/AMacro/dv-multiplayer.svg?style=for-the-badge +[contributors-url]: https://github.com/AMacro/dv-multiplayer/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/AMacro/dv-multiplayer.svg?style=for-the-badge +[forks-url]: https://github.com/AMacro/dv-multiplayer/network/members +[stars-shield]: https://img.shields.io/github/stars/AMacro/dv-multiplayer.svg?style=for-the-badge +[stars-url]: https://github.com/AMacro/dv-multiplayer/stargazers +[issues-shield]: https://img.shields.io/github/issues/AMacro/dv-multiplayer.svg?style=for-the-badge +[issues-url]: https://github.com/AMacro/dv-multiplayer/issues +[license-shield]: https://img.shields.io/github/license/AMacro/dv-multiplayer.svg?style=for-the-badge +[license-url]: https://github.com/AMacro/dv-multiplayer/blob/master/LICENSE [altfuture-support-email-url]: mailto:support@altfuture.gg [contributing-quickstart-url]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects [asset-studio-url]: https://github.com/Perfare/AssetStudio [mapify-building-docs]: https://dv-mapify.readthedocs.io/en/latest/contributing/building/ -[project-board-url]: https://github.com/users/Insprill/projects/8 +[project-board-url]: https://github.com/users/AMacro/projects/2 diff --git a/info.json b/info.json index b6f7b0e..7857c50 100644 --- a/info.json +++ b/info.json @@ -1,9 +1,12 @@ { - "Id": "Multiplayer", - "Version": "0.1.0", - "DisplayName": "Multiplayer", - "Author": "Insprill", - "EntryMethod": "Multiplayer.Multiplayer.Load", - "ManagerVersion": "0.27.3", - "LoadAfter": [ "RemoteDispatch" ] + "Id": "Multiplayer", + "Version": "0.1.10.0", + "DisplayName": "Multiplayer", + "Author": "Insprill, Macka, Morm", + "EntryMethod": "Multiplayer.Multiplayer.Load", + "ManagerVersion": "0.27.3", + "LoadAfter": [ + "RemoteDispatch" + ], + "Repository": "https://www.andrewcraigmackenzie.com/unitymods/Releases.json" } diff --git a/locale.csv b/locale.csv index f8b269b..5852458 100644 --- a/locale.csv +++ b/locale.csv @@ -5,32 +5,98 @@ Key,Description,English,Bulgarian,Chinese (Simplified),Chinese (Traditional),Cze ,"When saving the file, ensure to save it using UTF-8 encoding!",,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Main Menu,,,,,,,,,,,,,,,,,,,,,,,,,, -mm/join_server,The 'Join Server' button in the main menu.,Join Server,,,,,,,,Rejoindre le serveur,Spiel beitreten,,,Entra in un Server,,,,,,,,,,Unirse a un servidor,,, -mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,,,,,,,,Rejoindre une session multijoueur,Trete einer Mehrspielersitzung bei.,,,Entra in una sessione multiplayer.,,,,,,,,,,Únete a una sesión multijugador.,,, +mm/join_server,The 'Join Server' button in the main menu.,Join Server,Присъединете се към сървъра,加入服务器,加入伺服器,Připojte se k serveru,Tilmeld dig server,Kom bij de server,Liity palvelimelle,Rejoindre le serveur,Spiel beitreten,सर्वर में शामिल हों,Csatlakozz a szerverhez,Entra in un Server,サーバーに参加する,서버에 가입,Bli med server,Dołącz do serwera,Conectar-se ao servidor,Ligar-se ao servidor,Alăturați-vă serverului,Присоединиться к серверу,Pripojte sa k serveru,Unirse a un servidor,Gå med i servern,Sunucuya katıl,Приєднатися до сервера +mm/join_server__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия.,加入多人游戏,加入多人遊戲會話。,Připojte se k relaci pro více hráčů.,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoindre une session multijoueur,Trete einer Mehrspielersitzung bei.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Entra in una sessione multiplayer.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador.,Participe numa sessão multijogador.,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. mm/join_server__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Server Browser,,,,,,,,,,,,,,,,,,,,,,,,,, -sb/title,The title of the Server Browser tab,Server Browser,,,,,,,,Navigateur de serveurs,Server Liste,,,Ricerca Server,,,,,,,,,,Buscar servidores,,, -sb/ip,IP popup,Enter IP Address,,,,,,,,Entrer l’adresse IP,IP Adresse eingeben,,,Inserire Indirizzo IP,,,,,,,,,,Ingrese la dirección IP,,, -sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,,,,,,,,Adresse IP invalide,Ungültige IP Adresse!,,,Indirizzo IP Invalido!,,,,,,,,,,¡Dirección IP inválida!,,, -sb/port,Port popup.,Enter Port (7777 by default),,,,,,,,Entrer le port (7777 par défaut),Port eingeben (Standard: 7777),,,Inserire Porta (7777 di default),,,,,,,,,,Introduzca el número de puerto(7777 por defecto),,, -sb/port_invalid,Invalid port popup.,Invalid Port!,,,,,,,,Port invalide !,Ungültiger Port!,,,Porta Invalida!,,,,,,,,,,¡Número de Puerto no válido!,,, -sb/password,Password popup.,Enter Password,,,,,,,,Entrer le mot de passe,Passwort eingeben,,,Inserire Password,,,,,,,,,,Introducir la contraseña,,, +sb/title,The title of the Server Browser tab,Server Browser,Браузър на сървъра,服务器浏览器,伺服器瀏覽器,Serverový prohlížeč,Server browser,Server browser,Palvelimen selain,Navigateur de serveurs,Server-Browser,सर्वर ब्राउजर,Szerverböngésző,Ricerca Server,サーバーブラウザ,서버 브라우저,Servernettleser,Przeglądarka serwerów,Navegador do servidor,Navegador do servidor,Browser server,Браузер серверов,Serverový prehliadač,Buscar servidores,Serverbläddrare,Sunucu tarayıcısı,Браузер сервера +sb/manual_connect,Connect to IP,Connect to IP,Свържете се с IP,连接到IP,連接到IP,Připojte se k IP,Opret forbindelse til IP,Maak verbinding met IP,Yhdistä IP-osoitteeseen,Se connecter à une IP,Mit IP verbinden,आईपी ​​से कनेक्ट करें,Csatlakozzon az IP-hez,Connettiti all'IP,IPに接続する,IP에 연결,Koble til IP,Połącz się z IP,Conecte-se ao IP,Ligue-se ao IP,Conectați-vă la IP,Подключиться к IP,Pripojte sa k IP,Conéctese a IP,Anslut till IP,IP'ye bağlan,Підключитися до IP +sb/manual_connect__tooltip,The tooltip shown when hovering over the 'manualconnect' button.,Direct connection to a multiplayer session.,Директна връзка към мултиплейър сесия.,直接连接到多人游戏,直接連接到多人遊戲會話。,Přímé připojení k relaci pro více hráčů.,Direkte forbindelse til en multiplayer-session.,Directe verbinding met een multiplayersessie.,Suora yhteys moninpeliistuntoon.,Connexion directe à une session multijoueur.,Direkte Verbindung zu einer Multiplayer-Sitzung.,मल्टीप्लेयर सत्र से सीधा कनेक्शन।,Közvetlen kapcsolat egy többjátékos munkamenethez.,Connessione diretta a una sessione multiplayer.,マルチプレイヤー セッションへの直接接続。,멀티플레이어 세션에 직접 연결됩니다.,Direkte tilkobling til en flerspillerøkt.,Bezpośrednie połączenie z sesją wieloosobową.,Conexão direta a uma sessão multijogador.,Ligação direta a uma sessão multijogador.,Conexiune directă la o sesiune multiplayer.,Прямое подключение к многопользовательской сессии.,Priame pripojenie k relácii pre viacerých hráčov.,Conexión directa a una sesión multijugador.,Direktanslutning till en multiplayer-session.,Çok oyunculu bir oturuma doğrudan bağlantı.,Пряме підключення до багатокористувацької сесії. +sb/manual_connect__tooltip_disabled,Unused,,,,,,,,,,,,Felhasználatlan,,,,,,,,,,,,,, +sb/host,Host Game,Host Game,Домакин на играта,主机游戏,主機遊戲,Hostitelská hra,Værtsspil,Gastheerspel,Isäntäpeli,Héberger une partie,Gastspiel,मेज़बान खेल,Gazdajáték,Ospita il gioco,ホストゲーム,호스트 게임,Vertsspill,Gra gospodarza,Jogo anfitrião,Jogo anfitrião,Găzduire joc,Хост-игра,Hostiteľská hra,Juego de acogida,Värdspel,Sunucu Oyunu,Ведуча гра +sb/host__tooltip,The tooltip shown when hovering over the 'Host Server' button.,Host a multiplayer session.,Организирайте сесия за мултиплейър.,主持多人游戏,主持多人遊戲會話。,Uspořádejte relaci pro více hráčů.,Vær vært for en multiplayer-session.,Organiseer een multiplayersessie.,Järjestä moninpeliistunto.,Héberger une session multijoueur.,Veranstalten Sie eine Multiplayer-Sitzung.,एक मल्टीप्लेयर सत्र की मेजबानी करें.,Hozz létre egy többjátékos munkamenetet.,Ospita una sessione multigiocatore.,マルチプレイヤー セッションをホストします。,멀티플레이어 세션을 호스팅하세요.,Vær vert for en flerspillerøkt.,Zorganizuj sesję wieloosobową.,Hospede uma sessão multijogador.,Acolhe uma sessão multijogador.,Găzduiește o sesiune multiplayer.,Организуйте многопользовательский сеанс.,Usporiadajte reláciu pre viacerých hráčov.,Organiza una sesión multijugador.,Var värd för en session för flera spelare.,Çok oyunculu bir oturuma ev sahipliği yapın.,Проведіть сеанс для кількох гравців. +sb/host__tooltip_disabled,Unused,,,,,,,,,,,,Felhasználatlan,,,,,,,,,,,,,, +sb/join_game,Join Game,Join Game,Присъединете се към играта,加入游戏,加入遊戲,Připojte se ke hře,Deltag i spil,Speel mee,Liity peliin,Rejoindre une partie,Spiel beitreten,खेल में शामिल हो,Belépni a játékba,Unisciti al gioco,ゲームに参加します,게임 참여,Bli med i spillet,Dołącz do gry,Entrar no jogo,Entrar no jogo,Alatura-te jocului,Присоединиться к игре,Pridať sa do hry,Unete al juego,Gå med i spel,Oyuna katılmak,Приєднуйся до гри +sb/join_game__tooltip,The tooltip shown when hovering over the 'Join Server' button.,Join a multiplayer session.,Присъединете се към мултиплейър сесия.,加入多人游戏,加入多人遊戲會話。,Připojte se k relaci pro více hráčů.,Deltag i en multiplayer session.,Neem deel aan een multiplayersessie.,Liity moninpeliistuntoon.,Rejoindre une session multijoueur.,Nehmen Sie an einer Multiplayer-Sitzung teil.,मल्टीप्लेयर सत्र में शामिल हों.,Csatlakozz egy többjátékos munkamenethez.,Partecipa a una sessione multigiocatore.,マルチプレイヤー セッションに参加します。,멀티플레이어 세션에 참여하세요.,Bli med på en flerspillerøkt.,Dołącz do sesji wieloosobowej.,Participe de uma sessão multijogador.,Participe numa sessão multijogador.,Alăturați-vă unei sesiuni multiplayer.,Присоединяйтесь к многопользовательской сессии.,Pripojte sa k relácii pre viacerých hráčov.,Únete a una sesión multijugador.,Gå med i en multiplayer-session.,Çok oyunculu bir oturuma katılın.,Приєднуйтеся до багатокористувацької сесії. +sb/join_game__tooltip_disabled,The tooltip shown when hovering over the 'Join Server' button.,Select a game to join.,Изберете игра за присъединяване,选择要加入的游戏,選擇要加入的遊戲,Vyberte si hru pro připojení,Vælg et spil at deltage i,Kies een spel om deel te nemen,Valitse peli liittyäksesi,Sélectionnez une partie à rejoindre,Wählen Sie ein Spiel zum Beitritt,खेल में शामिल होने के लिए चुनें,Válasszon egy játékot a csatlakozáshoz,Seleziona un gioco da unirti,参加するゲームを選択,게임을 선택하십시오,Velg et spill å bli med på,"Wybierz grę, aby dołączyć",Selecione um jogo para entrar,Selecione um jogo para participar,Alegeți un joc pentru a vă alătura,Выберите игру для присоединения,Vyberte si hru,Seleccione un juego para unirse,Välj ett spel att gå med,Katılmak için bir oyun seçin,Виберіть гру для приєднання +sb/refresh,refresh,Refresh,Опресняване,刷新,重新整理,Obnovit,Opdater,Vernieuwen,virkistää,Rafraîchir,Aktualisierung,ताज़ा करना,Frissítés,ricaricare,リフレッシュ,새로 고치다,Forfriske,Odświeżać,Atualizar,Atualizar,Reîmprospăta,Обновить,Obnoviť,Actualizar,Uppdatera,Yenile,Оновити +sb/refresh__tooltip,The tooltip shown when hovering over the 'Refresh Server' button.,Refresh server list.,Обновяване на списъка със сървъри.,刷新服务器列表,刷新伺服器清單。,Obnovit seznam serverů.,Opdater serverliste.,Vernieuw de serverlijst.,Päivitä palvelinluettelo.,Actualise la liste des serveurs.,Serverliste aktualisieren.,सर्वर सूची ताज़ा करें.,Szerverlista frissítése.,Aggiorna l'elenco dei server.,サーバーリストを更新します。,서버 목록을 새로 고칩니다.,Oppdater serverlisten.,Odśwież listę serwerów.,Atualizar lista de servidores.,Atualizar lista de servidores.,Actualizează lista de servere.,Обновить список серверов.,Obnoviť zoznam serverov.,Actualizar la lista de servidores.,Uppdatera serverlistan.,Sunucu listesini yenileyin.,Оновити список серверів. +sb/refresh__tooltip_disabled,Tooltip for refresh button while refreshing,"Refreshing, please wait...","Опресняване, моля, изчакайте...","正在刷新,请稍候...","正在刷新,請稍候...","Obnovuje se, prosím, počkejte...","Opdaterer, vent venligst...","Vernieuwen, een ogenblik geduld...","Päivitetään, odota hetki...","Actualisation en cours, veuillez patienter...","Aktualisierung läuft, bitte warten...","ताज़ा कर रहा है, कृपया प्रतीक्षा करें...","Frissítés, kérjük, várjon...","Aggiornamento in corso, attendere prego...",リフレッシュ中、お待ちください...,"새로고침 중, 잠시만 기다려 주세요...","Oppdaterer, vennligst vent...","Odświeżanie, proszę czekać...","Atualizando, por favor, aguarde...","Atualizando, por favor, aguarde...","Se actualizează, vă rugăm să așteptați...","Обновление, подождите...","Obnovuje sa, čakajte...","Actualizando, por favor, espere...","Uppdaterar, vänligen vänta...","Güncelleniyor, lütfen bekleyin...","Оновлення, будь ласка, зачекайте..." +sb/ip,IP popup,Enter IP Address,Въведете IP адрес,输入IP地址,輸入IP位址,Zadejte IP adresu,Indtast IP-adresse,Voer het IP-adres in,Anna IP-osoite,Entrez l’adresse IP,IP Adresse eingeben,आईपी ​​पता दर्ज करें,Írja be az IP-címet,Inserire Indirizzo IP,IPアドレスを入力してください,IP 주소를 입력하세요,Skriv inn IP-adresse,Wprowadź adres IP,Digite o endereço IP,Introduza o endereço IP,Introduceți adresa IP,Введите IP-адрес,Zadajte IP adresu,Ingrese la dirección IP,Ange IP-adress,IP Adresini Girin,Введіть IP-адресу +sb/ip_invalid,Invalid IP popup.,Invalid IP Address!,Невалиден IP адрес!,IP 地址无效!,IP 位址無效!,Neplatná IP adresa!,Ugyldig IP-adresse!,Ongeldig IP-adres!,Virheellinen IP-osoite!,Adresse IP invalide,Ungültige IP Adresse!,अमान्य आईपी पता!,Érvénytelen IP-cím!,Indirizzo IP Invalido!,IP アドレスが無効です!,IP 주소가 잘못되었습니다!,Ugyldig IP-adresse!,Nieprawidłowy adres IP!,Endereço IP inválido!,Endereço IP inválido!,Adresă IP nevalidă!,Неверный IP-адрес!,Neplatná IP adresa!,¡Dirección IP inválida!,Ogiltig IP-adress!,Geçersiz IP adresi!,Недійсна IP-адреса! +sb/port,Port popup.,Enter Port (7777 by default),Въведете порт (7777 по подразбиране),输入端口(默认为 7777),輸入連接埠(預設為 7777),Zadejte port (ve výchozím nastavení 7777),Indtast port (7777 som standard),Poort invoeren (standaard 7777),Anna portti (oletuksena 7777),Entrez le port (7777 par défaut),Port eingeben (Standard: 7777),पोर्ट दर्ज करें (डिफ़ॉल्ट रूप से 7777),Írja be a portot (alapértelmezés szerint 7777),Inserire Porta (7777 di default),ポートを入力します (デフォルトでは 7777),포트 입력(기본적으로 7777),Angi port (7777 som standard),Wprowadź port (domyślnie 7777),Insira a porta (7777 por padrão),Introduza a porta (7777 por defeito),Introduceți port (7777 implicit),Введите порт (7777 по умолчанию),Zadajte port (predvolene 7777),Introduzca el número de puerto(7777 por defecto),Ange port (7777 som standard),Bağlantı Noktasını Girin (varsayılan olarak 7777),Введіть порт (7777 за замовчуванням) +sb/port_invalid,Invalid port popup.,Invalid Port!,Невалиден порт!,端口无效!,埠無效!,Neplatný port!,Ugyldig port!,Ongeldige poort!,Virheellinen portti!,Port invalide !,Ungültiger Port!,अमान्य पोर्ट!,Érvénytelen port!,Porta Invalida!,ポートが無効です!,포트가 잘못되었습니다!,Ugyldig port!,Nieprawidłowy port!,Porta inválida!,Porta inválida!,Port nevalid!,Неверный порт!,Neplatný port!,¡Número de Puerto no válido!,Ogiltig port!,Geçersiz Bağlantı Noktası!,Недійсний порт! +sb/password,Password popup.,Enter Password,Въведете паролата,输入密码,輸入密碼,Zadejte heslo,Indtast adgangskode,Voer wachtwoord in,Kirjoita salasana,Entrez le mot de passe,Passwort eingeben,पास वर्ड दर्ज करें,Írd be a jelszót,Inserire Password,パスワードを入力する,암호를 입력,Oppgi passord,Wprowadź hasło,Digite a senha,Introduza a senha,Introdu parola,Введите пароль,Zadajte heslo,Introducir la contraseña,Skriv in lösenord,Parolanı Gir,Введіть пароль +sb/players,Player count in details text,Players,Играчите,玩家,玩家,Hráči,Spillere,Spelers,Pelaajat,Joueurs,Spieler,खिलाड़ी,Játékosok,Giocatori,プレイヤー,플레이어,Spillere,Gracze,Jogadores,Jogadores,Jucători,Игроки,Hráči,Jugadores,Spelare,Oyuncular,Гравці +sb/password_required,Password required in details text,Password,Парола,密码,密碼,Heslo,Adgangskode,Wachtwoord,Salasana,Mot de passe,Passwort,पासवर्ड,Jelszó,Password,パスワード,비밀번호,Passord,Hasło,Senha,Senha,Parola,Пароль,Heslo,Contraseña,Lösenord,Parola,Пароль +sb/mods_required,Mods required in details text,Requires mods,Изисква модове,需要模组,需要模組,Požaduje módy,Kræver mods,Vereist mods,Vaatii modit,Nécessite des mods,Benötigt Mods,मॉड की आवश्यकता है,Modokat igényel,Richiede mod,モッズが必要,모드 필요,Krever modifikasjoner,Wymaga modyfikacji,Requer mods,Requer mods,Necesită moduri,Требуются модификации,Požaduje módy,Requiere mods,Kräver moddar,Mod gerektirir,Потрібні модифікації +sb/game_version,Game version in details text,Game version,Версия на играта,游戏版本,遊戲版本,Verze hry,Spilversion,Spelversie,Pelin versio,Version du jeu,Spielversion,गेम संस्करण,Verze hry,Versione del gioco,ゲームバージョン,게임 버전,Spillversjon,Wersja gry,Versão do jogo,Versão do jogo,Versiunea jocului,Версия игры,Verzia hry,Versión del juego,Spelversion,Oyun versiyonu,Версія гри +sb/mod_version,Multiplayer version in details text,Multiplayer version,Мултиплейър версия,多人游戏版本,多人遊戲版本,Multiplayer verze,Multiplayer version,Multiplayer versie,Moninpeliversio,Version multijoueur,Multiplayer-Version,मल्टीप्लेयर संस्करण,Multiplayer verze,Versione multiplayer,マルチプレイヤーバージョン,멀티플레이어 버전,Multiplayer versjon,Wersja multiplayer,Versão multiplayer,Versão multiplayer,Versiunea multiplayer,Мультиплеерная версия,Multiplayer verzia,Versión multijugador,Multiplayer-version,Çok oyunculu sürüm,Багатокористувацька версія +sb/yes,Response 'yes' for details text,Yes,Да,是,是,Ano,Ja,Ja,Kyllä,Oui,Ja,हां,Ano,Sì,はい,네,Ja,Tak,Sim,Sim,Da,Да,Áno,Sí,Ja,Evet,Так +sb/no,Response 'no' for details text,No,Не,否,否,Ne,Nej,Nee,Ei,Non,Nein,नहीं,Ne,No,いいえ,아니요,Nei,Nie,Não,Não,Nu,Нет,Nie,Nie,Nej,Hayır,Ні +sb/no_servers,Label for no servers,No servers found. Refresh or start your own!,Няма намерени сървъри. Обновете или стартирайте свой собствен!,未找到服务器。 刷新或创建您自己的!,未找到伺服器。 刷新或創建您自己的!,Žádné servery nebyly nalezeny. Obnovte nebo spusťte vlastní!,Ingen servere fundet. Opdater eller start din egen!,Geen servers gevonden. Ververs of start je eigen!,Ei palvelimia löytynyt. Päivitä tai aloita oma!,Aucun serveur trouvé. Rafraîchissez ou créez le vôtre !,Keine Server gefunden. Aktualisieren oder eigenen starten!,कोई सर्वर नहीं मिला। ताज़ा करें या अपना स्वयं का प्रारंभ करें!,"Nem található szerver. Frissítsen, vagy indítson sajátot!",Nessun server trovato. Aggiorna o avvia il tuo!,サーバーが見つかりませんでした。 更新するか、自分で始めてください!,서버를 찾을 수 없습니다. 새로 고치거나 직접 시작하십시오!,Ingen servere funnet. Oppdater eller start din egen!,Nie znaleziono serwerów. Odśwież lub zacznij własny!,Nenhum servidor encontrado. Atualize ou inicie o seu próprio!,Nenhum servidor encontrado. Atualize ou inicie o seu!,Nu au fost găsite servere. Reîmprospătați sau începeți propriul dvs!,Серверы не найдены. Обновите или начните свой собственный!,Žiadne servery sa nenašli. Obnovte alebo spustite vlastný!,No se encontraron servidores. ¡Actualiza o empieza uno propio!,Inga servrar hittades. Uppdatera eller starta din egen!,Sunucu bulunamadı. Yenileyin veya kendi sunucunuzu başlatın!,Сервери не знайдено. Оновіть або почніть власний! +sb/no_servers__tooltip,Label for no servers,No servers found. Refresh or start your own!,Няма намерени сървъри. Обновете или стартирайте свой собствен!,未找到服务器。 刷新或创建您自己的!,未找到伺服器。 刷新或創建您自己的!,Žádné servery nebyly nalezeny. Obnovte nebo spusťte vlastní!,Ingen servere fundet. Opdater eller start din egen!,Geen servers gevonden. Ververs of start je eigen!,Ei palvelimia löytynyt. Päivitä tai aloita oma!,Aucun serveur trouvé. Rafraîchissez ou créez le vôtre !,Keine Server gefunden. Aktualisieren oder eigenen starten!,कोई सर्वर नहीं मिला। ताज़ा करें या अपना स्वयं का प्रारंभ करें!,"Nem található szerver. Frissítsen, vagy indítson sajátot!",Nessun server trovato. Aggiorna o avvia il tuo!,サーバーが見つかりませんでした。 更新するか、自分で始めてください!,서버를 찾을 수 없습니다. 새로 고치거나 직접 시작하십시오!,Ingen servere funnet. Oppdater eller start din egen!,Nie znaleziono serwerów. Odśwież lub zacznij własny!,Nenhum servidor encontrado. Atualize ou inicie o seu próprio!,Nenhum servidor encontrado. Atualize ou inicie o seu!,Nu au fost găsite servere. Reîmprospătați sau începeți propriul dvs!,Серверы не найдены. Обновите или начните свой собственный!,Žiadne servery sa nenašli. Obnovte alebo spustite vlastný!,No se encontraron servidores. ¡Actualiza o empieza uno propio!,Inga servrar hittades. Uppdatera eller starta din egen!,Sunucu bulunamadı. Yenileyin veya kendi sunucunuzu başlatın!,Сервери не знайдено. Оновіть або почніть власний! +sb/no_servers__tooltip_disabled,Label for no servers,No servers found. Refresh or start your own!,Няма намерени сървъри. Обновете или стартирайте свой собствен!,未找到服务器。 刷新或创建您自己的!,未找到伺服器。 刷新或創建您自己的!,Žádné servery nebyly nalezeny. Obnovte nebo spusťte vlastní!,Ingen servere fundet. Opdater eller start din egen!,Geen servers gevonden. Ververs of start je eigen!,Ei palvelimia löytynyt. Päivitä tai aloita oma!,Aucun serveur trouvé. Rafraîchissez ou créez le vôtre !,Keine Server gefunden. Aktualisieren oder eigenen starten!,कोई सर्वर नहीं मिला। ताज़ा करें या अपना स्वयं का प्रारंभ करें!,"Nem található szerver. Frissítsen, vagy indítson sajátot!",Nessun server trovato. Aggiorna o avvia il tuo!,サーバーが見つかりませんでした。 更新するか、自分で始めてください!,서버를 찾을 수 없습니다. 새로 고치거나 직접 시작하십시오!,Ingen servere funnet. Oppdater eller start din egen!,Nie znaleziono serwerów. Odśwież lub zacznij własny!,Nenhum servidor encontrado. Atualize ou inicie o seu próprio!,Nenhum servidor encontrado. Atualize ou inicie o seu!,Nu au fost găsite servere. Reîmprospătați sau începeți propriul dvs!,Серверы не найдены. Обновите или начните свой собственный!,Žiadne servery sa nenašli. Obnovte alebo spustite vlastný!,No se encontraron servidores. ¡Actualiza o empieza uno propio!,Inga servrar hittades. Uppdatera eller starta din egen!,Sunucu bulunamadı. Yenileyin veya kendi sunucunuzu başlatın!,Сервери не знайдено. Оновіть або почніть власний! +sb/info/title,Title for server browser info,Server Browser Info,,服务器浏览器介绍,,,,,,Informations du navigateur de serveurs,,,Szerverböngésző információ,,,,,,,,,,,,,, +sb/info/content,Content for server browser info,"Welcome to Derail Valley Multiplayer Mod!\n\nThe server list refreshes automatically every {0} seconds, but you can refresh manually once every {1} seconds.",,"欢迎来到脱轨山谷的联机模式!\n\n服务器列表会在每{0}秒刷新,但是你可以手动让它在每{1}秒刷新",,,,,,"Bienvenue dans le mod multijoueur de Derail Valley !\n\nLa liste des serveurs est mise à jour automatiquement toutes les {0} secondes, mais vous pouvez la rafraîchir manuellement toutes les {1} secondes.",,,"Üdvözli a Derail Valley Multiplayer Mod!\n\nA szerverlista automatikusan frissül {0} másodpercenként, de manuálisan is frissíthetsz minden {1} másodpercet.",,,,,,,,,,,,,, +sb/connecting,Connecting dialogue,"Connecting, please wait...\nAttempt: {0}",,"正在连接中,请稍候片刻\n尝试次数: {0}",,,,,,"Connexion, merci de patienter...\nEssai : {0}",,,"Csatlakozás, kérjük, várjon...\nKísérlet: {0}",,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Server Host,,,,,,,,,,,,,,,,,,,,,,,,,, +host/title,The title of the Host Game page,Host Game,Домакин на играта,主持游戏,主機遊戲,Hostitelská hra,Værtsspil,Gastheerspel,Isäntäpeli,Partie hôte,Gastspiel,मेज़बान खेल,Gazdajáték,Ospita il gioco,ホストゲーム,호스트 게임,Vertsspill,Gra gospodarza,Jogo anfitrião,Jogo anfitrião,Găzduire joc,Хост-игра,Hostiteľská hra,Juego de acogida,Värdspel,Sunucu Oyunu,Ведуча гра +host/name,Server name field placeholder,Server Name,Име на сървъра,服务器名称,伺服器名稱,Název serveru,Server navn,Server naam,Palvelimen nimi,Nom du serveur,Servername,सर्वर का नाम,Szerver név,Nome del server,サーバーの名前,서버 이름,Server navn,Nazwa serwera,Nome do servidor,Nome do servidor,Numele serverului,Имя сервера,Názov servera,Nombre del servidor,Server namn,Sunucu adı,Ім'я сервера +host/name__tooltip,Server name field tooltip,The name of the server that other players will see in the server browser,"Името на сървъра,което другите играчи ще видят в сървърния браузър",其他玩家在服务器浏览器中看到的服务器名称,其他玩家在伺服器瀏覽器中看到的伺服器名稱,"Název serveru, který ostatní hráči uvidí v prohlížeči serveru","Navnet på den server, som andre spillere vil se i serverbrowseren",De naam van de server die andere spelers in de serverbrowser zien,"Palvelimen nimi, jonka muut pelaajat näkevät palvelimen selaimessa",Le nom du serveur que les autres joueurs verront dans le navigateur du serveur,"Der Name des Servers, den andere Spieler im Serverbrowser sehen",सर्वर का नाम जो अन्य खिलाड़ी सर्वर ब्राउज़र में देखेंगे,"A szerver neve, amelyet a többi játékos látni fog a szerver böngészőjében",Il nome del server che gli altri giocatori vedranno nel browser del server,他のプレイヤーがサーバー ブラウザに表示するサーバーの名前,다른 플레이어가 서버 브라우저에서 볼 수 있는 서버 이름,Navnet på serveren som andre spillere vil se i servernettleseren,"Nazwa serwera, którą inni gracze zobaczą w przeglądarce serwerów",O nome do servidor que outros jogadores verão no navegador do servidor,O nome do servidor que os outros jogadores verão no navegador do servidor,The name of the server that other players will see in the server browser,"Имя сервера, которое другие игроки увидят в браузере серверов.","Názov servera, ktorý ostatní hráči uvidia v prehliadači servera",El nombre del servidor que otros jugadores verán en el navegador del servidor.,Namnet på servern som andra spelare kommer att se i serverwebbläsaren,Diğer oyuncuların sunucu tarayıcısında göreceği sunucunun adı,"Назва сервера, яку інші гравці бачитимуть у браузері сервера" +host/password,Password field placeholder,Password (leave blank for no password),Парола (оставете празно за липса на парола),密码(无密码则留空),密碼(無密碼則留空),"Heslo (nechte prázdné, pokud nechcete heslo)",Adgangskode (lad tom for ingen adgangskode),Wachtwoord (leeg laten als er geen wachtwoord is),"Salasana (jätä tyhjäksi, jos et salasanaa)",Mot de passe (laisser vide s'il n'y a pas de mot de passe),"Passwort (leer lassen, wenn kein Passwort vorhanden ist)",पासवर्ड (बिना पासवर्ड के खाली छोड़ें),Jelszó (jelszó nélkül hagyja üresen),Password (lascia vuoto per nessuna password),パスワード (パスワードを使用しない場合は空白のままにします),비밀번호(비밀번호가 없으면 비워두세요),Passord (la det stå tomt for ingen passord),"Hasło (pozostaw puste, jeśli nie ma hasła)",Senha (deixe em branco se não houver senha),Palavra-passe (deixe em branco se não existir palavra-passe),Parola (lasa necompletat pentru nicio parola),"Пароль (оставьте пустым, если пароль отсутствует)","Heslo (nechávajte prázdne, ak nechcete zadať heslo)",Contraseña (dejar en blanco si no hay contraseña),Lösenord (lämna tomt för inget lösenord),Şifre (Şifre yoksa boş bırakın),"Пароль (залиште порожнім, якщо немає пароля)" +host/password__tooltip,Password field placeholder,Password for joining the game. Leave blank if no password is required,"Парола за присъединяване към играта. Оставете празно, ако не се изисква парола",加入游戏的密码。如果不需要密码则留空,加入遊戲的密碼。如果不需要密碼則留空,"Heslo pro vstup do hry. Pokud není vyžadováno heslo, ponechte prázdné","Adgangskode for at deltage i spillet. Lad stå tomt, hvis der ikke kræves adgangskode",Wachtwoord voor deelname aan het spel. Laat dit leeg als er geen wachtwoord vereist is,"Salasana peliin liittymiseen. Jätä tyhjäksi, jos salasanaa ei vaadita",Mot de passe pour rejoindre le jeu. Laissez vide si aucun mot de passe n'est requis,"Passwort für die Teilnahme am Spiel. Lassen Sie das Feld leer, wenn kein Passwort erforderlich ist",गेम में शामिल होने के लिए पासवर्ड. यदि पासवर्ड की आवश्यकता नहीं है तो खाली छोड़ दें,"Jelszó a játékhoz való csatlakozáshoz. Ha nincs szükség jelszóra, hagyja üresen",Password per partecipare al gioco. Lascia vuoto se non è richiesta alcuna password,ゲームに参加するためのパスワード。パスワードが必要ない場合は空白のままにしてください,게임에 참여하기 위한 비밀번호입니다. 비밀번호가 필요하지 않으면 비워두세요,Passord for å bli med i spillet. La det stå tomt hvis du ikke trenger passord,"Hasło umożliwiające dołączenie do gry. Pozostaw puste, jeśli hasło nie jest wymagane",Senha para entrar no jogo. Deixe em branco se nenhuma senha for necessária,Palavra-passe para entrar no jogo. Deixe em branco se não for necessária nenhuma palavra-passe,Parola pentru a intra in joc. Lăsați necompletat dacă nu este necesară o parolă,"Пароль для входа в игру. Оставьте пустым, если пароль не требуется","Heslo pre vstup do hry. Ak heslo nie je potrebné, ponechajte pole prázdne",Contraseña para unirse al juego. Déjelo en blanco si no se requiere contraseña,Lösenord för att gå med i spelet. Lämna tomt om inget lösenord krävs,Oyuna katılmak için şifre. Şifre gerekmiyorsa boş bırakın,"Пароль для входу в гру. Залиште поле порожнім, якщо пароль не потрібен" +host/public,Public checkbox label,Public Game,Публична игра,公共游戏,公開遊戲,Veřejná hra,Offentligt spil,Openbaar spel,Julkinen peli,Jeu public,Öffentliches Spiel,,Nyilvános Játék,Gioco pubblico,パブリックゲーム,공개 게임,Offentlig spill,Gra publiczna,Jogo Público,Jogo Público,Joc public,Публичная игра,Verejná hra,Juego público,Offentligt spel,Halka Açık Oyun,Громадська гра +host/public__tooltip,Public checkbox tooltip,List this game in the server browser.,Избройте тази игра в браузъра на сървъра.,在服务器浏览器中列出该游戏,在伺服器瀏覽器中列出該遊戲。,Vypište tuto hru v prohlížeči serveru.,List dette spil i serverbrowseren.,Geef dit spel weer in de serverbrowser.,Listaa tämä peli palvelimen selaimeen.,Lister ce jeu dans le navigateur du serveur.,Listen Sie dieses Spiel im Serverbrowser auf.,इस गेम को सर्वर ब्राउज़र में सूचीबद्ध करें।,Listázza ezt a játékot a szerver böngészőjében.,Elenca questo gioco nel browser del server.,このゲームをサーバー ブラウザーにリストします。,서버 브라우저에 이 게임을 나열하세요.,List dette spillet i servernettleseren.,Dodaj tę grę do przeglądarki serwerów.,Liste este jogo no navegador do servidor.,Liste este jogo no browser do servidor.,Listați acest joc în browserul serverului.,Добавьте эту игру в браузер серверов.,Uveďte túto hru v prehliadači servera.,Incluya este juego en el navegador del servidor.,Lista detta spel i serverwebbläsaren.,Bu oyunu sunucu tarayıcısında listeleyin.,Показати цю гру в браузері сервера. +host/public__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +host/details,Details field placeholder,Enter some details about your server,Въведете някои подробности за вашия сървър,输入有关您的服务器的一些详细信息,輸入有關您的伺服器的一些詳細信息,Zadejte nějaké podrobnosti o vašem serveru,Indtast nogle detaljer om din server,Voer enkele gegevens over uw server in,Anna joitain tietoja palvelimestasi,Entrez quelques détails sur votre serveur,Geben Sie einige Details zu Ihrem Server ein,अपने सर्वर के बारे में कुछ विवरण दर्ज करें,Adjon meg néhány adatot a szerveréről,Inserisci alcuni dettagli sul tuo server,サーバーに関する詳細を入力します,서버에 대한 세부 정보를 입력하세요.,Skriv inn noen detaljer om serveren din,Wprowadź kilka szczegółów na temat swojego serwera,Insira alguns detalhes sobre o seu servidor,Introduza alguns detalhes sobre o seu servidor,Introduceți câteva detalii despre serverul dvs,Введите некоторые сведения о вашем сервере,Zadajte nejaké podrobnosti o svojom serveri,Ingrese algunos detalles sobre su servidor,Ange några detaljer om din server,Sunucunuzla ilgili bazı ayrıntıları girin,Введіть деякі відомості про ваш сервер +host/details__tooltip,Details field tooltip,Details about your server visible in the server browser.,"Подробности за вашия сървър, видими в сървърния браузър.",有关服务器的详细信息在服务器浏览器中可见,有關伺服器的詳細資訊在伺服器瀏覽器中可見。,Podrobnosti o vašem serveru viditelné v prohlížeči serveru.,Detaljer om din server er synlige i serverbrowseren.,Details over uw server zichtbaar in de serverbrowser.,Palvelimesi tiedot näkyvät palvelimen selaimessa.,Détails sur votre serveur visibles dans le navigateur de serveurs.,Details zu Ihrem Server im Serverbrowser sichtbar.,आपके सर्वर के बारे में विवरण सर्वर ब्राउज़र में दिखाई देता है।,A szerver böngészőjében láthatók a szerver adatai.,Dettagli sul tuo server visibili nel browser del server.,サーバーブラウザに表示されるサーバーに関する詳細。,서버 브라우저에 표시되는 서버에 대한 세부정보입니다.,Detaljer om serveren din er synlig i servernettleseren.,Szczegóły dotyczące Twojego serwera widoczne w przeglądarce serwerów.,Detalhes sobre o seu servidor visíveis no navegador do servidor.,Detalhes sobre o seu servidor visíveis no browser do servidor.,Detalii despre serverul dvs. vizibile în browserul serverului.,Подробная информация о вашем сервере отображается в браузере серверов.,Podrobnosti o vašom serveri viditeľné v prehliadači servera.,Detalles sobre su servidor visibles en el navegador del servidor.,Detaljer om din server visas i serverwebbläsaren.,Sunucunuzla ilgili ayrıntılar sunucu tarayıcısında görünür.,Детальна інформація про ваш сервер відображається в браузері сервера. +host/max_players,Maximum players slider label,Maximum Players,Максимален брой играчи,最大玩家数,最大玩家數,Maximální počet hráčů,Maksimalt antal spillere,Maximale spelers,Pelaajien enimmäismäärä,Joueurs maximum,Maximale Spielerzahl,अधिकतम खिलाड़ी,Maximális játékosok száma,Giocatori massimi,最大プレイヤー数,최대 플레이어,Maksimalt antall spillere,Maksymalna liczba graczy,Máximo de jogadores,Máximo de jogadores,Jucători maxim,Максимальное количество игроков,Maximálny počet hráčov,Personas máximas,Maximalt antal spelare,Maksimum Oyuncu,Максимальна кількість гравців +host/max_players__tooltip,Maximum players slider tooltip,Maximum players allowed to join the game.,"Максимален брой играчи, разрешени да се присъединят към играта.",允许加入游戏的最大玩家数,允許加入遊戲的最大玩家數。,"Maximální počet hráčů, kteří se mohou připojit ke hře.",Maksimalt antal spillere tilladt at deltage i spillet.,Maximaal aantal spelers dat aan het spel mag deelnemen.,Peliin saa osallistua maksimissaan pelaajia.,Nombre maximum de joueurs autorisés à rejoindre la partie.,"Maximal zulässige Anzahl an Spielern, die dem Spiel beitreten dürfen.",अधिकतम खिलाड़ियों को खेल में शामिल होने की अनुमति।,Maximum játékos csatlakozhat a játékhoz.,Numero massimo di giocatori autorizzati a partecipare al gioco.,ゲームに参加できる最大プレイヤー数。,게임에 참여할 수 있는 최대 플레이어 수입니다.,Maksimalt antall spillere som får være med i spillet.,"Maksymalna liczba graczy, którzy mogą dołączyć do gry.",Máximo de jogadores autorizados a entrar no jogo.,Máximo de jogadores autorizados a entrar no jogo.,Numărul maxim de jucători permis să se alăture jocului.,"Максимальное количество игроков, которым разрешено присоединиться к игре.",Do hry sa môže zapojiť maximálny počet hráčov.,Número máximo de jugadores permitidos para unirse al juego.,Maximalt antal spelare som får gå med i spelet.,Oyuna katılmasına izin verilen maksimum oyuncu.,"Максимальна кількість гравців, які можуть приєднатися до гри." +host/max_players__tooltip_disabled,Unused,,,,,,,,,,,,,,,,,,,,,,,,,, +host/start,Maximum players slider label,Start,Започнете,开始,開始,Start,Start,Begin,alkaa,Commencer,Start,शुरू,Indít!,Inizio,始める,시작,Start,Początek,Começar,Iniciar,start,Начинать,Štart,Comenzar,Start,Başlangıç,Почніть +host/start__tooltip,Maximum players slider tooltip,Start the server.,Стартирайте сървъра.,启动服务器,啟動伺服器。,Spusťte server.,Start serveren.,Start de server.,Käynnistä palvelin.,Démarre le serveur.,Starten Sie den Server.,सर्वर प्रारंभ करें.,Szerver Indul!,Avviare il server.,サーバーを起動します。,서버를 시작합니다.,Start serveren.,Uruchom serwer.,Inicie o servidor.,Inicie o servidor.,Porniți serverul.,Запустите сервер.,Spustite server.,Inicie el servidor.,Starta servern.,Sunucuyu başlatın.,Запустіть сервер. +host/start__tooltip_disabled,Maximum players slider tooltip,Check your settings are valid.,Проверете дали вашите настройки са валидни.,检查您的设置是否有效,檢查您的設定是否有效。,"Zkontrolujte, zda jsou vaše nastavení platná.",Tjek at dine indstillinger er gyldige.,Controleer of uw instellingen geldig zijn.,"Tarkista, että asetuksesi ovat oikein.",Vérifiez que vos paramètres sont valides.,"Überprüfen Sie, ob Ihre Einstellungen gültig sind.",जांचें कि आपकी सेटिंग्स वैध हैं।,"Ellenőrizze, hogy a beállítások érvényesek-e.",Controlla che le tue impostazioni siano valide.,設定が有効であることを確認してください。,설정이 유효한지 확인하세요.,Sjekk at innstillingene dine er gyldige.,"Sprawdź, czy ustawienia są prawidłowe.",Verifique se suas configurações são válidas.,Verifique se as suas definições são válidas.,Verificați că setările dvs. sunt valide.,"Убедитесь, что ваши настройки действительны.","Skontrolujte, či sú vaše nastavenia platné.",Verifique que su configuración sea válida.,Kontrollera att dina inställningar är giltiga.,Ayarlarınızın geçerli olup olmadığını kontrol edin.,Перевірте правильність ваших налаштувань. +host/instructions/first,Instructions for the host 1,"First time hosts, please see the {0}Hosting{1} section of our Wiki.",,"第一次主持游戏的话, 请看我们wiki的{0}Hosting{1} 模块",,,,,,"La première fois que vous hébergez, merci de consulter la section {0}Hébergement{1} sur notre Wiki.",,,"Az első házigazdák, kérjük, tekintse meg Wikink {0}Hosting{1} részét.",,,,,,,,,,,,,, +host/instructions/mod_warning,Instructions for the host 2,Using other mods may cause unexpected behaviour including de-syncs. See {0}Mod Compatibility{1} for more info.,,"同时使用其他模组可能会导致游戏出错,比如物品不同步, 看 {0}Mod Compatibility{1} 模块来获取更多信息",,,,,,"L’utilisation d’autres mods peut causer un comportement inattendu, y-compris des désynchronisation. Consultez les {0}Mods compatibles{1} pour plus d’information.",,,"Más modok használata váratlan viselkedést okozhat, beleértve a szinkronizálást. További információért lásd a {0}Modkompatibilitást{1}.",,,,,,,,,,,,,, +host/instructions/recommend,Instructions for the host 3,It is recommended that other mods are disabled and Derail Valley restarted prior to playing in multiplayer.,,"推荐你卸载其他模组并重启游戏后,再进行联机",,,,,,Il est recommandé de désactiver les autres mods et de redémarrer Derail Valley avant de joueur en multijoueur.,,,"Javasoljuk, hogy tiltsa le a többi modot, és indítsa újra a Derail Valleyt, mielőtt többjátékos módban játszana.",,,,,,,,,,,,,, +host/instructions/signoff,Instructions for the host 4,We hope to have your favourite mods compatible with multiplayer in the future.,,我们希望未来能让你装联机模组的同时也能玩其他模组,,,,,,Nous espérons avoir vos mods favoris compatibles avec le multijoueur dans le futur.,,,"Reméljük, hogy kedvenc modjai a jövőben kompatibilisek lesznek a többjátékos játékkal.",,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Disconnect Reason,,,,,,,,,,,,,,,,,,,,,,,,,, -dr/invalid_password,Invalid password popup.,Invalid Password!,,,,,,,,Mot de passe incorrect !,Ungültiges Passwort!,,,Password non valida!,,,,,,,,,,¡Contraseña invalida!,,, -dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.",,,,,,,,"Version du jeu incompatible ! Version du serveur : {0}, version locale : {1}","Spielversion stimmt nicht überein! Server Version: {0}, Lokale Version: {1}.",,,"Versioni del gioco non combacianti! Versione del Server: {0}, La tua versione: {1}.",,,,,,,,,,"¡La versión del juego no coincide! Versión del servidor: {0}, tu versión: {1}.",,, -dr/full_server,The server is already full.,The server is full!,,,,,,,,Le serveur est complet !,Der Server ist voll!,,,Il Server è pieno!,,,,,,,,,,¡El servidor está lleno!,,, -dr/mods,"The client is missing, or has extra mods.",Mod mismatch!,,,,,,,,Mod incompatible !,Mods stimmen nicht überein!,,,Mod non combacianti!,,,,,,,,,,"Falta el cliente, o tiene modificaciones adicionales.",,, -dr/mods_missing,The list of missing mods.,Missing Mods:\n- {0},,,,,,,,Mods manquants:\n-{0},Fehlende Mods:\n- {0},,,Mod Mancanti:\n- {0},,,,,,,,,,Mods faltantes:\n- {0},,, -dr/mods_extra,The list of extra mods.,Extra Mods:\n- {0},,,,,,,,Mods extras:\n-{0},Zusätzliche Mods:\n- {0},,,Mod Extra:\n- {0},,,,,,,,,,Modificaciones adicionales:\n- {0},,, +dr/invalid_password,Invalid password popup.,Invalid Password!,Невалидна парола!,无效的密码!,無效的密碼!,Neplatné heslo!,Forkert kodeord!,Ongeldig wachtwoord!,Väärä salasana!,Mot de passe incorrect !,Ungültiges Passwort!,अवैध पासवर्ड!,Érvénytelen jelszó!,Password non valida!,無効なパスワード!,유효하지 않은 비밀번호!,Ugyldig passord!,Nieprawidłowe hasło!,Senha inválida!,Verifique se as suas definições são válidas.,Parolă Invalidă!,Неверный пароль!,Nesprávne heslo!,¡Contraseña invalida!,Felaktigt lösenord!,Geçersiz şifre!,Невірний пароль! +dr/game_version,Different game versions.,"Game version mismatch! Server version: {0}, your version: {1}.","Несъответствие на версията на играта! Версия на сървъра: {0}, вашата версия: {1}.","游戏版本不匹配!服务器版本:{0},您的版本:{1}。","遊戲版本不符!伺服器版本:{0},您的版本:{1}。","Nesoulad verze hry! Verze serveru: {0}, vaše verze: {1}.","Spilversionen stemmer ikke overens! Serverversion: {0}, din version: {1}.","Spelversie komt niet overeen! Serverversie: {0}, jouw versie: {1}.","Peliversio ei täsmää! Palvelimen versio: {0}, sinun versiosi: {1}.","Version du jeu incompatible ! Version du serveur : {0}, version locale : {1}","Spielversion stimmt nicht überein! Server Version: {0}, Lokale Version: {1}.","गेम संस्करण बेमेल! सर्वर संस्करण: {0}, आपका संस्करण: {1}.","Nem egyezik a játék verziója! Szerververzió: {0}, az Ön verziója: {1}.","Versioni del gioco non combacianti! Versione del Server: {0}, La tua versione: {1}.",ゲームのバージョンが不一致です!サーバーのバージョン: {0}、あなたのバージョン: {1}。,"게임 버전이 일치하지 않습니다! 서버 버전: {0}, 귀하의 버전: {1}.","Spillversjonen samsvarer ikke! Serverversjon: {0}, din versjon: {1}.","Niezgodna wersja gry! Wersja serwera: {0}, Twoja wersja: {1}.","Incompatibilidade de versão do jogo! Versão do servidor: {0}, sua versão: {1}.","Incompatibilidade de versão do jogo! Versão do servidor: {0}, a sua versão: {1}.","Versiunea jocului nepotrivită! Versiunea serverului: {0}, versiunea dvs.: {1}.","Несоответствие версии игры! Версия сервера: {0}, ваша версия: {1}.","Nesúlad verzie hry! Verzia servera: {0}, vaša verzia: {1}.","¡La versión del juego no coincide! Versión del servidor: {0}, tu versión: {1}.","Spelversionen matchar inte! Serverversion: {0}, din version: {1}.","Oyun sürümü uyuşmazlığı! Sunucu sürümü: {0}, sürümünüz: {1}.","Невідповідність версії гри! Версія сервера: {0}, ваша версія: {1}." +dr/full_server,The server is already full.,The server is full!,Сървърът е пълен!,服务器已满!,伺服器已滿!,Server je plný!,Serveren er fuld!,De server is vol!,Palvelin täynnä!,Le serveur est complet !,Der Server ist voll!,सर्वर पूर्ण है!,Tele a szerver!,Il Server è pieno!,サーバーがいっぱいです!,서버가 꽉 찼어요!,Serveren er full!,Serwer jest pełny!,O servidor está cheio!,O servidor está cheio!,Serverul este plin!,Сервер переполнен!,Server je plný!,¡El servidor está lleno!,Servern är full!,Sunucu dolu!,Сервер заповнений! +dr/mods,"The client is missing, or has extra mods.",Mod mismatch!,Несъответствие на мода!,模组不匹配!,模組不符!,Neshoda modů!,Mod uoverensstemmelse!,Mod-mismatch!,Modi ei täsmää!,Mod incompatible !,Mods stimmen nicht überein!,मॉड बेमेल!,Mod eltérés!,Mod non combacianti!,モジュールが不一致です!,모드 불일치!,Moduoverensstemmelse!,Niezgodność modów!,Incompatibilidade de mod!,Incompatibilidade de mod!,Nepotrivire mod!,Несоответствие модов!,Nezhoda modov!,"Falta el cliente, o tiene modificaciones adicionales.",Mod-felmatchning!,Mod uyumsuzluğu!,Невідповідність модів! +dr/mods_missing,The list of missing mods.,Missing Mods:\n- {0},Липсващи модификации:\n- {0},缺少模组:\n- {0},缺少模組:\n- {0},Chybějící mody:\n- {0},Manglende mods:\n- {0},Ontbrekende mods:\n- {0},Puuttuvat modit:\n- {0},Mods manquants :\n- {0},Fehlende Mods:\n- {0},गुम मॉड्स:\n- {0},Hiányzó modok:\n- {0},Mod Mancanti:\n- {0},不足している MOD:\n- {0},누락된 모드:\n- {0},Manglende modi:\n- {0},Brakujące mody:\n- {0},Modificações ausentes:\n- {0},Modificações em falta:\n- {0},Moduri lipsă:\n- {0},Отсутствующие моды:\n- {0},Chýbajúce modifikácie:\n- {0},Mods faltantes:\n- {0},Mods saknas:\n- {0},Eksik Modlar:\n- {0},Відсутні моди:\n- {0} +dr/mods_extra,The list of extra mods.,Extra Mods:\n- {0},Допълнителни модификации:\n- {0},额外模组:\n- {0},額外模組:\n- {0},Extra modifikace:\n- {0},Ekstra mods:\n- {0},Extra aanpassingen:\n- {0},Lisämodit:\n- {0},Mods en trop :\n- {0},Zusätzliche Mods:\n- {0},अतिरिक्त मॉड:\n- {0},Extra modok:\n- {0},Mod Extra:\n- {0},追加の Mod:\n- {0},추가 모드:\n- {0},Ekstra modi:\n- {0},Dodatkowe mody:\n- {0},Modificações extras:\n- {0},Modificações extra:\n- {0},Moduri suplimentare:\n- {0},Дополнительные моды:\n- {0},Extra modifikácie:\n- {0},Modificaciones adicionales:\n- {0},Extra mods:\n- {0},Ekstra Modlar:\n- {0},Додаткові моди:\n- {0} +dr/disconnect/unreachable,Host Unreachable error message,Host Unreachable,,无法找到房主,,,,,,Hôte injoignable,,,A házigazda elérhetetlen,,,,,,,,,,,,,, +dr/disconnect/unknown,Unknown Host error message,Unknown Host,,房主未知,,,,,,Hôte inconnu,,,Ismeretlen gazda,,,,,,,,,,,,,, +dr/disconnect/kicked,Player Kicked error message,Player Kicked,,玩家已被踢出,,,,,,Joueur éjecté,,,Játékos kirúgva,,,,,,,,,,,,,, +dr/disconnect/rejected,Rejected! error message,Rejected!,,你已被拒绝加入服务器!,,,,,,Rejeté !,,,Elutasítva!,,,,,,,,,,,,,, +dr/disconnect/shutdown,Server Shutting Down error message,Server Shutting Down,,服务器已经关闭,,,,,,Arrêt du serveur,,,Szerver leállás,,,,,,,,,,,,,, +dr/disconnect/timeout,Server Timed out,Server Timed out,,服务器连接超时,,,,,,Le serveur n’a pas répondu à temps,,,Szerver időtúllépés,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Career Manager,,,,,,,,,,,,,,,,,,,,,,,,,, -carman/fees_host_only,Text shown when a client tries to manage fees.,Only the host can manage fees!,,,,,,,,Seul l'hôte peut gérer les frais !,Nur der Host kann Gebühren verwalten!,,,Solo l’Host può gestire gli addebiti!,,,,,,,,,,¡Solo el anfitrión puede administrar las tarifas!,,, +carman/fees_host_only,Text shown when a client tries to manage fees.,Only the host can manage fees!,Само домакинът може да управлява таксите!,只有房东可以管理费用!,只有房東可以管理費用!,Poplatky může spravovat pouze hostitel!,Kun værten kan administrere gebyrer!,Alleen de host kan de kosten beheren!,Vain isäntä voi hallita maksuja!,Seul l'hôte peut gérer les frais !,Nur der Host kann Gebühren verwalten!,केवल मेज़बान ही फीस का प्रबंधन कर सकता है!,Csak a házigazda kezelheti a díjakat!,Solo l’Host può gestire gli addebiti!,料金を管理できるのはホストだけです。,호스트만이 수수료를 관리할 수 있습니다!,Bare verten kan administrere gebyrer!,Tylko gospodarz może zarządzać opłatami!,Somente o anfitrião pode gerenciar as taxas!,Só o anfitrião pode gerir as taxas!,Doar gazda poate gestiona taxele!,Только хозяин может управлять комиссией!,Poplatky môže spravovať iba hostiteľ!,¡Solo el anfitrión puede administrar las tarifas!,Endast värden kan hantera avgifter!,Ücretleri yalnızca ev sahibi yönetebilir!,Тільки господар може керувати оплатою! ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Player List,,,,,,,,,,,,,,,,,,,,,,,,,, -plist/title,The title of the player list.,Online Players,,,,,,,,Joueurs en ligne,Verbundene Spieler,,,Giocatori Online,,,,,,,,,,Jugadores en línea,,, +plist/title,The title of the player list.,Online Players,Онлайн играчи,在线玩家,線上玩家,Online hráči,Online spillere,Online spelers,Online-pelaajat,Joueurs en ligne,Verbundene Spieler,ऑनलाइन खिलाड़ी,Online játékosok,Giocatori Online,,온라인 플레이어,Online spillere,Gracze sieciowi,Jogadores on-line,Jogadores on-line,Jucători online,Онлайн-игроки,Online hráči,Jugadores en línea,Spelare online,Çevrimiçi Oyuncular,Онлайн гравці ,,,,,,,,,,,,,,,,,,,,,,,,,,, ,Loading Info,,,,,,,,,,,,,,,,,,,,,,,,,, -linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to load,,,,,,,,En attente du chargement du serveur,Warte auf das Laden des Servers,,,In attesa del caricamento del Server,,,,,,,,,,Esperando a que cargue el servidor...,,, -linfo/sync_world_state,Text shown in the loading screen.,Syncing world state,,,,,,,,Synchronisation des données du monde,Synchronisiere Daten,,,Sincronizzazione dello stato del mondo,,,,,,,,,,Sincronizando estado global,,, +linfo/wait_for_server,Text shown in the loading screen.,Waiting for server to load,Изчаква се зареждане на сървъра,等待服务器加载,等待伺服器加載,Čekání na načtení serveru,"Venter på, at serveren indlæses",Wachten tot de server is geladen,Odotetaan palvelimen latautumista,En attente du chargement du serveur,Warte auf das Laden des Servers,सर्वर लोड होने की प्रतीक्षा की जा रही है,Várakozás a szerver betöltésére,In attesa del caricamento del Server,サーバーがロードされるのを待っています,서버가 로드되기를 기다리는 중,Venter på at serveren skal lastes,Czekam na załadowanie serwera,Esperando o servidor carregar,sperando que o servidor carregue,Se așteaptă încărcarea serverului,Ожидание загрузки сервера,Čaká sa na načítanie servera,Esperando a que cargue el servidor...,Väntar på att servern ska laddas,Sunucunun yüklenmesi bekleniyor,Очікування завантаження сервера +linfo/sync_world_state,Text shown in the loading screen.,Syncing world state,Синхронизиране на световното състояние,同步世界状态,同步世界狀態,Synchronizace světového stavu,Synkroniserer verdensstaten,Het synchroniseren van de wereldstaat,Synkronoidaan maailmantila,Synchronisation des données du monde,Synchronisiere Daten,सिंक हो रही विश्व स्थिति,Szinkronizáló világállapot,Sincronizzazione dello stato del mondo,世界状態を同期しています,세계 상태 동기화 중,Synkroniserer verdensstaten,Synchronizacja stanu świata,Sincronizando o estado mundial,Sincronizando o estado mundial,Sincronizarea stării mondiale,Синхронизация состояния мира,Synchronizácia svetového štátu,Sincronizando estado global,Synkroniserar världsstaten,Dünya durumunu senkronize etme,Синхронізація стану світу +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Chat,,,,,,,,,,,,,,,,,,,,,,,,,, +chat/placeholder,Chat input placeholder,Type a message and press Enter!,,"在此输入文字,按回车发送",,,,,,Tapez un message et appuyez sur Entrée !,,,Írjon be egy üzenetet és nyomja meg az Entert!,,,,,,,,,,,,,, +chat/help/available,Chat help info available commands,Available commands:,,可用命令:,,,,,,Commandes disponibles :,,,Elérhető parancsok:,,,,,,,,,,,,,, +chat/help/servermsg,Chat help send message as server,Send a message as the server (host only),,以服务器的身份发消息(仅限房主),,,,,,Envoyer un message au nom du serveur (hôte uniquement),,,Üzenet küldése szerverként (csak gazdagép),,,,,,,,,,,,,, +chat/help/whispermsg,Chat help whisper to a player,Whisper to a player,,向一位玩家说悄悄话,,,,,,Chuchoter à un joueur,,,Suttogj egy játékosnak,,,,,,,,,,,,,, +chat/help/help,Chat help show help,Display this help message,,展示此帮助信息,,,,,,Afficher ce message d’aide,,,Jelenítse meg ezt a súgóüzenetet,,,,,,,,,,,,,, +chat/help/msg,Chat help parameter e.g. /s ,message,,信息,,,,,,message,,,Üzenet,,,,,,,,,,,,,, +chat/help/playername,Chat help parameter e.g. /w ,player name,,玩家名字,,,,,,nom du joueur,,,Játékos neve,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,, +,Pause Menu,,,,,,,,,,,,,,,,,,,,,,,,,, +pm/disconnect_msg,Message when disconnecting from server (back to main menu),Disconnect and return to main menu?,,确定要断开连接并退回到主界面吗?,,,,,,Se déconnecter et revenir au menu principal ?,,,Leválasztás és visszatérés a főmenübe?,,,,,,,,,,,,,, +pm/quit_msg,Message when disconnecting from server (quit game),Disconnect and quit?,,确定要断开连接并直接退出吗?,,,,,,Se déconnecter et quitter ?,,,Lekapcsolja és kilép?,,,,,,,,,,,,,, diff --git a/package.ps1 b/package.ps1 deleted file mode 100644 index 474e0e5..0000000 --- a/package.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -param ( - [switch]$NoArchive, - [string]$OutputDirectory = $PSScriptRoot -) - -Set-Location "$PSScriptRoot" -$FilesToInclude = "build/*" - -$modInfo = Get-Content -Raw -Path "info.json" | ConvertFrom-Json -$modId = $modInfo.Id -$modVersion = $modInfo.Version - -$DistDir = "$OutputDirectory/dist" -if ($NoArchive) { - $ZipWorkDir = "$OutputDirectory" -} else { - $ZipWorkDir = "$DistDir/tmp" -} -$ZipOutDir = "$ZipWorkDir/$modId" - -New-Item "$ZipOutDir" -ItemType Directory -Force -Copy-Item -Force -Path $FilesToInclude -Destination "$ZipOutDir" - -if (!$NoArchive) -{ - $FILE_NAME = "$DistDir/${modId}_v$modVersion.zip" - Compress-Archive -Update -CompressionLevel Fastest -Path "$ZipOutDir/*" -DestinationPath "$FILE_NAME" -} diff --git a/post-build.ps1 b/post-build.ps1 new file mode 100644 index 0000000..ae93e0b --- /dev/null +++ b/post-build.ps1 @@ -0,0 +1,50 @@ +param +( + [switch]$NoArchive, + [string]$GameDir, + [string]$Target, + [string]$Ver +) + +Write-Host "Root: $PSScriptRoot" +Write-Host "No Archive: $NoArchive" +Write-Host "Target: $Target" +Write-Host "Game Dir: $GameDir" +Write-Host "Version: $Ver" + +$compress + +#Update the JSON +$json = Get-Content ($PSScriptRoot + '/info.json') -raw | ConvertFrom-Json +$json.Version = $Ver +$modId = $json.Id +$json | ConvertTo-Json -depth 32| set-content ($PSScriptRoot + '/info.json') + +#Copy files to Build Dir +Copy-Item ($PSScriptRoot + '/info.json') -Destination ("$PSScriptRoot/build/") +Copy-Item ($Target) -Destination ("$PSScriptRoot/build/") +Copy-Item ($PSScriptRoot + '/LICENSE') -Destination ("$PSScriptRoot/build/") + +#Copy files to Game Dir +if (!(Test-Path ($GameDir))) { + New-Item -ItemType Directory -Path $GameDir +} +Copy-Item ("$PSScriptRoot/build/*") -Destination ($GameDir) + + +#Files to be compressed if we make a zip +$compress = @{ + Path = ($PSScriptRoot + "/build/*") + CompressionLevel = "Fastest" + DestinationPath = ($PSScriptRoot + "/Releases/$modId $Ver.zip") +} + +#Are we building a release or debug? +if (!$NoArchive){ + if (!(Test-Path ($PSScriptRoot + "/releases"))) { + New-Item -ItemType Directory -Path ($PSScriptRoot + "/releases") + } + + Write-Host "Zip Path: " $compress.DestinationPath + Compress-Archive @compress -Force +} \ No newline at end of file